·

Advanced Tailwind CSS Techniques

Master advanced Tailwind CSS v4 techniques — CSS-first theming, custom variants, container queries, and dynamic design systems.

B

Bagas

@bagas.co
Advanced Tailwind CSS Techniques

Tailwind CSS has quickly become a favorite among front-end developers for its utility-first approach, letting you build fast without leaving your markup. Version 4 pushed this further: configuration moved from a JavaScript file into plain CSS, the engine got dramatically faster, and a handful of long-requested features — container queries, arbitrary variants, @starting-style support — finally shipped as first-class utilities. This post walks through the techniques that separate a basic Tailwind setup from a properly tuned design system.

1. CSS-first theming with @theme

Tailwind v4 no longer needs a tailwind.config.js for most projects. Design tokens — colors, fonts, spacing, breakpoints — are declared directly in CSS using the @theme directive, and Tailwind turns each token into both a utility class and a native CSS variable.

/* app.css */
@import "tailwindcss";
 
@theme {
  --color-primary: oklch(0.58 0.2 265);
  --color-secondary: oklch(0.62 0.24 320);
  --color-accent: oklch(0.75 0.15 195);
 
  --font-sans: "Inter", sans-serif;
  --font-serif: "Merriweather", serif;
 
  --breakpoint-tablet: 40rem;
  --breakpoint-laptop: 64rem;
}

Every token is now available both as a utility and as a CSS variable, so bg-primary and var(--color-primary) always agree:

<div className="bg-primary text-white p-4 font-sans">
  Tailwind custom theming in action!
</div>

Colors declared with oklch() interpolate more predictably than hex or HSL when you generate shades or animate between them — worth adopting even if you're just replacing existing hex values.

2. Container queries, not just media queries

Media-query breakpoints (sm:, md:, lg:) respond to the viewport. That falls apart the moment a component — a card, a sidebar widget — gets reused in a narrower column than the page itself. Tailwind v4 ships container queries as built-in utilities, no plugin required.

<div className="@container">
  <div className="grid grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-3 gap-4">
    <Card />
    <Card />
    <Card />
  </div>
</div>

@sm: and @lg: here respond to the width of the nearest @container, not the viewport — so the same <Card /> grid lays out correctly whether it's rendered full-width or squeezed into a narrow sidebar.

3. Custom variants and smarter dark mode

Dark mode is still a dark: prefix, but you can now define arbitrary variants with @custom-variant instead of reaching for a plugin. A common one: toggling dark mode from a data-theme attribute instead of the dark class.

@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
<div className="bg-white dark:bg-gray-800 text-black dark:text-white p-4">
  This component supports dark mode.
</div>

Flip <html data-theme="dark"> and every dark: utility on the page responds — no config file, no darkMode: "class" setting to remember.

State-based variants get the same treatment. has-* reaches into a parent to check its children, and not-* negates any variant:

<label className="group flex items-center gap-2 has-checked:text-primary">
  <input type="checkbox" className="peer" />
  <span className="not-peer-checked:opacity-50">Enable notifications</span>
</label>

4. Complex layouts with grid and flex

Tailwind's grid and flex utilities still do the heavy lifting for layout — v4 adds grid-cols-subgrid for aligning nested grids to a parent's tracks, useful for cards that need their header/body/footer rows to line up across a row of unequal content.

<div className="grid grid-cols-1 tablet:grid-cols-2 laptop:grid-cols-3 gap-4">
  <div className="bg-primary text-white p-4 rounded-lg">Item 1</div>
  <div className="bg-secondary text-white p-4 rounded-lg">Item 2</div>
  <div className="bg-accent text-white p-4 rounded-lg">Item 3</div>
</div>

This produces a single column on small screens, two at the tablet breakpoint we defined in @theme, and three at laptop — all without touching a config file.

5. Animations defined in @theme

Custom animations move into @theme the same way colors do. Declare the keyframes once, then reference them by name.

@theme {
  --animate-fade-in: fade-in 0.6s ease-out forwards;
 
  @keyframes fade-in {
    from {
      opacity: 0;
      transform: translateY(8px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
}
<div className="opacity-0 animate-fade-in">This text will fade in.</div>

Pair it with @starting-style for entry animations on elements that use display: none right up until they appear (dialogs, popovers) — something CSS couldn't express cleanly before v4.

6. Dynamic theming with CSS variables

Because @theme tokens compile down to real CSS variables, swapping an entire palette at runtime is just a matter of overriding those variables on a scope — no @apply indirection needed.

:root {
  --color-bg: oklch(0.98 0 0);
  --color-text: oklch(0.2 0 0);
}
[data-theme="dark"] {
  --color-bg: oklch(0.15 0 0);
  --color-text: oklch(0.95 0 0);
}
<div className="bg-[var(--color-bg)] text-[var(--color-text)] p-4">
  Theme-based styling with CSS variables.
</div>

Toggling data-theme on <html> or <body> now drives both your custom CSS and every Tailwind utility built from those tokens — one source of truth for the whole theme.

Conclusion

The shift to CSS-first configuration in Tailwind v4 isn't just a syntax change — it collapses the gap between "the value in my config" and "the value in my stylesheet" into a single CSS variable, and it unlocks features like container queries and @starting-style that a JS config couldn't express. If you're still on a tailwind.config.js-based v3 setup, the techniques above are the ones worth learning first when you migrate.

Available for work

Let's build something together.

Open to freelance projects, collaborations, and full-time roles.

Get in touch

Bagas.

Fullstack developer building clean, fast, and meaningful web experiences.

Pages

© 2026 Bagas. All rights reserved.

SvelteKitSvelte 5Tailwind CSSDrizzle ORMTursobits-ui