Skip to content

feat: migrate workspace to Tailwind CSS v4#377

Merged
KMKoushik merged 2 commits intomainfrom
better-ui
Mar 14, 2026
Merged

feat: migrate workspace to Tailwind CSS v4#377
KMKoushik merged 2 commits intomainfrom
better-ui

Conversation

@KMKoushik
Copy link
Member

@KMKoushik KMKoushik commented Mar 14, 2026

Summary

  • migrate workspace Tailwind setup from v3 to v4 by upgrading tailwindcss and switching PostCSS to @tailwindcss/postcss in web, marketing, ui, email-editor, and shared tailwind-config packages
  • move app stylesheet entrypoints to app-local globals.css files that import Tailwind v4 and shared UI globals via @config, preserving existing shared theme tokens and component styles
  • fix Tailwind v4 compatibility regressions by converting arbitrary CSS variable utilities to var(...) syntax (for sidebar/dropdown/charts widths and origins), adding @reference "tailwindcss" for email-editor @apply, and resolving dashboard content overlap next to the fixed sidebar

Verification

  • pnpm install
  • pnpm --filter=@usesend/email-editor dev (build starts successfully; watch process runs)
  • pnpm --filter=web exec next dev -p 3100 --turbopack (startup/compile succeeds)
  • manual browser verification on http://localhost:3000/contacts after restart/hard refresh confirms sidebar/content no longer overlap

Summary by cubic

Migrates the workspace to Tailwind CSS v4 with @tailwindcss/postcss. Moves Tailwind setup into CSS directives and app-local globals.css, and fixes v4 UI regressions in the sidebar, dropdowns, charts, and email editor.

  • Dependencies

    • Upgrade tailwindcss to v4 across web, marketing, ui, and email-editor; replace PostCSS plugin with @tailwindcss/postcss.
    • Remove @usesend/tailwind-config and all per-app tailwind.config.ts; define theme, plugins, and dark mode via CSS (@plugin, @theme, @custom-variant) in @usesend/ui globals.
    • Add per-app globals.css that imports tailwindcss, defines @source globs, and imports @usesend/ui globals.
  • Bug Fixes

    • Convert CSS variable utilities to var(...) in dropdown widths/origin, chart tooltip colors, and sidebar widths.
    • Prevent horizontal overflow and content overlap with the fixed sidebar by adding min-w-0, overflow-x-hidden, and resetting horizontal scroll on route changes.
    • Restore @apply in email-editor by adding @reference "tailwindcss".

Written for commit 16d6bb7. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed dropdown menu width rendering and CSS variable usage across the UI.
    • Corrected dashboard scroll behavior to prevent unwanted horizontal scrolling/overflow.
    • Resolved various styling/layout issues in sidebar and components for more consistent visuals.
  • Chores

    • Upgraded Tailwind CSS and updated PostCSS plugin usage across apps and packages.

@vercel
Copy link

vercel bot commented Mar 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
unsend-marketing Ready Ready Preview, Comment Mar 14, 2026 7:48pm

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 976f6374-0ad6-49ef-a90f-0a19690e55f5

📥 Commits

Reviewing files that changed from the base of the PR and between 8e6abeb and 16d6bb7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/marketing/package.json
  • apps/marketing/src/app/globals.css
  • apps/marketing/tailwind.config.ts
  • apps/web/package.json
  • apps/web/src/app/globals.css
  • apps/web/tailwind.config.ts
  • packages/email-editor/package.json
  • packages/email-editor/tailwind.config.ts
  • packages/tailwind-config/package.json
  • packages/tailwind-config/tailwind.config.ts
  • packages/tailwind-config/tsconfig.json
  • packages/ui/package.json
  • packages/ui/styles/globals.css
  • packages/ui/tailwind.config.ts
💤 Files with no reviewable changes (7)
  • apps/web/tailwind.config.ts
  • packages/tailwind-config/tailwind.config.ts
  • packages/ui/tailwind.config.ts
  • packages/tailwind-config/package.json
  • packages/tailwind-config/tsconfig.json
  • packages/email-editor/tailwind.config.ts
  • apps/marketing/tailwind.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/package.json
  • packages/ui/package.json

Walkthrough

This PR migrates the workspace to Tailwind v4.1.0 and the new PostCSS plugin: adds @tailwindcss/postcss devDependency, updates tailwindcss versions, replaces tailwindcss plugin entries in postcss.config files with @tailwindcss/postcss, adds app-local globals.css files and switches app layout imports to them, fixes CSS custom-property usages to var(...), and removes per-package Tailwind config files (including the shared packages/tailwind-config).

Possibly related PRs

  • usesend/useSend PR 211: modifies Tailwind/PostCSS setup and app globals, touching the shared tailwind-config and apps' global imports.
  • usesend/useSend PR 210: updates Tailwind and PostCSS dependencies and adjusts postcss.config and per-app Tailwind configs.

Suggested labels

codex

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: migrate workspace to Tailwind CSS v4' accurately describes the primary change: upgrading Tailwind from v3 to v4 across the workspace, which is the main objective of this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 14, 2026

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 16d6bb7
Status: ✅  Deploy successful!
Preview URL: https://71304410.usesend.pages.dev
Branch Preview URL: https://better-ui.usesend.pages.dev

View logs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/ui/src/dropdown-menu.tsx (1)

50-50: Consider using idiomatic Tailwind v4 syntax for CSS variables.

Per Tailwind v4 breaking changes, CSS variables in arbitrary values should use parentheses instead of brackets:

  • Old (v3): origin-[--radix-dropdown-menu-content-transform-origin]
  • New (v4): origin-(--radix-dropdown-menu-content-transform-origin)

The current origin-[var(...)] syntax will work but wrapping in var() inside brackets isn't the idiomatic v4 approach.

♻️ Suggested change for both occurrences

Line 50:

-      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[var(--radix-dropdown-menu-content-transform-origin)]",
+      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",

Line 68:

-        "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-xl border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[var(--radix-dropdown-menu-content-transform-origin)]",
+        "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-xl border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",

Note: Line 68 also has max-h-[var(--radix-dropdown-menu-content-available-height)] which should become max-h-(--radix-dropdown-menu-content-available-height).

