chore: Tailwind CSS v4 migration#4139
Conversation
CSS-first @theme in app/tailwind.css: static palettes plus semantic color tokens as CSS variables, overridable per theme. Tailwind runs through @tailwindcss/postcss (Remix integration off). Plugins upgraded or replaced with v4 equivalents; tailwind-merge bumped to v3. Utility classes codemodded by the official upgrade tool. Fixed two invalid arbitrary variants the upgrade surfaced (table sticky cell :has(), number input attribute variant).
prism-react-renderer passes extra props through, so key ended up in the spread and triggered a React warning. Key is already set directly on the elements.
|
|
Important Review skippedToo many files! This PR contains 208 files, which is 58 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (208)
You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR migrates the webapp from Tailwind CSS v3 to v4. It rewrites Changes
Sequence Diagram(s)Not applicable — this PR consists of styling/className updates and a string identifier rename with no new interaction flows. Related PRs: None identified. Suggested labels: Suggested reviewers: webapp maintainers familiar with Tailwind CSS and the design system primitives Poem A rabbit hopped through classNames wide, 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
Adds surface (background-deep/hover/raised, surface-control/-hover/-active), border (border-bright/brighter/brightest) and text (text-faint) tokens to the themable layer, and sweeps ~930 raw charcoal-* usages across the dashboard to use them. Remaining raw charcoal usages are contrast-fixed decoration on colored accents, intentionally theme-independent.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
apps/webapp/app/components/runs/v3/TaskTriggerSource.tsx (1)
16-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor inconsistency:
size-[1.125rem]left as bracket syntax whilemin-wwas converted.
min-w-[1.125rem]→min-w-4.5, but the adjacentsize-[1.125rem]on the same elements wasn't converted to the equivalentsize-4.5. Purely cosmetic consistency in the token-based utility codemod.♻️ Optional consistency fix
- return <TaskIconSmall className={cn("size-[1.125rem] min-w-4.5 text-tasks", className)} />; + return <TaskIconSmall className={cn("size-4.5 min-w-4.5 text-tasks", className)} />;apps/webapp/package.json (1)
289-289: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent version pinning for
tailwindcss.
tailwindcssis pinned to an exact version (4.3.1) while other Tailwind-related devDependencies (@tailwindcss/forms,@tailwindcss/postcss,tailwind-scrollbar) use^ranges. If intentional (to lock the core engine during migration), consider a brief comment; otherwise align the range style.apps/webapp/app/tailwind.css (2)
309-315: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
::-moz-selectionvendor-prefix lint error is likely a required exception.
selector-no-vendor-prefixflags line 313, but::-moz-selectioncan't be combined into the standard::selectionselector (older Firefox ignores the rule if grouped) — the duplication here is typically intentional. Suggest a scopedstylelint-disable-next-lineinstead of removing the prefix.Source: Linters/SAST tools
229-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider deduplicating
animated-gradient-glowandanimated-gradient-glow-small.Both utilities repeat the identical 5-color conic-gradient definition, differing only in
inset,filter: blur(...), andopacity. Parameterizing via CSS custom properties (set per-utility, consumed by a single shared rule) would avoid having to keep two gradient stops lists in sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 05461b5b-921c-4d80-834d-b439b5fbd3da
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (133)
.server-changes/tailwind-v4-migration.mdapps/webapp/app/components/AskAI.tsxapps/webapp/app/components/BackgroundWrapper.tsxapps/webapp/app/components/DefinitionTooltip.tsxapps/webapp/app/components/Feedback.tsxapps/webapp/app/components/GitMetadata.tsxapps/webapp/app/components/ListPagination.tsxapps/webapp/app/components/ProductHuntBanner.tsxapps/webapp/app/components/WarmStarts.tsxapps/webapp/app/components/code/AIQueryInput.tsxapps/webapp/app/components/code/ChartConfigPanel.tsxapps/webapp/app/components/code/CodeBlock.tsxapps/webapp/app/components/code/TSQLResultsTable.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsxapps/webapp/app/components/dashboard-agent/DashboardAgentHistory.tsxapps/webapp/app/components/logs/LogsTable.tsxapps/webapp/app/components/metrics/BigNumber.tsxapps/webapp/app/components/metrics/QueryWidget.tsxapps/webapp/app/components/metrics/SaveToDashboardDialog.tsxapps/webapp/app/components/metrics/TitleWidget.tsxapps/webapp/app/components/navigation/EnvironmentSelector.tsxapps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsxapps/webapp/app/components/navigation/NotificationPanel.tsxapps/webapp/app/components/navigation/SideMenu.tsxapps/webapp/app/components/navigation/SideMenuItem.tsxapps/webapp/app/components/onboarding/TechnologyPicker.tsxapps/webapp/app/components/primitives/Alert.tsxapps/webapp/app/components/primitives/AnimatingArrow.tsxapps/webapp/app/components/primitives/AppliedFilter.tsxapps/webapp/app/components/primitives/Avatar.tsxapps/webapp/app/components/primitives/Buttons.tsxapps/webapp/app/components/primitives/Calendar.tsxapps/webapp/app/components/primitives/Callout.tsxapps/webapp/app/components/primitives/Checkbox.tsxapps/webapp/app/components/primitives/ClientTabs.tsxapps/webapp/app/components/primitives/ClipboardField.tsxapps/webapp/app/components/primitives/DateField.tsxapps/webapp/app/components/primitives/DateTimePicker.tsxapps/webapp/app/components/primitives/Dialog.tsxapps/webapp/app/components/primitives/Input.tsxapps/webapp/app/components/primitives/InputNumberStepper.tsxapps/webapp/app/components/primitives/InputOTP.tsxapps/webapp/app/components/primitives/LoadingBarDivider.tsxapps/webapp/app/components/primitives/Pagination.tsxapps/webapp/app/components/primitives/Popover.tsxapps/webapp/app/components/primitives/RadioButton.tsxapps/webapp/app/components/primitives/Resizable.tsxapps/webapp/app/components/primitives/SegmentedControl.tsxapps/webapp/app/components/primitives/Select.tsxapps/webapp/app/components/primitives/ShortcutKey.tsxapps/webapp/app/components/primitives/SimpleSelect.tsxapps/webapp/app/components/primitives/Slider.tsxapps/webapp/app/components/primitives/Switch.tsxapps/webapp/app/components/primitives/Table.tsxapps/webapp/app/components/primitives/Tooltip.tsxapps/webapp/app/components/primitives/TreeView/TreeView.tsxapps/webapp/app/components/primitives/UsageSparkline.tsxapps/webapp/app/components/primitives/charts/BigNumberCard.tsxapps/webapp/app/components/primitives/charts/Chart.tsxapps/webapp/app/components/primitives/charts/ChartCard.tsxapps/webapp/app/components/primitives/charts/ChartLegendCompound.tsxapps/webapp/app/components/primitives/charts/ChartRoot.tsxapps/webapp/app/components/primitives/charts/ChartZoom.tsxapps/webapp/app/components/query/QueryEditor.tsxapps/webapp/app/components/run/RunTimeline.tsxapps/webapp/app/components/runs/v3/AIFilterInput.tsxapps/webapp/app/components/runs/v3/ReplayRunDialog.tsxapps/webapp/app/components/runs/v3/RunStatusCellTooltip.tsxapps/webapp/app/components/runs/v3/RunTag.tsxapps/webapp/app/components/runs/v3/SharedFilters.tsxapps/webapp/app/components/runs/v3/TaskRunsTable.tsxapps/webapp/app/components/runs/v3/TaskTriggerSource.tsxapps/webapp/app/components/runs/v3/agent/AgentMessageView.tsxapps/webapp/app/components/runs/v3/ai/AIChatMessages.tsxapps/webapp/app/components/runs/v3/ai/AIModelSummary.tsxapps/webapp/app/components/runs/v3/ai/AISpanDetails.tsxapps/webapp/app/components/runs/v3/ai/SpanMetricRow.tsxapps/webapp/app/components/sessions/v1/SessionsTable.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/AITabContent.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsxapps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsxapps/webapp/app/routes/_app.orgs.new/route.tsxapps/webapp/app/routes/_app.timezones/route.tsxapps/webapp/app/routes/account.authorization-code.$authorizationCode/route.tsxapps/webapp/app/routes/account.security/route.tsxapps/webapp/app/routes/confirm-basic-details.tsxapps/webapp/app/routes/invites.tsxapps/webapp/app/routes/login._index/route.tsxapps/webapp/app/routes/resources.incidents.tsxapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsxapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsxapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language.tsxapps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsxapps/webapp/app/routes/storybook.agent-ui/route.tsxapps/webapp/app/routes/storybook.popover/route.tsxapps/webapp/app/routes/storybook.table/route.tsxapps/webapp/app/routes/storybook.timeline/route.tsxapps/webapp/app/routes/unsubscribe.$userId.$token.tsxapps/webapp/app/routes/vercel.onboarding.tsxapps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.tsapps/webapp/app/services/realtime/shadowRealtimeClientInstance.server.tsapps/webapp/app/tailwind.cssapps/webapp/app/utils/cn.tsapps/webapp/app/utils/randomWords.tsapps/webapp/app/v3/featureFlags.tsapps/webapp/app/v3/services/computeTemplateCreation.server.tsapps/webapp/package.jsonapps/webapp/postcss.config.jsapps/webapp/remix.config.jsapps/webapp/tailwind.config.js
💤 Files with no reviewable changes (1)
- apps/webapp/tailwind.config.js
| disabled={isLoading} | ||
| rows={5} | ||
| className="m-0 min-h-10 w-full resize-none border-0 bg-background-bright px-3 py-2.5 text-sm text-text-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 placeholder:text-text-dimmed focus:border-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" | ||
| className="m-0 min-h-10 w-full resize-none border-0 bg-background-bright px-3 py-2.5 text-sm text-text-bright scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600 placeholder:text-text-dimmed focus:border-0 focus:outline-hidden focus:ring-0 focus-visible:outline-hidden focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Restore a visible keyboard focus style on the textarea.
outline-hidden plus both ring-0 overrides remove the only focus cue here, so keyboard users lose track of the active field. Keep a visible ring/outline on focus-visible.
Run-status and chart colors in JS now reference var(--color-*) tokens (run-status palette added to the theme). Streamdown container vars derive from semantic tokens instead of hardcoded dark HSL values; removed dead forced-dark code block overrides. dark: variant is now driven by data-theme on <html> (set to dark) instead of the OS preference, with color-scheme following the theme.
Shiki (streamdown), prism (CodeBlock) and CodeMirror themes now read from --color-code-* and --color-editor-* palettes declared in tailwind.css; callout variants use --color-callout-* tokens. A light theme can restyle all of them by overriding variables only.
Activity sparkbars on the project index use the shared run-status palette; chart grids, ticks, reference lines and loading skeletons read semantic tokens; usage meter and progress colors use success/warning/error tokens.
The upgrade codemod treated string literals in .ts files as utility classes: realtimeBackend "shadow" flag values, compute template creation "shadow" mode, and randomWords dictionary entries were renamed to shadow-sm/ring-3/ outline-solid. Restores the original values so persisted flags keep parsing.
| > | ||
| <ResizablePanel id="payload" min="300px"> | ||
| <div className="rounded-smbg-charcoal-900 mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"> | ||
| <div className="rounded-smbg-background-deep mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"> |
There was a problem hiding this comment.
🟡 Missing space between two CSS classes causes the replay dialog's editor container to lose its rounded corners and background color
Two CSS class names are concatenated without a space (rounded-smbg-background-deep at apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx:239), so neither rounded-sm nor bg-background-deep is applied.
Impact: The JSON editor container in the replay-run dialog renders without rounded corners and without its dark background.
Concatenated class token produces a single unknown utility
The old code already had this bug (rounded-smbg-charcoal-900), and the PR carried it forward by replacing only the color token (charcoal-900 → background-deep) without inserting the missing space. Tailwind sees rounded-smbg-background-deep as one unknown class and silently drops it, so neither rounded-sm (border-radius) nor bg-background-deep (background-color) takes effect on the <div> wrapping the <JSONEditor>.
| <div className="rounded-smbg-background-deep mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"> | |
| <div className="rounded-sm bg-background-deep mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"> |
Was this helpful? React with 👍 or 👎 to provide feedback.
| [ | ||
| "var(--color-success)", | ||
| "var(--color-success)", | ||
| "var(--color-warning)", | ||
| "var(--color-error)", | ||
| "var(--color-error)", | ||
| ] |
There was a problem hiding this comment.
🚩 CSS variables in framer-motion color interpolation may not animate smoothly
The useTransform hook from framer-motion at apps/webapp/app/components/billing/FreePlanUsage.tsx:9-18 was changed from hex color strings (#22C55E, #F59E0B, #F43F5E) to CSS variable references (var(--color-success), var(--color-warning), var(--color-error)). Framer-motion's useTransform interpolates between output values by parsing color strings into numeric components. CSS variable references like var(--color-success) are opaque strings that framer-motion cannot parse or interpolate — the color will likely snap between values at the breakpoints rather than smoothly transitioning. Given the breakpoints are [0, 74, 75, 95, 100] with duplicate colors at the edges, the interpolation window is very narrow (1 unit at 74→75, 5 units at 95→100), so the visual impact may be minimal, but the behavior has changed from smooth interpolation to discrete snapping.
Was this helpful? React with 👍 or 👎 to provide feedback.
Migrates the webapp from Tailwind CSS 3.4 to 4.x.
CSS-first
@themeinapp/tailwind.cssreplacestailwind.config.js. Semantic color tokens are CSS variables overridable per theme support.Note: the default palette is now oklch.
Testing
Typecheck, format and lint pass. Verified visually against the local dashboard — colors, borders, scrollbars, animations and focus s