Skip to content

Upgrade Tailwind CSS 3 to 4 with v3 visual parity (T17, Dependabot #9) - #221

Merged
Programcode merged 9 commits into
mainfrom
chore/t17-tailwind-v4
Oct 23, 2026
Merged

Programcode merged 9 commits into
mainfrom
chore/t17-tailwind-v4

Conversation

@Programcode

@Programcode Programcode commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Upgrades Tailwind CSS from 3.4 to 4.3 and closes Dependabot alert #9: braces 3.0.3 (GHSA-vfj7-8cjw-p6xm, high), which only Tailwind 3's chokidar, micromatch and fast-glob dependencies pulled in. npm audit now finds 0 vulnerabilities and npm ls braces is empty.

The goal is the same UI. Where v4 changed a default, this PR restores the v3 behaviour instead of accepting the change.

What changed

  • Build. @tailwindcss/vite replaces the PostCSS plugin. tailwind.config.ts, postcss.config.js, autoprefixer, postcss and tailwindcss-animate are gone, and tw-animate-css replaces the animate plugin. tailwind-merge 3 knows the v4 class names. knip now reads src/**/*.css, so it sees dependencies that only CSS imports.
  • Config. The theme lives in src/index.css. Tokens use @theme inline, so .dark and prefers-contrast still switch them at runtime. The app's shadow variables are renamed --app-shadow-*, because v4 owns the --shadow-* names. Tailwind still scans only src/**/*.{ts,tsx}, like v3's content.
  • Class renames. npx @tailwindcss/upgrade made them, for example outline-none to outline-hidden, break-words to wrap-break-word, !mt-2 to mt-2!, bg-gradient-to-* to bg-linear-to-* and [var(--x)] to (--x). The tool also rewrote the Button prop variant="outline" to "outline-solid" in six places; this PR reverts those.
  • v3 behaviour kept on purpose.
    • The palette. The 11 colour families the app uses keep their v3 hex values, because v4's oklch values look different.
    • space-x and space-y keep v3's sibling margins. v4 puts a bottom margin on each child, and an inline <Label> ignores it, so every label lost 8px above its field.
    • Hover styles apply only on fine pointers, as v3's hoverOnlyWhenSupported did.
    • Defaults v4's preflight dropped come back: grey placeholders, a pointer cursor on buttons and 1px table-cell padding.
    • Breakpoints stay in px, and the container still caps at 1400px.
    • Cascade order. v3 emitted everything unlayered, and v4 uses real cascade layers. So the scrollbar rules move into @layer base and .scrollbar-hide still wins. [id] stays unlayered. .motion-lift:hover and .motion-press:active become @utility, so they still outrank a button's shadow-* and duration-*. The toast surface classes get !, because Sonner's unlayered stylesheet would otherwise win.
    • Line heights. In v4, leading-* beats a responsive text-* size, so the landing section headings and the sparring motion box pin the line heights v3 gave them.
    • Where wrap-break-word and wrap-anywhere were both present, v4's sort order let break-word win, and long URLs overflowed /expocit/sources at 375px. Those elements keep only wrap-anywhere.
    • v4 centres with the translate property, and tw-animate-css's keyframes add a transform on top. Dialogs and alert dialogs drop their -50%/-48% slide offsets and keep v3's 2% rise on open. The select popper's slide starts one step further out and closes back onto the trigger.

Before and after

I took 142 screenshots against main (91e1e85) at 1280 and 375 px, in light and dark. They cover every main page: landing, /login, /privacy, /terms, the tools hub and each free tool, the sources page, 404 and every /app/* page. They also cover the onboarding tour, the start-debate and motion dialogs, the mobile menu, the pricing modal, a toast, keyboard focus and hover on three controls. I pixel-diffed each pair and compared computed styles element by element.

  • 140 of 142 screenshots are identical.
  • Sparring (2 screenshots) draws text with grayscale antialiasing instead of subpixel antialiasing. You only see it zoomed in, and macOS and phones always render grayscale.
  • Typography plugin greys (Markdown answers, the sparring scorecard) use v4's oklch values, about one RGB step off. The plugin bundles its own palette, so the theme override doesn't reach it.
  • Gradients interpolate in oklab instead of sRGB, at most 5 of 255 on one channel at the midpoint.
  • One intended change: the Ask "Jump to latest" button and one Library button no longer jump off-centre on hover or press. In v3 the hover transform replaced their centring translate.
  • Frame-by-frame sampling matches main for the dialog and select popper opening and closing, and for the hover lift shadow on a shadow-lg button.

The comparison caught these, and this PR fixes them:

  • every label-to-field gap was 8px short;
  • the toast had Sonner's own background and border;
  • /expocit/sources overflowed at 375px;
  • landing headings grew from 48px to 60px line height;
  • table rows in the speech calculator and timer tools lost 2px each;
  • palette colours on badges and buttons;
  • dialog and select motion;
  • the hover lift shadow.

Checks

These are local results at f75e979. The current head, 32a0181, only adds the merge of main with T16's three locale-string changes; after that merge I reran lint, typecheck and the 605 unit tests, and they pass.

  • npm run verify passes: docs, lint and knip, typecheck, 605 unit tests, 30 collaboration tests, 457 edge tests, build and check-site, test:sw (offline routes, precache and worker upgrade) and 47 smoke e2e tests. Three smoke tests needed a retry right after the Supabase stack stopped; run alone 3 times each with no retries, they passed every time.
  • npm run test:e2e (desktop and mobile Chromium): 271 passed, 5 skipped, 2 flaky (Hungarian parity, which passed 3 of 3 times when rerun alone) and 6 failed. The 6 failures are three specs on each project: loading-performance.spec.ts:63, navigation-performance.spec.ts:344 and search-quality.spec.ts:7. All three fail the same way on main (91e1e85) and on this branch's base (a7610fa), so this change didn't cause them.
  • verify-debatelab drive all in fake mode, on desktop-chromium and mobile-chromium: 62 passed and 2 skipped on each project.
  • GitHub Actions: every job on 32a0181 failed within 2 or 3 seconds with no logs, which is the out-of-minutes billing state, not a code failure. So there is no CI pass on this head, only the local results above.
  • npm audit: found 0 vulnerabilities. On main it reports 5 high, all braces through tailwindcss.

Notes for review

  • Tailwind 4 needs Safari 16.4, Chrome 111 or Firefox 128. Older browsers render translucent theme colours such as bg-primary/10 solid where color-mix() is missing. Vite's default JS target is already Safari 16 and Chrome 107, so the gap is small. docs/architecture.md records this.
  • Gzipped CSS grows from 20.3 KB to 23.6 KB, mostly v4's @property rules and color-mix fallbacks.
  • The first two commits are the raw upgrade-tool output, and later commits correct it. Please squash on merge.
  • The select chevron's [&>svg]:data-[state=open]:rotate-180 never matched in v3 either. I left it for a follow-up.
  • Three rounds of review by fresh Claude Opus subagents. This PR fixes every Medium or higher finding.

🤖 Generated with Claude Code

Programcode and others added 8 commits October 23, 2026 07:38
Moves the theme into index.css (@theme), builds through @tailwindcss/vite,
replaces tailwindcss-animate with tw-animate-css and drops postcss,
autoprefixer and tailwind.config.ts. tailwind-merge 3 understands the v4
class names.

Keeps the v3 look where v4 changed defaults: the v3 palette for the
families in use, v3 space-x/space-y, hover only on fine pointers, grey
placeholders, pointer buttons, table-cell padding and the container. Fixes
the upgrade tool's variant="outline-solid" rewrite, overflow-wrap order,
two line heights and Sonner's unlayered styles overriding the toast.

Removes braces (GHSA-vfj7-8cjw-p6xm, Dependabot #9) from the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v4 centres dialogs with the translate property, which tw-animate-css's
transform keyframes add to, so the old -50%/-48% slide offsets sent every
dialog in from far up and left. The dialogs now only rise 2% on open, as
they did in v3.

Breakpoints stay in px so a larger browser font doesn't move them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v3 emitted @layer components unlayered, so .motion-lift:hover and
.motion-press:active outranked a button's shadow-* and duration-* classes;
they are utilities now so specificity decides again. The [id] scroll
margin is unlayered again and beats scroll-mt-*, as before.

The select popper's offset is a translate the slide now adds to, so its
slide starts one step further out and closes back onto the trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 10:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between ed6b1c5 and f75e979.

⛔ Files ignored due to path filters: 1
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing: 75
  • .claude/skills/verify-debatelab/SKILL.md
  • components.json
  • docs/architecture.md
  • knip.json
  • package.json
  • postcss.config.js
  • src/components/LanguageSwitcher.tsx
  • src/components/OfflineIndicator.tsx
  • src/components/StagingBanner.tsx
  • src/components/admin/AdminAnnouncements.tsx
  • src/components/features/ask/AskChat.tsx
  • src/components/features/crossfire/SavedCrossfireView.tsx
  • src/components/features/debate/CurrentDebateMenu.tsx
  • src/components/features/debate/CurrentDebateStrip.tsx
  • src/components/features/debate/DebateContextControl.tsx
  • src/components/features/debate/DebateDialogs.tsx
  • src/components/features/debate/DebateMotionGate.tsx
  • src/components/features/debate/UnsortedWork.tsx
  • src/components/features/sparring/SparringRound.tsx
  • src/components/features/sparring/SparringSetup.tsx
  • src/components/features/subscription/TrialBanner.tsx
  • src/components/features/tools/ResearchedSources.tsx
  • src/components/features/tools/tool-links.tsx
  • src/components/free-tools/DebateTimer.tsx
  • src/components/free-tools/MotionGenerator.tsx
  • src/components/free-tools/ToolPageLayout.tsx
  • src/components/landing/DeepDive.tsx
  • src/components/landing/FAQ.tsx
  • src/components/landing/FinalCTA.tsx
  • src/components/landing/Hero.tsx
  • src/components/landing/HowItWorks.tsx
  • src/components/landing/Navbar.tsx
  • src/components/landing/Pricing.tsx
  • src/components/layout/AnnouncementBanner.tsx
  • src/components/layout/AppLayout.tsx
  • src/components/layout/PageHeader.tsx
  • src/components/layout/PublicPageShell.tsx
  • src/components/layout/SkipLink.tsx
  • src/components/ui/alert-dialog.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/ui/checkbox.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/input.tsx
  • src/components/ui/markdown.tsx
  • src/components/ui/popover.tsx
  • src/components/ui/scroll-area.tsx
  • src/components/ui/select.tsx
  • src/components/ui/separator.tsx
  • src/components/ui/sheet.tsx
  • src/components/ui/slider.tsx
  • src/components/ui/sonner.tsx
  • src/components/ui/switch.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/tooltip.tsx
  • src/index.css
  • src/lib/workspace/editor-schema.ts
  • src/pages/AdminPage.tsx
  • src/pages/CrossfirePage.tsx
  • src/pages/CurrentDebatePage.tsx
  • src/pages/FlowchartPage.tsx
  • src/pages/HistoryPage.tsx
  • src/pages/HomePage.tsx
  • src/pages/LandingPage.tsx
  • src/pages/LibraryPage.tsx
  • src/pages/SettingsPage.tsx
  • src/pages/SharedPage.tsx
  • src/pages/SourcesPage.tsx
  • src/pages/WorkspacePage.tsx
  • src/pages/WorkspacesPage.tsx
  • src/pages/tools/MotionGeneratorToolPage.tsx
  • src/pages/tools/ToolsHubPage.tsx
  • tailwind.config.ts
  • vite.config.ts
💤 Files with no reviewable changes: 2
  • tailwind.config.ts
  • postcss.config.js

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

📝 Walkthrough

The project migrates from Tailwind CSS 3 to Tailwind CSS 4. It replaces the PostCSS and Tailwind configuration with Vite integration and CSS-based configuration, then updates utility classes across UI components, application components, and pages.

Changes: Tailwind CSS 4 migration

Layer / File(s) Summary
Tailwind v4 build and CSS foundation
package.json, vite.config.ts, src/index.css, components.json, knip.json, postcss.config.js, tailwind.config.ts, docs/architecture.md, .claude/skills/verify-debatelab/SKILL.md
The dependencies and Vite plugin are updated for Tailwind CSS 4. The PostCSS and Tailwind configuration files are removed, and src/index.css now defines the theme, variants, utilities, and base rules. Project configuration and documentation are updated.
UI primitive utility and animation updates
src/components/ui/*, src/lib/workspace/editor-schema.ts
UI primitives adopt updated utility syntax for outlines, arbitrary values, and state variants. Dialog animation classes and select positioning animations change. Toast surface utilities gain important modifiers.
Application utility class updates
src/components/features/*, src/components/layout/*, src/components/landing/*, src/components/free-tools/*, src/components/LanguageSwitcher.tsx, src/components/OfflineIndicator.tsx, src/components/StagingBanner.tsx, src/components/admin/AdminAnnouncements.tsx, src/pages/*
Application class strings adopt updated wrapping, focus, gradient, spacing, opacity, and custom-property utilities. The listed component and page summaries report no changes to application logic, except for the described styling adjustments.

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

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

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

@Programcode
Programcode merged commit f1cb90c into main Oct 23, 2026
3 of 11 checks passed
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.

2 participants