Skip to content

Instantly share code, notes, and snippets.

@RickCogley
Last active May 26, 2025 11:43
Show Gist options
  • Save RickCogley/a4ceb398d0b38b82cc884e2069e409f6 to your computer and use it in GitHub Desktop.
Save RickCogley/a4ceb398d0b38b82cc884e2069e409f6 to your computer and use it in GitHub Desktop.
Tailwind v4.x Component Layer and Design Tokens

In tailwindcss v4, instead of using @apply in @layer components, the recommendation is to use tailwind variables like:

@layer components {
  .card {
    background-color: var(--color-white);
    border-radius: var(--rounded-lg);
    padding: var(--spacing-6);
    box-shadow: var(--shadow-xl);
  }
}

This style should be used in favor of using @apply like the following, since apparently @apply causes a lot of bugs and Adam is hinting he wants to get rid of it:

table not-prose {
    @apply table-fixed w-full text-left text-sm; 
}

To convert Tailwind CSS @apply usage into the new CSS variable-based approach recommended in Tailwind CSS v4, replace classes using @apply with their corresponding CSS custom properties (variables) where available, using Tailwind’s design tokens (like --spacing-*, --color-*, --text-*, --font-*, --font-weight-*, etc.) inside @layer components.

Note,

  • Not all utilities have direct variable equivalents (e.g., table-fixed, text-left), so those are written as raw CSS.
  • Dark mode styles are handled using @media (prefers-color-scheme: dark) or :is(.dark &) depending on your setup.
  • More info on design tokens aka "theme variables" - https://tailwindcss.com/docs/theme

Common Theme Variable Namespaces

Here are some of the most commonly used variable namespaces:

  • --color-*: For colors (e.g., --color-accent-500)
  • --spacing-*: For spacing (e.g., --spacing-4, --spacing-6)
  • --font-size-*: For font sizes (e.g., --font-size-sm)
  • --font-weight-*: For font weights (e.g., --font-semibold)
  • --line-height-*: For line heights
  • --shadow-*: For box shadows
  • --rounded-*: For border radii
  • --z-*: For z-index values
  • --opacity-*: For opacity levels
  • --breakpoint-*: For responsive breakpoints

Customizing

Define or override in your theme:

@theme {
  --color-brand: #1e40af;
  --spacing-7: 1.75rem;
}

Then use in components:

.card {
  background-color: var(--color-brand);
  padding: var(--spacing-7);
}

Using theme() vs --var()

THEME IS DEPRECATED

If tailwind finds a definition in your theme block, it uses that, but if not, it falls back to default. However, why even use something like line-height: var(--leading-relaxed); rather than theme('something')?

  • theme('something.value') fetches the computed value from Tailwind's internal theme (including defaults and your @theme overrides) at compile time. It's then replaced by a static value in the final CSS.
  • var(--variable-name) fetches the value of a CSS Custom Property (CSS variable) at runtime.

Here's why var(--leading-relaxed) would be preferred over something like theme('lineHeight.relaxed'):

  1. Consistency with Custom Variables: When you define your own custom design tokens in your @theme block (like --color-esoliaamber-50 or --font-weight-light), you are creating CSS variables. Using var() to access these is the only way. By extension, if Tailwind also exposes its own utility values (like leading-relaxed) as CSS variables, using var() creates a consistent pattern for accessing all your design tokens, whether custom or Tailwind-provided.

  2. Tailwind 4's Emphasis on CSS Variables: Tailwind v4 is designed to heavily leverage native CSS variables. For properties like line-height, font-weight, and many colors, Tailwind internally generates CSS variables (e.g., --leading-relaxed, --tw-font-weight-light, --tw-red-500). When you use var(--leading-relaxed), you are directly tapping into that dynamically generated CSS variable.

  3. Potential for Runtime Dynamism (though less common for line-height): While line-height isn't typically changed dynamically at runtime, using a CSS variable allows for it. For example, if you had a component where you wanted to adjust its line height based on a JavaScript state or a parent's CSS variable, using var() would enable that flexibility, whereas theme() would resolve to a static value that can't be easily overridden by later CSS or JS.

  4. Intellisense and Discoverability: With VSCode Intellisense, it can more easily suggest and complete CSS variables that exist in the global scope (either custom ones you define or those generated by Tailwind) than deeply nested theme() paths.

When to still use theme():

