Bagas.
    Blog
    Back to Blog
    September 4, 2026·

    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.

    css
    /* 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:

    jsx
    <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.

    jsx
    <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.

    css
    @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
    jsx
    <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:

    jsx
    <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.

    jsx
    <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.

    css
    @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);
        }
      }
    }
    jsx
    <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.

    css
    :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);
    }
    jsx
    <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.

    Back to Blog
    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

    • Home
    • Blog

    Connect

    • GitHub
    • Instagram
    • bagaskoro125@gmail.com

    © 2026 Bagas. All rights reserved.

    Next.jsReactTailwind CSSDrizzle ORMTursoNextAuth