Tailwind CSS 4 Theming & Tokens
Configuring CSS-first design systems in globals.css using the @theme directive, binding WordPress custom properties, and implementing robust dark mode.
Tailwind CSS 4 introduces a CSS-first architecture that eliminates the need for legacy JavaScript configuration files (tailwind.config.js). In ForgeWP, all design tokens, font scales, color variables, and animation plugins are configured directly in src/app/globals.css using the native @theme directive.
This design connects your React components directly to the WordPress design tokens you declared in wp.config.ts.
Anatomy of src/app/globals.css
Every ForgeWP theme scaffolds with a streamlined, zero-config stylesheet that binds Tailwind utilities to WordPress --wp--preset--* variables:
1@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800;900&display=swap");
2@import "tailwindcss";
3@plugin "tailwindcss-animate";
4
5@theme {
6 /* 1. Font Family Tokens */
7 --font-sans: var(
8 --wp--preset--font-family--sans,
9 "Segoe UI",
10 system-ui,
11 sans-serif
12 );
13 --font-serif: var(--wp--preset--font-family--serif, Georgia, serif);
14 --font-heading: "Space Grotesk", var(--font-sans);
15
16 /* 2. Brand & Palette Tokens */
17 --color-brand: var(--wp--preset--color--brand, oklch(0.61 0.22 42.5));
18 --color-brand-foreground: #ffffff;
19 --color-secondary: var(--wp--preset--color--secondary, #4f46e5);
20 --color-accent: var(--wp--preset--color--accent, #f59e0b);
21 --color-bg-light: var(--wp--preset--color--background, #fafafa);
22 --color-text-slate: var(--wp--preset--color--text, #0f172a);
23
24 /* 3. Shadcn UI Compatibility (Tailwind v4) */
25 --color-background: var(--wp--preset--color--background, #fafafa);
26 --color-foreground: var(--wp--preset--color--text, #0f172a);
27 --color-primary: oklch(0.61 0.22 42.5);
28 --color-primary-foreground: #ffffff;
29 --color-card: var(--wp--preset--color--background, #fafafa);
30 --color-card-foreground: var(--wp--preset--color--text, #0f172a);
31 --color-border: #e4e4e7;
32 --color-input: #e4e4e7;
33 --color-ring: oklch(0.61 0.22 42.5);
34 --color-muted: #f4f4f5;
35 --color-muted-foreground: #71717a;
36}
37
38@layer base {
39 *,
40 ::before,
41 ::after {
42 border-color: var(--color-border);
43 }
44 html,
45 body {
46 @apply antialiased;
47 -webkit-font-smoothing: antialiased !important;
48 -moz-osx-font-smoothing: grayscale !important;
49 text-rendering: optimizeLegibility;
50 }
51 body {
52 @apply bg-bg-light text-text-slate font-sans;
53 }
54 body:not(.editor-styles-wrapper):not(#tinymce):not(.mce-content-body) {
55 min-height: 100vh;
56 }
57}
58
59/* Prevent Gutenberg editor iframe height feedback loop */
60.editor-styles-wrapper {
61 min-height: auto !important;
62}Why This Variable Binding is So Powerful
Notice how --color-brand is defined:
1--color-brand: var(--wp--preset--color--brand, oklch(0.61 0.22 42.5));This structure achieves two critical objectives:
- Fallback in Local Dev: During local preview (
pnpm dev), if the WordPress server variable is not yet present, the fallback value (oklch(...)or#0f172a) ensures your components look beautiful immediately. - WordPress Preset Variables: WordPress emits CSS custom properties for the
theme.jsonpresets. The current value is available to CSS that references it. A full Site Editor panel for changing the theme-wide palette is not available for every theme type, including ForgeWP's current PHP-backed hybrid output.
Can an editor use these colors in Gutenberg content?
The exported presets are available to WordPress blocks and editor controls that support theme.json. This does not imply that every theme-wide color can be changed through the Full Site Editor. Test the controls available in the WordPress version and theme configuration you deploy.
Semantic Tokens vs Hardcoded Colors
When authoring theme components, always prefer semantic tokens over hardcoded color utilities:
Recommended Semantic Utility Avoid Using Hardcoded Colors Purpose bg-background text-foregroundbg-white text-blackUniversal page container background and primary text. bg-card text-card-foregroundbg-white text-zinc-900Cards, popovers, and elevated content panels. border-borderborder-gray-200Structural layout dividers and card outlines. text-muted-foregroundtext-gray-500Secondary dates, metadata, author bylines, and subtitles. bg-brand text-brand-foregroundbg-blue-600 text-whitePrimary call-to-action buttons and badges. |
|---|
Practical Example: Composing a Themed Card
1export function ArticleCard({
2 title,
3 excerpt,
4 date,
5}: {
6 title: string
7 excerpt: string
8 date: string
9}) {
10 return (
11 <article className="rounded-xl border border-border bg-card p-6 shadow-sm transition hover:shadow-md">
12 <span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
13 {date}
14 </span>
15 <h3 className="mt-2 font-heading text-xl font-bold text-foreground">
16 {title}
17 </h3>
18 <p className="mt-2 line-clamp-3 text-sm leading-relaxed text-muted-foreground">
19 {excerpt}
20 </p>
21 <div className="mt-6">
22 <button className="bg-brand text-brand-foreground rounded-lg px-4 py-2 text-xs font-bold transition hover:opacity-90">
23 Read Story →
24 </button>
25 </div>
26 </article>
27 )
28}Dark Mode Architecture
ForgeWP themes support dark mode out of the box using Tailwind CSS's .dark class strategy.
1. Declaring Dark Variables in globals.css
To enable seamless dark mode transitions, define your semantic tokens as CSS variables that invert when the .dark class is attached to the <html> or <body> element:
1/* Enable class-based dark mode variants (dark:*) in Tailwind v4 */
2@custom-variant dark (&:where(.dark, .dark *));
3
4:root {
5 --bg-surface: #ffffff;
6 --text-main: #09090b;
7 --border-subtle: #e4e4e7;
8 --card-surface: #ffffff;
9 --muted-surface: #f4f4f5;
10 --muted-text: #71717a;
11}
12
13.dark {
14 --bg-surface: #09090b;
15 --text-main: #fafafa;
16 --border-subtle: #27272a;
17 --card-surface: #18181b;
18 --muted-surface: #27272a;
19 --muted-text: #a1a1aa;
20}
21
22@theme {
23 --color-background: var(--bg-surface);
24 --color-foreground: var(--text-main);
25 --color-border: var(--border-subtle);
26 --color-card: var(--card-surface);
27 --color-card-foreground: var(--text-main);
28 --color-muted: var(--muted-surface);
29 --color-muted-foreground: var(--muted-text);
30}2. Creating an Interactive Theme Toggle Component
Because changing theme modes requires client interaction (toggling the .dark class on document.documentElement and persisting the choice to localStorage), we author an interactive component that the ForgeWP compiler automatically detects and hydrators:
1import { useState, useEffect } from "react"
2import { Sun, Moon } from "lucide-react"
3
4export function ThemeToggle() {
5 const [isDark, setIsDark] = useState(false)
6
7 useEffect(() => {
8 // Check saved user preference or system color scheme
9 const saved = localStorage.getItem("theme")
10 const prefersDark = window.matchMedia(
11 "(prefers-color-scheme: dark)"
12 ).matches
13 const activeDark = saved === "dark" || (!saved && prefersDark)
14
15 setIsDark(activeDark)
16 document.documentElement.classList.toggle("dark", activeDark)
17 }, [])
18
19 const toggleTheme = () => {
20 const nextDark = !isDark
21 setIsDark(nextDark)
22 document.documentElement.classList.toggle("dark", nextDark)
23 localStorage.setItem("theme", nextDark ? "dark" : "light")
24 }
25
26 return (
27 <button
28 onClick={toggleTheme}
29 className="rounded-lg border border-border bg-card p-2 text-foreground transition hover:bg-muted"
30 aria-label="Toggle Dark Mode"
31 >
32 {isDark ? (
33 <Sun className="h-4 w-4 text-accent" />
34 ) : (
35 <Moon className="h-4 w-4" />
36 )}
37 </button>
38 )
39}The WordPress Admin Bar Guard
When logged-in administrators browse your site, WordPress injects the #wpadminbar toolbar at the very top of the page.
If your dark mode styles aggressively override all HTML or body styles, the WordPress Admin Bar can suffer from illegible styling bugs (such as dark text on the admin bar's dark background).
Add this standard guard in globals.css to protect native WordPress admin controls:
1/* Prevent dark theme styles from corrupting the WordPress Admin Bar */
2#wpadminbar {
3 color: #c3c4c7 !important;
4}
5
6#wpadminbar * {
7 box-sizing: content-box !important;
8}Next Step: Styling Gutenberg Content
Now that your theme tokens and dark mode architecture are configured, discover how to style raw WordPress post markup, handle core Gutenberg blocks, and ensure pixel-perfect editor parity: