Skip to content

fix(tailwind): load tailwind.config.js via @config - #2628

Merged
camielvs merged 1 commit into
masterfrom
fix/tailwind-config-directive
Aug 19, 2026
Merged

fix(tailwind): load tailwind.config.js via @config#2628
camielvs merged 1 commit into
masterfrom
fix/tailwind-config-directive

Conversation

@camielvs

Copy link
Copy Markdown
Collaborator

tailwind.config.js has been dead since the v4 upgrade. Tailwind v4 is CSS-first and does not
auto-load a JS config — it only reads one when a stylesheet asks for it with @config. src/styles/global.css
never did, so the theme.extend.fontSize entries in that file were never registered and text-2xs /
text-3xs compiled to nothing.

  @import "tailwindcss";

+ @config "../../tailwind.config.js";
+
  @plugin "tailwindcss-animate";

Proof the directive takes effect

A byte-identical build is ambiguous on its own — it is also what a silently-ignored directive looks
like. So I added a probe usage (className="… text-2xs text-3xs") to a real component and built the
app both ways:

.text-2xs .text-3xs
with @config font-size:.625rem font-size:.5rem
without @config not emitted not emitted

The probe was reverted; it is not part of this diff.

Proof nothing changes today

No file under src uses text-2xs or text-3xs — zero occurrences. With the probe removed, the built
stylesheet is byte-identical to master:

master:      161,345 bytes
this branch: 161,345 bytes   (cmp: identical)

That also settles the one real risk in loading a legacy config: its content: ["./src/**/*.{js,jsx,ts,tsx}"]
key narrows source detection from v4's automatic scanning. Identical output over a full app build means
no currently-used class lives outside that glob. Worth knowing if a class is ever added to a .html,
.mdx, or .mjs file — it would need adding to content.

Why @config rather than moving the values into @theme

Chosen per request. The alternative — deleting tailwind.config.js and adding --text-2xs: 0.625rem /
--text-3xs: 0.5rem to the existing @theme inline block — would be the more idiomatic v4 shape and
would leave one place to look for theme values instead of two. Happy to switch if preferred.

Context

Found while reviewing #2624, which independently discovered the same class of bug: text-md and
font-regular in typography.tsx also emit no CSS, because --text-md and --font-weight-regular are
not defined either. That one is a separate issue — those variants are referenced by every <Text> in the
app, so defining them would change rendered output, unlike this change.

Reviewer checklist

  • Agree @config is the direction, rather than folding the two steps into @theme inline
  • Sanity-check that a legacy content glob is acceptable (see above)

Validation

  • pnpm vite build ✅ — output byte-identical to master
  • pnpm prettier --check src/styles/global.css

@camielvs
camielvs requested a review from a team as a code owner August 13, 2026 22:46
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: fix/tailwind-config-directive/de45cd2

@morgan-wowk morgan-wowk 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.

🤖 Agent review. Correct and byte-identical output today; one latent footgun worth weighing (inline).

Comment thread src/styles/global.css Outdated
@camielvs
camielvs force-pushed the fix/tailwind-config-directive branch from 32b08bd to 32b1a10 Compare August 18, 2026 20:14

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Tailwind v4 does not read tailwind.config.js unless a CSS file asks for
it, so the theme.extend.fontSize entries in that file had never been
registered: text-2xs and text-3xs compiled to nothing.

Verified by building the app twice — with the directive the compiler emits
.text-2xs{font-size:.625rem} and .text-3xs{font-size:.5rem} for a probe
usage; without it, neither rule appears.

No call site uses either class today, so the emitted CSS is byte-identical
to master (161,345 bytes both ways). This only makes the two steps
available; nothing changes size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs force-pushed the fix/tailwind-config-directive branch from 32b1a10 to de45cd2 Compare August 18, 2026 20:17
@camielvs
camielvs requested a review from morgan-wowk August 18, 2026 20:21

@morgan-wowk morgan-wowk 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.

🤖 Agent-assisted re-review (posted by Morgan). Approving. You took the v4-idiomatic path — --text-2xs/--text-3xs now live in the existing @theme inline block and the @config/tailwind.config.js reintroduction is gone. That removes the latent footgun entirely: there's no content glob to silently drop classes authored outside src/**, and source detection stays on v4's default scan. Exactly the fix I hoped for.

camielvs commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Aug 19, 12:06 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Aug 19, 12:06 AM UTC: @camielvs merged this pull request with Graphite.

@camielvs
camielvs merged commit dd0bff1 into master Aug 19, 2026
17 checks passed
@camielvs
camielvs deleted the fix/tailwind-config-directive branch August 19, 2026 00:06
camielvs added a commit that referenced this pull request Aug 19, 2026
…#2629)

Resolves **B2** of #2626.

## The bug

`textVariants` in `src/components/ui/typography.tsx` mapped `size="md"` to `text-md` and `weight="regular"` to `font-regular`. Neither is a Tailwind v4 token — the default theme defines `--text-xs/sm/base/lg/xl/2xl` and `--font-weight-light/normal/semibold/bold`. There is no `--text-md` and no `--font-weight-regular`.

A class with no matching token compiles to **nothing**. So every `<Text>`, `<Paragraph>`, `<Heading level={1}>` and `<Link>` on the default size/weight emitted a class that did not exist, and inherited its font-size and weight from an ancestor instead. It failed silently, and was copied from `typography.tsx` into `link.tsx` before anyone noticed.

## The fix

| | before | after |
|---|---|---|
| `textVariants.size.md` | `text-md` | `text-base` |
| `textVariants.weight.regular` | `font-regular` | `font-normal` |
| `linkVariants.size.md` | `text-md` | `text-base` |
| `AppMenu` CopyText | `text-md` | `text-base` |

`text-base` is 1rem/1.5 and `font-normal` is 400 — identical to what Preflight already gives an element that inherits (`html { line-height: 1.5 }`, `h1..h6 { font-size: inherit; font-weight: inherit }`, no `body` font-size). So on its own this change is a visual no-op **except** where an ancestor overrode font-size or weight; there, the element used to pick up the ancestor's value and would now snap to 1rem/400.

## Preserving current rendering

I walked every `.tsx` in `src` with the TypeScript AST, tracking each typography primitive's ancestor chain — including ancestors contributed by wrapper components that put a font utility on the element wrapping `{children}` (`InfoBox`'s body is `text-sm`, `TooltipContent` is `text-xs`, `DialogTitle` is `text-lg font-semibold`, `TableHead` is `font-medium`, …). 13 elements across 9 files would have changed. Each now states the value it previously inherited:

- **`InfoBox` body (`text-sm`) → `size="sm"`** — `PipelineValidationList` ×2, `ExamplePipelines` ×2, `FeaturedExamples`, `IOSection` ×2, `RemoteAuthErrorView`, `PipelineRun` ×2, `RunViewV2` ×2. (A sibling in `PipelineValidationList` already wrote `size="sm"` explicitly, confirming the intent.)
- **`InfoBox` with `className="text-xs"` → `size="xs"`** — the three inline `<Link>`s in `ManualSubmissionInstructions`.
- **`TooltipContent` (`text-xs`) → `size="xs"`** — `BetaFeatureWrapper`, matching its two siblings.
- **`DialogTitle` (`text-lg font-semibold`) → `size="lg" weight="semibold"`** — `ComponentQuickDetailsDialog`.
- **`TableHead` (`font-medium`) → `className="font-medium"`** — `TableVisualizer` ×2. `Text`'s weight scale has no `medium` step, so this one cannot be expressed as a prop.

Cases that look like nesting but are not, and were verified to need no change:

- **`asChild`** (`<Button asChild><Link>`, `<DialogDescription asChild><Paragraph>`) — Radix merges onto the *same* element, and `cn(variants, className)` puts the incoming class last, so twMerge lets the wrapper's `text-xs`/`text-sm` win.
- **`CopyText`** forwards `className` into its inner `<Text>`, so all 9 call sites are same-element merges; verified per-site with a `twMerge` probe.
- **`[&_.text-sm]:text-xs!`** (`FlexNodeDetails`, `RecentRunsContent`) keys off the emitted class name and only matches `.text-sm`. Confirmed no selector anywhere in `src` keys on `.text-base` or `.font-normal`.

## The guard

`src/components/ui/typography.test.tsx` reads the token tables Tailwind actually compiles from (`node_modules/tailwindcss/theme.css` + `src/styles/global.css`, plus `tailwind.config.js` only when `global.css` carries an `@config` directive — see #2628), renders every step of `Text`, `Paragraph`, `Heading` and `Link`, and asserts each emits a font-size/weight class that resolves to a real token. A final pair of cases bans the two dead class names from `src` outright.

Mutation-tested: restoring `text-md`/`font-regular` in `typography.tsx` fails 6 of the 23 cases — both the scale assertions and the ban, independently.

> [!NOTE]
> The issue proposed enforcing this with an ESLint `no-restricted-syntax` rule. That would not have worked: `no-restricted-syntax` is already configured for `REACT_COMPILER_ENABLED_GLOBS`, which explicitly includes `src/components/ui/typography.tsx` and `src/routes/**`, and flat config *replaces* a rule's options rather than merging them — so the new rule would be silently disabled in the very file it protects. CI also runs `pnpm run lint` without `--max-warnings`, so its `"warn"` severity could not fail the build. A test achieves the intent without either hole.

## Verification

- `pnpm run typecheck` — clean
- `pnpm run lint` — clean
- `pnpm run test` — 192 files, 1989 passed
- AST scan re-run post-fix: no unintended nested sites remain

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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