Tailwind v3 + v4

Architecture behind Tailwind CSS v3 / v4 dual support.
Components use a single source codebase, with theme files absorbing version differences.
For setup instructions, see the Installation page.

Dual support
Single source
2 theme files

Architecture

figma-tokens.json → variables.css is the single source of truth. v3-preset.js and v4-theme.css are mapping layers that reference variables.css.

Sync Flow
figma-tokens.json
Single Source of Truth
sync-tokens
Generated files
variables.css
All token HEX values
light.css
:root
dark.css
.dark
v3-preset.js
JS config + plugins
v4-theme.css
@theme + @utility
Provides utilities
Components (shared code)
bg-primary, text-md, rounded-md ...

How v3 works

  • JS preset (theme.extend)
  • Reference CSS variables via var()
  • Define custom utilities via plugins
  • Semantic color bg-primary/50 support (RGB channel variables)

How v4 works

  • CSS @theme + @utility
  • var() reference + HEX fallback (same-name variables overwritten by cascade)
  • color-mix() for runtime conversion
  • bg-primary/50 support

Sync Logic

2 patterns in which v3/v4 theme files are generated when running sync-tokens from Figma tokens.

1

var() Reference (Different Variable Names)

When theme variable names differ from CSS variable names, var() is used for direct reference.

variables.css — Source
--color-primary-text
Referenced via var()
v3/v4 theme — alias definition
--color-primary-foreground
Developer-facing class
text-primary-foreground
v4-theme.css
--color-primary-foreground: var(--color-primary-text)
--text-md: var(--font-size-md)
--color-foreground: var(--color-text)
v3-preset.js
primary.foreground: 'var(--color-primary-text)'
fontSize.md: 'var(--font-size-md)'
foreground: 'var(--color-text)'
2

CSS Cascade (Same-name Variables)

Same-name variables cannot self-reference via var(). sync-tokens generates HEX fallback values in v4-theme.css @theme, and variables.css (unlayered) always takes priority over @layer theme in the cascade. Utilities use var() references, so dark mode variable switching is automatically reflected.

CSS Cascade Order
variables.cssPriority
unlayered — always wins in cascade
↑ Overrides
v4-theme.csstheme layer
@theme — HEX fallback values
v4-theme.css (@theme = theme layer)
--color-primary: #15A0AC /* /* fallback */ */
--color-border: #E4E4E7
--radius-md: 6px
variables.css (unlayered = always wins)
--color-primary: #15A0AC /* /* priority */ */
--color-border: #E4E4E7
--radius-md: 6px
variables.css
var() reference
var(--color-primary-text)
When names differ
Colors (-foreground)Typography
CSS cascade
#15A0AC → unlayered override
Same-name variables
Colors (primary, etc.)RadiusShadow
@utility / plugins
@utility { ... var(...) }
Items not in @theme
DurationScaleZ-indexIcon

These sync operations are auto-generated by the sync-tokens CLI. There is no need to manually edit v3-preset.js / v4-theme.css.

Naming Differences

v3 and v4 differ in how theme variables are defined. The utility classes used by components are identical.

text-md
v3fontSize: { md: ['var(--font-size-md)', ...] }
v4--text-md: var(--font-size-md)
bg-primary
v3colors: { primary: 'var(--color-primary)' }
v4--color-primary: #15A0AC (cascade)
rounded-md
v3borderRadius: { md: 'var(--radius-md)' }
v4--radius-md: 6px (cascade)
text-foreground
v3colors: { foreground: 'var(--color-text)' }
v4--color-foreground: var(--color-text)
shadow-sm
v3boxShadow: { sm: 'var(--shadow-sm)' }
v4--shadow-sm: 0 1px 3px ... (cascade)
border-border
v3colors: { border: 'var(--color-border)' }
v4--color-border: #E4E4E7 (cascade)
icon-xs ~ xl
v3addUtilities plugin
v4@utility { width/height: var(...) }

Items not in the v4 @theme namespace (duration, scale, z-index, icon-size, etc.) are defined individually via @utility. In v3, the same functionality is achieved using plugins's addUtilities().

In v4, *-foreground aliases are also available (e.g., text-muted-foreground = text-text-muted). Compatible with ecosystems like shadcn/ui.

Custom Utilities

Items not in the standard Tailwind theme are defined as custom utilities, each with its own approach for v3 and v4.

Duration

duration-micro, duration-fast, duration-normal

Transition duration

Scale

scale-pressed

Scale animation on button press

Icon Sizes

icon-xs ~ icon-xl

5-step icon sizes

Focus Ring

focus-ring

Focus ring utility

Z-index

z-modal, z-tooltip, z-toast

Named Z-index layers

Animation

animate-checkbox-enter, animate-accordion-down

Component animations

v4 — @utility (defined in v4-theme.css)
duration-micro
transition-duration: var(--duration-micro)
icon-xs
width / height: var(--icon-size-xs)
focus-ring
box-shadow: 0 0 0 2px var(--color-focus-ring)
v3 — plugins (defined in v3-preset.js)
.duration-micro
transition-duration: var(--duration-micro)
.icon-xs
width / height: var(--icon-size-xs)
.focus-ring
box-shadow: 0 0 0 2px var(--color-focus-ring)

Dark Mode

Dark mode is fully managed by the .dark block in themes/dark.css. No dark mode definitions are needed in the v3/v4 theme files.

Light:root
background
text
primary
Dark.dark
background
text
primary
.dark class toggle automatically switches appearance — colors like primary can be tone-adjusted in dark.css

v3

Set darkMode: ['class'] in the user's tailwind.config.js.Since v3-preset.js references via var(),switching variables with the .dark class is automatically reflected.

v4

variables.css (unlayered) always overrides @theme (theme layer) via cascade. Utilities use var(), so .dark variable switching is automatically reflected.No .dark block is needed in v4-theme.css.

Opacity Modifier

Both v3 and v4 support opacity modifiers like bg-primary/50. v3 uses RGB channel variables, v4 uses color-mix().

100%
75%
50%
25%
10%

v3

Full support via RGB channel variables
bg-primary
bg-primary/50
text-foreground/80
border-border/50

v4

Full support via color-mix()
bg-primary
bg-primary/50
text-foreground/80
border-border/50

In v3, opacity modifiers like bg-primary/50 are also supported.RGB channel variables (--color-*-rgb) are auto-generated and <alpha-value> is applied.