You should continue to use theme() for values that are primarily configuration lookups or for properties where Tailwind doesn't typically expose a direct CSS variable:

  • Breakpoints: theme('screens.lg') is the correct way to get the pixel value of a breakpoint for a media query. Breakpoints are conceptual "screen sizes" rather than direct CSS properties that vary.
  • Font Sizes: font-size: theme('fontSize.lg'); is common because fontSize.lg isn't just a number; it often includes a line-height property as well in Tailwind's configuration, and theme() correctly extracts the font-size part.
  • Arbitrary Values: background-image: url(theme('backgroundImage.hero')); (if you had a hero image defined in backgroundImage in your config).

In essence:

  • If Tailwind exposes a utility's value as a CSS variable (like leading-relaxed maps to --leading-relaxed), it's generally best practice to use var(--leading-relaxed).
  • If you're accessing a fundamental configuration value (like a breakpoint) or a property that might include multiple values (like a complex font-size definition that includes line-height), theme() is still the appropriate choice.

This approach lets you leverage Tailwind 4's capabilities fully and makes your CSS variables consistent with its internal workings.

Spacing

Tailwind CSS v4 --spacing() Reference Chart

Assuming a root font size of 16px:

--spacing() rem px
0.25 0.0625 1px
0.5 0.125 2px
0.75 0.1875 3px
1 0.25 4px
1.25 0.3125 5px
1.5 0.375 6px
1.75 0.4375 7px
2 0.5 8px
2.25 0.5625 9px
2.5 0.625 10px
2.75 0.6875 11px
3 0.75 12px
3.25 0.8125 13px
3.5 0.875 14px
3.75 0.9375 15px
4 1 16px
4.25 1.0625 17px
4.5 1.125 18px
4.75 1.1875 19px
5 1.25 20px
5.25 1.3125 21px
5.5 1.375 22px
5.75 1.4375 23px
6 1.5 24px
6.25 1.5625 25px
6.5 1.625 26px
6.75 1.6875 27px
7 1.75 28px
7.25 1.8125 29px
7.5 1.875 30px
7.75 1.9375 31px
8 2 32px
8.25 2.0625 33px
8.5 2.125 34px
8.75 2.1875 35px
9 2.25 36px
9.25 2.3125 37px
9.5 2.375 38px
9.75 2.4375 39px
10 2.5 40px

image

@layer components {
table not-prose {
@apply table-fixed w-full text-left text-sm;
}
table.not-prose thead {
@apply bg-accent-50 text-accent-700 dark:bg-accent-600 dark:text-accent-300 text-left;
}
table.not-prose th,
table.not-prose td {
@apply p-2;
}
table.not-prose th {
@apply font-semibold;
}
table.not-prose tbody {
@apply divide-y divide-accent-200 dark:divide-accent-700;
}
table.not-prose caption {
@apply caption-bottom text-left p-2 text-xs text-accent-400;
}
/* Override the prose <a> tag hover classes */
.prose :where(a) {
@apply hover:opacity-70;
}
.prose figure + p {
margin-top: 1.3333333em; /* Add top margin to paragraphs after figures */
}
.prose p {
margin-bottom: 1.3333333em; /* Add bottom margin to all paragraphs */
}
/* Optional: Adjust top margin for the very first paragraph in the prose block */
.prose > *:first-child:is(p) {
margin-top: 0;
}
}
@layer components {
table.not-prose {
table-layout: fixed;
width: 100%;
text-align: left;
font-size: var(--text-sm);
}
table.not-prose thead {
background-color: var(--color-accent-50);
color: var(--color-accent-700);
@media (prefers-color-scheme: dark) {
background-color: var(--color-accent-600);
color: var(--color-accent-300);
}
text-align: left;
}
table.not-prose th,
table.not-prose td {
padding: var(--spacing-2);
}
table.not-prose th {
font-weight: var(--font-semibold);
}
table.not-prose tbody {
border-collapse: collapse;
border-spacing: 0;
border-top: 1px solid var(--color-accent-200);
@media (prefers-color-scheme: dark) {
border-top-color: var(--color-accent-700);
}
}
table.not-prose caption {
caption-side: bottom;
text-align: left;
padding: var(--spacing-2);
font-size: var(--text-xs);
color: var(--color-accent-400);
}
.prose :where(a):hover {
opacity: 0.7;
}
.prose figure + p {
margin-top: 1.3333333em;
}
.prose p {
margin-bottom: 1.3333333em;
}
.prose > *:first-child:is(p) {
margin-top: 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment