Skip to content

fix(tailwind): collapse empty-fallback var() refs in inline styles#3359

Open
mvanhorn wants to merge 2 commits intoresend:canaryfrom
mvanhorn:fix/2898-font-variant-numeric-empty-var-fallback
Open

fix(tailwind): collapse empty-fallback var() refs in inline styles#3359
mvanhorn wants to merge 2 commits intoresend:canaryfrom
mvanhorn:fix/2898-font-variant-numeric-empty-var-fallback

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Apr 17, 2026

Closes #2898

Repro

With the Tailwind component and a tabular-nums className:

<Tailwind><p className="tabular-nums">1234</p></Tailwind>

Rendered output (before this PR):

<p style="font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) tabular-nums var(--tw-numeric-fraction,)">1234</p>

No email client reliably evaluates CSS custom properties, so most inboxes drop the entire font-variant-numeric declaration and the user never sees tabular figures.

Root cause

Tailwind v4 compiles every font-variant-numeric utility into a variant-stacking chain. Each optional slot is an unresolved var(--tw-<name>,) with an empty fallback, so when a variant isn't in use it collapses to nothing per the CSS Custom Properties spec: "If the declared property does not have a value, substitute the fallback value" -- empty fallback = empty string.

makeInlineStylesFor already tries to substitute variables, but only when the custom property has an initialValue registered on :root. Tailwind v4 deliberately leaves these variant vars undefined, so the walker leaves the var() calls in place, and the generate() call writes them to the inline style object as-is.

Fix

After generate(declaration.value), strip any leftover var(--name,) calls with empty fallbacks and collapse the leftover whitespace before committing the style to the output map. Diff: +7 lines of logic + rationale comment in make-inline-styles-for.ts.

Scoped regex: only matches var( + --... + , + ) (empty fallback). Non-empty fallbacks and var(--foo) without a fallback are untouched, so this doesn't change existing resolution paths.

Rendered output (after)

<p style="font-variant-numeric:tabular-nums">1234</p>

Tests

  • New strips Tailwind v4 variant-stacking var() refs with empty fallbacks test in make-inline-styles-for.spec.ts covering the exact output Tailwind v4 emits for tabular-nums.
  • Re-snapshotted does basic local variable resolution: the previously retained leading space (" #3490dc") was an accidental artifact of generate() - the new whitespace collapse makes the output "#3490dc" without the leading space. Functionally identical for React's inline style map.
  • All 87 packages/react-email/src/components/tailwind/ tests pass locally.

Scope

  • One function touched. Does not affect resolveAllCssVariables (which already has its own fallback handling at resolve-all-css-variables.ts:192).
  • Does not fix the same class of bug for other Tailwind multi-var properties (transform, filter, backdrop-filter) - those still have the same pattern but only font-variant-numeric is in the issue. Follow-up welcome; regex is class-agnostic so it may Just Work for those if exercised.

This contribution was developed with AI assistance (Claude Code).


Summary by cubic

Removes empty-fallback var() refs from Tailwind v4’s variant-stacking in inline styles (e.g., tabular-nums) so font-variant-numeric works in emails. Scoped to --tw-* vars and trims leftover whitespace.

  • Bug Fixes
    • After value generation, remove only var(--tw-*,) tokens, collapse spaces, and preserve !important; non-empty fallbacks and non---tw-* vars are untouched.
    • Adds tests for Tailwind v4 tabular-nums and for preserving user-authored var(--my-color,)/var(--brand,) while collapsing var(--tw-custom,); updates one snapshot for whitespace trimming.

Written for commit d73e815. Summary will update on new commits.

Closes resend#2898

Tailwind v4 compiles single-class utilities like `tabular-nums` into a
variant-stacking idiom where optional variants are represented by
`var(--tw-ordinal,)` / `var(--tw-slashed-zero,)` / etc. - each with an
empty fallback so missing variants collapse to nothing.

Per the CSS Custom Properties spec an empty var() fallback resolves to
empty string, but the declarations were landing verbatim in the inline
style output because `makeInlineStylesFor` only substituted variables
that had a registered `initialValue` in `:root`. Email clients don't
support custom properties reliably, so the entire `font-variant-numeric:
...` rule was dropped in most inboxes.

Strips the empty-fallback `var(--name,)` calls from the generated CSS
value before writing it to the inline-style object, and collapses the
leftover whitespace. Adds a regression test covering the exact output
Tailwind v4 emits for `tabular-nums`. Also re-snapshots the existing
basic-local-variable test where the previously retained leading space
was a formatting artifact, not intentional behavior.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 17, 2026

⚠️ No Changeset found

Latest commit: d73e815

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 17, 2026

@mvanhorn is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 17, 2026

Open in StackBlitz

npm i https://pkg.pr.new/react-email@3359

commit: d73e815

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

// inline-style time.
const rawValue = generate(declaration.value);
const cleanedValue = rawValue
.replace(/var\(\s*--[\w-]+\s*,\s*\)/g, ' ')
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.

can this be removed using css tree? I believe they allow us to parse functions like this

// inline-style time.
const rawValue = generate(declaration.value);
const cleanedValue = rawValue
.replace(/var\(\s*--[\w-]+\s*,\s*\)/g, ' ')
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 main problem with this is that the user might want to do CSS variables themselves, and they might want to use them in tailwind utilities, so this would remove those.

Prior regex collapsed any empty-fallback var(--foo,). Narrow to --tw-*
so user-authored empty-fallback vars (even inside tailwind utilities)
pass through unchanged.

Adds a spec that asserts var(--my-color,) and var(--brand,) are
preserved while var(--tw-custom,) collapses.

Addresses review feedback from @gabrielmfern on resend#3359.
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Scoped the regex to --tw-* specifically in d73e815 so user-authored empty-fallback vars pass through untouched. Added a spec that asserts var(--my-color,) and var(--brand,) are preserved while var(--tw-custom,) collapses.

On css-tree: happy to switch to an AST-based walk if you'd rather not carry the regex. The regex felt like the minimal shape for this specific Tailwind v4 idiom, but I don't have a strong preference.

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.

font-variant-numeric not rendering correctly

2 participants