Skip to content

WIP: Upgrade Tailwind V3 to V4 - #594

Open
tarunsinghofficial wants to merge 14 commits into
developfrom
feat/tailwind-v4
Open

tarunsinghofficial wants to merge 14 commits into
developfrom
feat/tailwind-v4

Conversation

@tarunsinghofficial

@tarunsinghofficial tarunsinghofficial commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrates Wayfinder from Tailwind CSS v3.4 to v4, using the @tailwindcss/vite plugin and keeping the existing JS config via @config so env-var branding (generatePalette(), brand/surface colors) continues to work unchanged.

Closes #353

What changed

Build pipeline

  • Install tailwindcss@4 + @tailwindcss/vite
  • Wire Tailwind into vite.config.js; remove postcss.config.js, autoprefixer, and @tailwindcss/line-clamp (built into v4)
  • Bump @tailwindcss/forms and prettier-plugin-tailwindcss

CSS / config

  • Replace @tailwind directives with @import "tailwindcss" + @config "../tailwind.config.js"
  • Add @custom-variant dark using :is(.dark, .dark *) so dark utilities beat light ones on the same node (fixes search pane in dark mode)
  • Adapt tailwind.config.js for ESM plugin imports

Codemod + fixes

  • Run @tailwindcss/upgrade for v4 utility renames (bg-gradient-to-*bg-linear-to-*, break-wordswrap-break-word, etc.)
  • Add @reference in StopMarker.svelte for @apply in component styles
  • Add maplibre-gl as a direct dependency (peer of @maplibre/maplibre-gl-leaflet, dropped by --legacy-peer-deps installs)

Flowbite

Spiked flowbite-svelte@1.33 — needs Svelte 5.40+, Datepicker build/API churn, and component API updates across 6 files. flowbite-svelte@0.47 works with Tailwind v4 (build + UI verified), so it stays on 0.47 for this PR. A Flowbite 1.x upgrade can be a follow-up issue.

Components in use: Modal, Button, Tabs/TabItem, Datepicker, Radio, Checkbox, Input (6 files).

Visual QA

Compared 8 dark-mode before/after screenshots (search pane, All Routes, route detail, map pins, etc.) — no visible regressions after the dark-mode specificity fix.

Test plan

  • npm run lint
  • npx vitest run (103 files, 1794 tests)
  • npm run build

Summary by CodeRabbit

  • Refactor

    • Updated styling for compatibility with Tailwind CSS 4.
    • Improved text wrapping for route names and badges.
    • Refined light and dark theme colors across route, stop, search, and itinerary views.
    • Updated menu, header, toast, accordion, and skip-link positioning.
  • Bug Fixes

    • Improved dark-mode backgrounds in search panels.
    • Standardized gradient and layout styling across loading, error, and itinerary screens.
  • Documentation

    • Updated arrival and departure redesign specifications to reflect revised text wrapping.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tarunsinghofficial, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fb69dff-52fd-448f-93f7-d1f32b580fdd

📥 Commits

Reviewing files that changed from the base of the PR and between 26a4042 and a383acf.

⛔ Files ignored due to path filters (10)
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.02.32 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.02.40 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.02.45 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.02.54 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.03.00 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.03.12 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.03.52 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.03.58 PM.png is excluded by !**/*.png
  • docs/tailwind-bef/Screenshot 2026-08-20 at 9.04.06 PM.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (48)
  • .github/workflows/lint.yml
  • docs/superpowers/plans/2026-07-22-arrival-departure-redesign.md
  • docs/superpowers/plans/2026-07-23-map-stop-selection.md
  • docs/superpowers/plans/2026-08-07-survey-banner.md
  • docs/superpowers/specs/2026-07-22-arrival-departure-redesign-design.md
  • package.json
  • src/app.css
  • src/components/ArrivalDeparture.svelte
  • src/components/FullPageLoadingSpinner.svelte
  • src/components/LoadingSpinner.svelte
  • src/components/MapExperience.svelte
  • src/components/RouteBadge.svelte
  • src/components/StopItem.svelte
  • src/components/map/ContextMenuPopup.svelte
  • src/components/map/PopupContent.svelte
  • src/components/map/RouteLegend.svelte
  • src/components/map/StopMarker.svelte
  • src/components/map/VehiclePopupContent.svelte
  • src/components/navigation/AlertsModal.svelte
  • src/components/navigation/BottomSheet.svelte
  • src/components/navigation/Header.svelte
  • src/components/navigation/LanguageSwitcher/LanguageSwitcher.svelte
  • src/components/navigation/OverflowMenu.svelte
  • src/components/oba/TripDetailsPane.svelte
  • src/components/routes/RouteModal.svelte
  • src/components/routes/ViewAllRoutesModal.svelte
  • src/components/search/CollapsedSearchField.svelte
  • src/components/search/SearchField.svelte
  • src/components/search/SearchPane.svelte
  • src/components/search/SearchResultItem.svelte
  • src/components/service-alerts/ServiceAlertItem.svelte
  • src/components/service-alerts/ServiceAlerts.svelte
  • src/components/service-alerts/__tests__/ServiceAlerts.test.js
  • src/components/stops/StopPageHeader.svelte
  • src/components/stops/StopPane.svelte
  • src/components/surveys/SurveyBanner.svelte
  • src/components/surveys/SurveyLauncher.svelte
  • src/components/surveys/SurveyModal.svelte
  • src/components/surveys/SurveyQuestion.svelte
  • src/components/trip-planner/ItineraryDetails.svelte
  • src/components/trip-planner/LegDetails.svelte
  • src/components/trip-planner/RecentTripsList.svelte
  • src/components/trip-planner/TripOptionsModal.svelte
  • src/components/trip-planner/TripPlan.svelte
  • src/routes/+error.svelte
  • src/routes/+layout.svelte
  • src/routes/stops/[stopID]/schedule/+page.svelte
  • vite.config.js

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 00f5c97b-9f16-4b67-9c3f-aebba66bd5b9

📥 Commits

Reviewing files that changed from the base of the PR and between 1fd4dd8 and 26a4042.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • docs/superpowers/plans/2026-07-22-arrival-departure-redesign.md
  • docs/superpowers/specs/2026-07-22-arrival-departure-redesign-design.md
  • package.json
  • postcss.config.js
  • src/app.css
  • src/components/FullPageLoadingSpinner.svelte
  • src/components/RouteBadge.svelte
  • src/components/RouteItem.svelte
  • src/components/StopItem.svelte
  • src/components/__tests__/RouteItem.test.js
  • src/components/containers/AccordionItem.svelte
  • src/components/map/StopMarker.svelte
  • src/components/navigation/Header.svelte
  • src/components/navigation/LanguageSwitcher/LanguageSwitcher.svelte
  • src/components/navigation/OverflowMenu.svelte
  • src/components/notification/Toast.svelte
  • src/components/search/SearchField.svelte
  • src/components/search/SearchPane.svelte
  • src/components/stops/__tests__/StopItem.test.js
  • src/components/trip-planner/ItineraryDetails.svelte
  • src/routes/+error.svelte
  • src/routes/+layout.svelte
  • vite.config.js
💤 Files with no reviewable changes (1)
  • postcss.config.js

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Tailwind CSS 4 migration

Layer / File(s) Summary
Tailwind CSS 4 build and theme setup
package.json, vite.config.js, src/app.css, src/components/map/StopMarker.svelte
Tailwind CSS 4 replaces Tailwind CSS 3. Vite now loads the Tailwind plugin. Global CSS uses the new import and dark variant syntax.
Tailwind utility syntax migration
src/components/FullPageLoadingSpinner.svelte, src/components/RouteBadge.svelte, docs/superpowers/..., src/components/containers/AccordionItem.svelte, src/components/navigation/..., src/components/notification/Toast.svelte, src/components/search/SearchField.svelte, src/components/trip-planner/ItineraryDetails.svelte, src/routes/...
Components and related documentation replace changed Tailwind utility forms for gradients, wrapping, positioning, z-index, and growth behavior.
Theme-aware component updates
src/components/RouteItem.svelte, src/components/StopItem.svelte, src/components/search/SearchPane.svelte, src/components/**/__tests__/*
Route and stop text styles use updated theme utilities. Search pane dark background placement changes. Matching test assertions are updated.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 26a40

This dependency and build-tooling upgrade has no actionable merge-blocking risk remaining based on the supplied evidence and is merge-ready after normal checks and review.

Suggested reviewers: aaronbrethorst, ahmedhossamdev

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: upgrading the project from Tailwind CSS v3 to v4.
Linked Issues check ✅ Passed The changes address issue #353 through Tailwind v4 tooling, plugin cleanup, CSS migration, configuration retention, and utility updates.
Out of Scope Changes check ✅ Passed All listed changes support the Tailwind CSS v4 migration or its required compatibility and styling updates.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tailwind-v4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

flowbite-svelte-icons@1.6.2 peers tailwindcss ^3.3.2, which breaks
plain npm install on Tailwind v4. The package is not imported anywhere.
@tarunsinghofficial
tarunsinghofficial marked this pull request as ready for review August 23, 2026 07:21
maplibre-gl pulls @mapbox/jsonlint-lines-primitives@2.0.3, which
requires Node >=22. With engine-strict, npm install fails on CI
(Node 18 lint / Node 20 test). Override to 2.0.2.
Tailwind v4 pulls @tailwindcss/oxide, which requires Node >=20.
@coveralls

coveralls commented Aug 23, 2026

Copy link
Copy Markdown

Coverage Status

Coverage is 93.106%feat/tailwind-v4 into develop. No base build found for develop.

0.8.1 crashes with "a.startsWith is not a function" on non-Svelte
files, making prettier --check exit 2 (lint fails in CI). 0.6.14
still sorts Tailwind v4 classes. Reformat affected files.
Import Flowbite components from subpath exports instead of the package
barrel, and rewrite internal barrel imports in vite.config.js. Prevents
Vite SSR crash after dependency re-optimization on Tailwind v4.
@aaronbrethorst

Copy link
Copy Markdown
Member

Code review

Found 3 issues:

  1. bg-opacity-* / ring-opacity-* were removed in Tailwind v4 and three of them survive the codemod on lines this PR touched, so they now emit no CSS at all. Verified by compiling tailwindcss@4.3.3 with an equivalent @config legacy config: neither class produces a rule. The loading overlay below loses its 80% transparency and becomes a solid bg-neutral-800 sheet over the map; bg-brand-accent bg-opacity-80 in src/components/routes/RouteModal.svelte#L80 becomes fully opaque; focus:ring-opacity-50 in src/components/surveys/SurveyLauncher.svelte#L43 is a no-op. These need the v4 opacity-modifier form (bg-neutral-800/80, focus:ring-green-400/50). Note that src/components/__tests__/LoadingSpinner.test.js#L84 still asserts bg-opacity-80 as a class string, so the suite stays green while the style is dead.

aria-live="polite"
class="bg-opacity-80 absolute inset-0 z-50 flex items-center justify-center bg-neutral-800 md:rounded-lg"
>

  1. The v4 preflight changes the default border color from gray-200 to currentColor (*, ::after, ... { border: 0 solid }), and no compatibility base layer was added to src/app.css to preserve the old default. Three elements on lines this PR modified use a bare border with no color class, all in header chrome: the nav-link chip below, the overflow-menu button at src/components/navigation/Header.svelte#L265, and the language-switcher button at src/components/navigation/LanguageSwitcher/LanguageSwitcher.svelte#L134. The first two inherit the header's text-brand-foreground (white by default), so their borders go from gray to effectively invisible against bg-surface/80; the switcher inherits text-surface-foreground and goes to a hard black border. This is the area commits 7194fd8 and 2712902 were recently tuning for contrast. Either add the border-color: var(--color-gray-200, currentColor) compat layer that @tailwindcss/upgrade normally injects, or give these three an explicit border color. The visual QA in the description was dark-mode only, which is why this wouldn't have shown up.

{#each visibleLinks as { key, value }}
<div class="bg-surface/80 dark:bg-surface-dark flex-shrink-0 rounded-md border">
<a

  1. docs/tailwind-bef/ adds 9 PNGs totalling ~18 MB to the repo, with raw macOS screenshot filenames (including the narrow no-break space characters). Nothing references them and there is no matching "after" set. These look like the QA scratch artifacts mentioned in the description rather than intended documentation; once merged they are in the history permanently.

https://github.com/OneBusAway/wayfinder/tree/a383acfb02ba3a94753f1b849d1b1027a94e8812/docs/tailwind-bef

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@aaronbrethorst aaronbrethorst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structural decisions here are all the ones I'd have made. Moving to @tailwindcss/vite and dropping postcss.config.js/autoprefixer, keeping tailwind.config.js alive through the @config bridge so the COLOR_*generatePalette() pipeline keeps working, and the @custom-variant dark (&:is(.dark, .dark *)) fix for v4's dark-mode specificity — that's a well-reasoned path through a migration that could easily have turned into a rewrite. Deferring the Flowbite upgrade and paying for it with a scoped Vite patch is a defensible trade too, and it's honestly commented.

The title says WIP and I think that's accurate. Three things need fixing, and two of them silently change what renders.

1. Dead opacity utilities survive the codemod

bg-opacity-* and ring-opacity-* are on v4's removed-deprecated list — they emit no CSS at all, so these fail silently rather than erroring:

  • LoadingSpinner.svelte:9bg-opacity-80 on bg-neutral-800. The loading overlay becomes fully opaque instead of 80%, so the map disappears behind it during every load.
  • RouteModal.svelte:80bg-opacity-80 on bg-brand-accent. Same, on the route modal header.
  • SurveyLauncher.svelte:43focus:ring-opacity-50.

The replacement is the slash modifier: bg-neutral-800/80, bg-brand-accent/80, focus:ring-green-400/50. Worth grepping the whole tree for the rest of that family (text-opacity-*, border-opacity-*, divide-opacity-*, placeholder-opacity-*) since the codemod clearly didn't catch them all.

2. v4's default border color change isn't compensated

In v3, border defaulted to gray-200. In v4 it defaults to currentColor, so every bare border with no explicit color now draws in the element's text color. src/app.css has no base layer restoring the old behavior, and there are bare border classes on lines this PR modified — several in the LanguageSwitcher / Header dropdown region:

rounded-md border            (flex-shrink-0 wrapper)
rounded-md border            (8x8 button)
rounded-md border px-2       (font-semibold trigger)

Those pick up the surrounding text color, which on the dark surface means a border that either changes color or effectively vanishes. This is also almost certainly why it slipped through — the QA pass was dark-mode only.

The upgrade guide's own compat snippet is the cheapest fix, and it goes in app.css:

@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }
}

Auditing every bare border and giving it an explicit color is the better long-term answer, but for a migration PR I'd take the compat layer and do the audit separately.

3. 19 MB of screenshots got committed

docs/tailwind-bef/ has nine PNGs totaling about 19 MB — raw macOS screenshots with timestamp filenames, no "after" set, and nothing referencing them. Those want to be in the PR description, not in git history where they're permanent. Please drop them from the branch.

Smaller notes

  • The PR body says "Adapt tailwind.config.js for ESM plugin imports," but that file is byte-identical to developrequire('@tailwindcss/forms') and all. That's harmless (jiti handles require in an ESM config under @config), but the description should match the diff.
  • focus:outline-none is still used in ~14 places. In v4 that's outline-hidden; plain outline-none drops v3's forced-colors fallback. Most sites pair it with a visible focus:ring-*, so the loss is confined to forced-colors mode — low priority, but it's part of the same codemod gap.
  • The codemod rewrote class strings inside docs/superpowers/plans/* and specs/*. Those are dated historical records; rewriting them makes them no longer describe the code as it was. I'd revert those files.
  • overrides: { "@mapbox/jsonlint-lines-primitives": "2.0.2" } appears with no explanation — worth a line in the body or a comment.
  • postcss is still in devDependencies after postcss.config.js was deleted.
  • The flowbiteSvelteNoCircular plugin regex-patches node_modules source, so it'll silently no-op on any flowbite-svelte update rather than failing loudly. Worth a version assertion so a future bump doesn't quietly break the build.

Worth saying explicitly since the PR reads as reaching v4: this lands on the compat path, not the CSS-first @theme migration — tailwind.config.js still drives everything through @config. I think that's the right call given the brand pipeline needs JS at build time, and I verified the COLOR_* flow still works end-to-end under v4. Just want it to be a stated choice rather than an assumed way-station.

Fix the opacity utilities and the border default, drop the screenshots, and I'll do a full pass on the rest. This is good work on a genuinely unpleasant migration.

role="status"
aria-live="polite"
class="absolute inset-0 z-50 flex items-center justify-center bg-neutral-800 bg-opacity-80 md:rounded-lg"
class="bg-opacity-80 absolute inset-0 z-50 flex items-center justify-center bg-neutral-800 md:rounded-lg"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bg-opacity-80 is on v4's removed-deprecated list, so this emits no CSS — silently, no build error.

The loading overlay goes from 80% to fully opaque, which means the map is completely hidden behind it during every load rather than dimmed.

-class="bg-opacity-80 absolute inset-0 z-50 ... bg-neutral-800 md:rounded-lg"
+class="absolute inset-0 z-50 ... bg-neutral-800/80 md:rounded-lg"

Same family to sweep for across the tree: text-opacity-*, border-opacity-*, divide-opacity-*, placeholder-opacity-*, ring-opacity-* (there's one in SurveyLauncher.svelte:43).

<div class="space-y-4">
<div>
<div class="min-h-36 rounded-lg bg-brand-accent bg-opacity-80 p-4">
<div class="bg-brand-accent bg-opacity-80 min-h-36 rounded-lg p-4">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as LoadingSpinnerbg-opacity-80 emits nothing in v4, so this header renders at full opacity.

bg-brand-accent/80 is the replacement.

Comment thread src/app.css
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
@config '../tailwind.config.js';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right place for the v4 default-border-color compat layer, and it's missing.

In v3 a bare border defaulted to gray-200; in v4 it defaults to currentColor, so every bare border now draws in the element's text color. There are several on lines this PR modifies (the LanguageSwitcher / Header dropdown uses rounded-md border with no color three times).

The upgrade guide's own snippet:

@layer base {
  *, ::after, ::before, ::backdrop, ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }
}

Auditing each bare border and giving it an explicit color is the better end state, but for a migration PR the compat layer keeps the blast radius at zero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade from Tailwind CSS v3 to v4

3 participants