tailwind-csstailwind-css-4

Should I use `@theme` or `@theme inline`?


In some guides, they use @theme, while in others, they use @theme inline. If I stick to the @theme inline written by Next.js, the dark mode override mentioned in several answers here does not work:

document.querySelector('button').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
});
:root {
  --primaryLight: oklch(51.1% 0.262 276.966);
  --primaryDark: oklch(43.8% 0.218 303.724);
}
<script src="https://unpkg.com/@tailwindcss/browser"></script>
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));

@theme inline {
  --color-background: oklch(80.9% 0.105 251.813);
  --color-foreground: var(--primaryLight);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@layer theme {
  :root, :host {
    @variant dark {
      --color-background: oklch(74% 0.238 322.16);
      --color-foreground: var(--primaryDark);
    }
  }
}
</style>

<button class="w-100 h-12 bg-sky-200 text-sky-800">Click Here</button>
<div class="w-100 h-12 bg-purple-200 dark:bg-purple-900 dark:text-white">
  Example with variant (working)
</div>
<div class="w-100 h-12 bg-background text-foreground">
  Example with background and foreground (not working)
</div>

In the example, I declared two colors.

Neither works. In contrast, the dark mode toggle works well because the original TailwindCSS colors change properly with the dark: variant.

The only difference compared to the linked SO answer is that I am using @theme inline based on Next.js's recommendation. How should I properly use @theme inline so that it works in dark mode as well?


Solution

  • TL;DR: With @theme inline, you are responsible for providing the variable override, since the variable does not exist globally. In contrast, with @theme, the value is embedded into a global variable, which means its value can be overridden later.

    @theme or @theme inline?

    Firstly, @theme and @theme inline can be used together - and in fact, you can declare them in multiple places. So you don't have to choose between the two. You can confidently declare both, or even multiple identical ones, for example:

    document.querySelector('button').addEventListener('click', () => {
      document.documentElement.classList.toggle('dark');
    });
    :root {
      --primaryLight: oklch(51.1% 0.262 276.966);
      --primaryDark: oklch(43.8% 0.218 303.724);
    }
    <script src="https://unpkg.com/@tailwindcss/browser"></script>
    <style type="text/tailwindcss">
    @custom-variant dark (&:where(.dark, .dark *));
    
    @theme {
      --color-background: oklch(80.9% 0.105 251.813);
    }
    
    @theme {
      --color-foreground: var(--primaryLight);
    }
    
    @theme inline {
      --font-sans: var(--font-geist-sans);
      --font-mono: var(--font-geist-mono);
    }
    
    @layer theme {
      :root, :host {
        @variant dark {
          --color-background: oklch(74% 0.238 322.16);
          --color-foreground: var(--primaryDark);
        }
      }
    }
    </style>
    
    <button class="w-100 h-12 bg-sky-200 text-sky-800">Click Here</button>
    <div class="w-100 h-12 bg-purple-200 dark:bg-purple-900 dark:text-white">
      Example with variant (working)
    </div>
    <div class="w-100 h-12 bg-background text-foreground">
      Example with background and foreground<br>(working by "@theme" instead of "@theme inline")
    </div>

    Why can't you override the value with @theme inline the same way as in @theme?

    Due to the very nature of @theme inline, this is not possible. Unlike @theme, @theme inline does not declare a global CSS variable for the given key, so its value cannot be overridden later anywhere by referencing a global variable.

    Therefore, a hardcoded color cannot be overridden at all later on. A color managed with a variable can be overridden, but for that, you need to override the original variable's value.

    document.querySelector('button').addEventListener('click', () => {
      document.documentElement.classList.toggle('dark');
    });
    :root {
      --primaryLight: oklch(51.1% 0.262 276.966);
      --primaryDark: oklch(43.8% 0.218 303.724);
    }
    <script src="https://unpkg.com/@tailwindcss/browser"></script>
    <style type="text/tailwindcss">
    @custom-variant dark (&:where(.dark, .dark *));
    
    @theme inline {
      /* So if you provide a hardcoded color, it cannot be overridden anymore */
      --color-background: oklch(80.9% 0.105 251.813);
      
      /* If you provide a variable, it can be overridden.
         This way, you avoid having to nest the variable
         inside another global variable
         (This is the purpose of the inline approach)
      */
      --color-foreground: var(--primaryLight);
      
      --font-sans: var(--font-geist-sans);
      --font-mono: var(--font-geist-mono);
    }
    
    @layer theme {
      /* In this case, the value previously declared in `:root` can be overridden by using `*` */
      * {
        @variant dark {
          /* So only inline values declared with variables
             can be overridden by overriding the original variable */
          --primaryLight: var(--primaryDark);
          
          /* Let's be honest, this looks awkward.
             So it's recommended to name the variables more carefully */
        }
      }
    }
    </style>
    
    <button class="w-100 h-12 bg-sky-200 text-sky-800">Click Here</button>
    <div class="w-100 h-12 bg-purple-200 dark:bg-purple-900 dark:text-white">
      Example with variant (working)
    </div>
    <div class="w-100 h-12 bg-background text-foreground font-bold">
      Example with background and foreground<br>(foreground working by original variable)
    </div>

    Related:

    So, values that we want to override later in a global variable should be declared in @theme regardless of the recommendation. For values where no extra global variable is needed - such as values already received as variables - we can save an embedded global variable declaration by using @theme inline, which simply passes through the original variable's value.

    document.querySelector('button').addEventListener('click', () => {
      document.documentElement.classList.toggle('dark');
    });
    <script src="https://unpkg.com/@tailwindcss/browser"></script>
    <style type="text/tailwindcss">
    @custom-variant dark (&:where(.dark, .dark *));
    
    @theme {
      --color-background: oklch(80.9% 0.105 251.813);
    }
    
    @theme inline {
      --color-foreground: var(--myForegroundColor);
      --font-sans: var(--font-geist-sans);
      --font-mono: var(--font-geist-mono);
    }
    
    @layer theme {
      :root, :host {
        --myForegroundColor: oklch(51.1% 0.262 276.966);
        
        @variant dark {
          --color-background: oklch(74% 0.238 322.16);
          --myForegroundColor: oklch(43.8% 0.218 303.724);
        }
      }
    }
    </style>
    
    <button class="w-100 h-12 bg-sky-200 text-sky-800">Click Here</button>
    <div class="w-100 h-12 bg-purple-200 dark:bg-purple-900 dark:text-white">
      Example with variant (working)
    </div>
    <div class="w-100 h-12 bg-background text-foreground">
      Example with background and foreground (working)
    </div>