Also applies to: 68-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/dropdown-menu.tsx` at line 50, The Tailwind classes in the
Dropdown Menu component use legacy bracketed CSS-variable syntax; update the
arbitrary-value usages to Tailwind v4 idiomatic parentheses. Replace occurrences
of origin-[var(--radix-dropdown-menu-content-transform-origin)] with
origin-(--radix-dropdown-menu-content-transform-origin) and
max-h-[var(--radix-dropdown-menu-content-available-height)] with
max-h-(--radix-dropdown-menu-content-available-height) in the className
string(s) inside packages/ui/src/dropdown-menu.tsx (the dropdown content class
that currently contains "origin-[var(...)]" and the one containing
"max-h-[var(...)]") so they follow Tailwind v4 syntax.
packages/ui/src/charts.tsx (1)

71-73: Avoid shadowing config in the filter callback.

Using config as the callback variable shadows the outer config prop and makes this block harder to read.

♻️ Suggested readability tweak
-  const colorConfig = Object.entries(config).filter(
-    ([_, config]) => config.theme || config.color,
-  );
+  const colorConfig = Object.entries(config).filter(
+    ([_, itemConfig]) => itemConfig.theme || itemConfig.color,
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/charts.tsx` around lines 71 - 73, The filter callback in the
colorConfig computation shadows the outer config prop by reusing the name
"config"; rename the inner destructured variable (e.g., from ([_, config]) to
([_, cfg]) or ([_, entry]) and update the predicate to use that new name so the
expression becomes ([_, cfg]) => cfg.theme || cfg.color, ensuring the outer
config prop is not shadowed and readability is improved for the colorConfig
constant.
apps/web/src/app/(dashboard)/dasboard-layout.tsx (1)

33-33: Remove duplicated min-w-0 on SidebarInset.

SidebarInset already applies min-w-0 in its base classes (packages/ui/src/sidebar.tsx, Lines 343-345), so this prop is redundant.

♻️ Proposed cleanup
-        <SidebarInset className="min-w-0">
+        <SidebarInset>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`(dashboard)/dasboard-layout.tsx at line 33, The
SidebarInset component is being passed a redundant className ("min-w-0") even
though SidebarInset already includes min-w-0 in its base classes; remove the
className="min-w-0" prop from the SidebarInset JSX in dasboard-layout.tsx (the
SidebarInset element) so the component relies on its internal base classes and
avoids duplicate/unused props.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/email-editor/src/styles/index.css`:
- Line 1: The CSS file uses the Tailwind v4 at-rule `@reference` "tailwindcss"
which will be flagged by linters; update project lint configs by adding
ignoreAtRules: ["reference","config","theme","source","custom-variant"] to
.stylelintrc.json rules and adjust biome.json to either exclude
packages/email-editor/src/styles/index.css from CSS parsing or add support for
Tailwind v4 directives so the `@reference` at-rule is not reported as invalid.

---

Nitpick comments:
In `@apps/web/src/app/`(dashboard)/dasboard-layout.tsx:
- Line 33: The SidebarInset component is being passed a redundant className
("min-w-0") even though SidebarInset already includes min-w-0 in its base
classes; remove the className="min-w-0" prop from the SidebarInset JSX in
dasboard-layout.tsx (the SidebarInset element) so the component relies on its
internal base classes and avoids duplicate/unused props.

In `@packages/ui/src/charts.tsx`:
- Around line 71-73: The filter callback in the colorConfig computation shadows
the outer config prop by reusing the name "config"; rename the inner
destructured variable (e.g., from ([_, config]) to ([_, cfg]) or ([_, entry])
and update the predicate to use that new name so the expression becomes ([_,
cfg]) => cfg.theme || cfg.color, ensuring the outer config prop is not shadowed
and readability is improved for the colorConfig constant.

In `@packages/ui/src/dropdown-menu.tsx`:
- Line 50: The Tailwind classes in the Dropdown Menu component use legacy
bracketed CSS-variable syntax; update the arbitrary-value usages to Tailwind v4
idiomatic parentheses. Replace occurrences of
origin-[var(--radix-dropdown-menu-content-transform-origin)] with
origin-(--radix-dropdown-menu-content-transform-origin) and
max-h-[var(--radix-dropdown-menu-content-available-height)] with
max-h-(--radix-dropdown-menu-content-available-height) in the className
string(s) inside packages/ui/src/dropdown-menu.tsx (the dropdown content class
that currently contains "origin-[var(...)]" and the one containing
"max-h-[var(...)]") so they follow Tailwind v4 syntax.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66a062cf-d412-413d-b19b-e12b96985a8b

📥 Commits

Reviewing files that changed from the base of the PR and between d7b196c and 8e6abeb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • apps/marketing/package.json
  • apps/marketing/postcss.config.cjs
  • apps/marketing/src/app/globals.css
  • apps/marketing/src/app/layout.tsx
  • apps/marketing/tailwind.config.ts
  • apps/web/package.json
  • apps/web/postcss.config.cjs
  • apps/web/src/app/(dashboard)/dasboard-layout.tsx
  • apps/web/src/app/(dashboard)/webhooks/add-webhook.tsx
  • apps/web/src/app/(dashboard)/webhooks/webhook-update-dialog.tsx
  • apps/web/src/app/globals.css
  • apps/web/src/app/layout.tsx
  • apps/web/src/components/AppSideBar.tsx
  • packages/email-editor/package.json
  • packages/email-editor/postcss.config.cjs
  • packages/email-editor/src/styles/index.css
  • packages/tailwind-config/package.json
  • packages/tailwind-config/tailwind.config.ts
  • packages/ui/package.json
  • packages/ui/src/charts.tsx
  • packages/ui/src/dropdown-menu.tsx
  • packages/ui/src/sidebar.tsx
  • packages/ui/styles/globals.css
💤 Files with no reviewable changes (2)
  • packages/ui/styles/globals.css
  • apps/marketing/tailwind.config.ts

@@ -1,3 +1,5 @@
@reference "tailwindcss";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CSS files using Tailwind v4 directives =="
rg -n --type=css '@reference|@theme|@source|@utility|@variant|@custom-variant|@config|@apply'

echo
echo "== Stylelint unknown-at-rule configuration =="
rg -n -C2 'scss/at-rule-no-unknown|at-rule-no-unknown|ignoreAtRules' .

echo
echo "== Biome/Stylelint config files =="
fd -HI 'biome.json|biome.jsonc|stylelint.config.*|.stylelintrc.*|.stylelintrc' -x sh -c 'echo "----- {} -----"; sed -n "1,220p" "{}"'

Repository: usesend/useSend

Length of output: 1143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Finding Biome config files =="
find . -maxdepth 3 -type f \( -name "biome.json" -o -name "biome.jsonc" \) 2>/dev/null || echo "No biome config found"

echo
echo "== Finding Stylelint config files =="
find . -maxdepth 3 -type f \( -name ".stylelintrc*" -o -name "stylelint.config.*" \) 2>/dev/null || echo "No stylelint config found"

echo
echo "== Checking package.json for linting setup =="
find . -maxdepth 2 -name "package.json" -type f -exec grep -l "stylelint\|biome" {} \;

Repository: usesend/useSend

Length of output: 209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Content of biome.json =="
cat biome.json

echo
echo "== Content of .stylelintrc.json =="
cat .stylelintrc.json

Repository: usesend/useSend

Length of output: 2903


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Checking for GitHub Actions workflows =="
find .github -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -5

echo
echo "== Checking package.json scripts for linting =="
find . -maxdepth 2 -name "package.json" -type f | xargs grep -l "lint" 2>/dev/null | head -3

echo
echo "== Root package.json lint scripts =="
grep -A 10 '"scripts"' ./package.json | grep -E 'lint|stylelint|biome' || echo "No lint scripts found"

Repository: usesend/useSend

Length of output: 387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/email-editor/package.json =="
cat packages/email-editor/package.json 2>/dev/null || echo "File not found"

echo
echo "== Check for tailwind.config in email-editor =="
find packages/email-editor -name "tailwind.config.*" -o -name "postcss.config.*" 2>/dev/null

echo
echo "== Check for tailwind.config at root =="
ls -la tailwind.config.* postcss.config.* 2>/dev/null || echo "None found"

echo
echo "== All CSS files in email-editor =="
find packages/email-editor -name "*.css" -type f

Repository: usesend/useSend

Length of output: 2328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== postcss.config.cjs in email-editor =="
cat packages/email-editor/postcss.config.cjs

echo
echo "== tailwind.config.ts in email-editor =="
cat packages/email-editor/tailwind.config.ts

echo
echo "== Checking if Biome is configured to scan email-editor =="
grep -A 20 '"biome"' biome.json || echo "No biome workspace config found"

Repository: usesend/useSend

Length of output: 546


Configure linting tools to support Tailwind v4 at-rules.

The @reference "tailwindcss" directive at line 1 will be flagged as invalid by both Biome and Stylelint without explicit configuration.

Update .stylelintrc.json to add ignoreAtRules: ["reference", "config", "theme", "source", "custom-variant"] to the rules, and configure biome.json to either skip CSS parsing for this file or add support for Tailwind v4 directives.

🧰 Tools
🪛 Biome (2.4.6)

[error] 1-1: Tailwind-specific syntax is disabled.

(parse)

🪛 Stylelint (17.4.0)

[error] 1-1: Unexpected unknown at-rule "@reference" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/email-editor/src/styles/index.css` at line 1, The CSS file uses the
Tailwind v4 at-rule `@reference` "tailwindcss" which will be flagged by linters;
update project lint configs by adding ignoreAtRules:
["reference","config","theme","source","custom-variant"] to .stylelintrc.json
rules and adjust biome.json to either exclude
packages/email-editor/src/styles/index.css from CSS parsing or add support for
Tailwind v4 directives so the `@reference` at-rule is not reported as invalid.

@KMKoushik KMKoushik merged commit 9a306b1 into main Mar 14, 2026
6 checks passed
@KMKoushik KMKoushik deleted the better-ui branch March 14, 2026 20:10
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.

1 participant