
Tailwind V4
- 764 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
tailwind-v4 is a frontend styling skill that implements reliable dark mode and theme switching with Tailwind CSS v4 for developers who need correct `dark:` variants and user-preference-aware themes.
About
tailwind-v4 is a skill from existential-birds/beagle focused on dark mode strategies in Tailwind CSS v4. The skill documents media-query detection via `prefers-color-scheme`, class-based and attribute-based theme toggles, and theme-switching implementation that respects user preferences. It shows v4 defaults where `@import 'tailwindcss'` enables `dark:` variants without extra configuration, plus generated CSS examples using oklch color values. Developers reach for tailwind-v4 when shipping theme toggles, fixing inconsistent dark styles, or migrating dark-mode behavior to Tailwind v4's variant model.
- Media Query Strategy that automatically respects system prefers-color-scheme with zero JavaScript
- Class-Based Strategy using the .dark class on the html element for manual theme toggling
- Attribute-Based Strategy for granular control via data attributes
- Theme Switching Implementation patterns with full user preference persistence
- Respecting User Preferences guide covering both automatic and manual controls
Tailwind V4 by the numbers
- 764 all-time installs (skills.sh)
- +9 installs in the week ending Jul 24, 2026 (Skillselion tracking)
- Ranked #456 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill tailwind-v4Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 764 |
|---|---|
| repo stars | ★ 74 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
How do you implement dark mode in Tailwind CSS v4?
Implement reliable dark mode and theme switching strategies using Tailwind CSS v4.
Who is it for?
Frontend developers building Tailwind CSS v4 UIs who need dependable dark mode across media, class, and attribute strategies.
Skip if: Projects on legacy Tailwind v3-only configs, backend API work, or teams not using Tailwind for styling.
When should I use this skill?
The user implements or debugs dark mode, theme toggles, or `dark:` variants in a Tailwind CSS v4 frontend.
What you get
Tailwind v4 CSS config, `dark:` variant usage, and theme-switching code respecting system preferences.
- theme CSS configuration
- dark mode component styles
Files
Tailwind CSS v4 Best Practices
Quick Reference
Vite Plugin Setup:
// vite.config.ts
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss()],
});CSS Entry Point:
/* src/index.css */
@import 'tailwindcss';@theme Inline Directive:
@theme inline {
--color-primary: oklch(60% 0.24 262);
--color-surface: oklch(98% 0.002 247);
}Key Differences from v3
| Feature | v3 | v4 |
|---|---|---|
| Configuration | tailwind.config.js | @theme in CSS |
| Build Tool | PostCSS plugin | @tailwindcss/vite |
| Colors | rgb() / hsl() | oklch() (default) |
| Theme Extension | extend: {} in JS | CSS variables |
| Dark Mode | darkMode config option | CSS variants |
@theme Directive Modes
default (standard mode)
Generates CSS variables that can be referenced elsewhere:
@theme {
--color-brand: oklch(60% 0.24 262);
}
/* Generates: :root { --color-brand: oklch(...); } */
/* Usage: text-brand → color: var(--color-brand) */Note: You can also use @theme default explicitly to mark theme values that can be overridden by non-default @theme declarations.
inline
Inlines values directly without CSS variables (better performance):
@theme inline {
--color-brand: oklch(60% 0.24 262);
}
/* Usage: text-brand → color: oklch(60% 0.24 262) */reference
Inlines values as fallbacks without emitting CSS variables:
@theme reference {
--color-internal: oklch(50% 0.1 180);
}
/* No :root variable, but utilities use fallback */
/* Usage: bg-internal → background-color: var(--color-internal, oklch(50% 0.1 180)) */OKLCH Color Format
OKLCH provides perceptually uniform colors with better consistency across hues:
oklch(L% C H)- L (Lightness): 0% (black) to 100% (white)
- C (Chroma): 0 (gray) to ~0.4 (vibrant)
- H (Hue): 0-360 degrees (red → yellow → green → blue → magenta)
Examples:
--color-sky-500: oklch(68.5% 0.169 237.323); /* Bright blue */
--color-red-600: oklch(57.7% 0.245 27.325); /* Vibrant red */
--color-zinc-900: oklch(21% 0.006 285.885); /* Near-black gray */CSS Variable Naming
Tailwind v4 uses double-dash CSS variable naming conventions:
@theme {
/* Colors: --color-{name}-{shade} */
--color-primary-500: oklch(60% 0.24 262);
/* Spacing: --spacing multiplier */
--spacing: 0.25rem; /* Base unit for spacing scale */
/* Fonts: --font-{family} */
--font-display: 'Inter Variable', system-ui, sans-serif;
/* Breakpoints: --breakpoint-{size} */
--breakpoint-lg: 64rem;
/* Custom animations: --animate-{name} */
--animate-fade-in: fade-in 0.3s ease-out;
}No Config Files Needed
Tailwind v4 eliminates configuration files:
- No `tailwind.config.js` - Use @theme in CSS instead
- No `postcss.config.js` - Use @tailwindcss/vite plugin
- TypeScript support - Add
@types/nodefor path resolution
{
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22.0.0",
"tailwindcss": "^4.0.0",
"vite": "^6.0.0"
}
}Progressive Disclosure
- Setup & Installation: See references/setup.md for Vite plugin configuration, package setup, TypeScript config
- Theming & Design Tokens: See references/theming.md for @theme modes, color palettes, custom fonts, animations
- Dark Mode Strategies: See references/dark-mode.md for media queries, class-based, attribute-based approaches
Gates (setup verification)
Before recommending @theme choices, OKLCH tokens, or v4 utilities, confirm the project is actually on the v4 integration path:
1. Build wiring: The Vite config loads tailwindcss() from @tailwindcss/vite, and the CSS entry uses @import 'tailwindcss'. If this fails, stop — fix wiring per references/setup.md first. 2. Major versions: package.json lists tailwindcss (and @tailwindcss/vite when using Vite) at major 4, not a v3 PostCSS-only toolchain. 3. Theme source: New theme tokens live in @theme / @theme inline in CSS — not tailwind.config.js extend (v3). If a v3 config still drives the theme, migrating wiring takes precedence over token tweaks.
Decision Guide
When to use @theme inline vs default
Use `@theme inline`:
- Better performance (no CSS variable overhead)
- Static color values that won't change
- Animation keyframes with multiple values
- Utilities that need direct value inlining
Use `@theme` (default):
- Dynamic theming with JavaScript
- CSS variable references in custom CSS
- Values that change based on context
- Better debugging (inspect CSS variables in DevTools)
When to use @theme reference
Use `@theme reference`:
- Provide fallback values without CSS variable overhead
- Values that should work even if variable isn't defined
- Reducing :root bloat while maintaining utility support
- Combining with inline for direct value substitution
Common Patterns
Two-Tier Variable System
Semantic variables that map to design tokens:
@theme {
/* Design tokens (OKLCH colors) */
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-slate-800: oklch(27.9% 0.041 260.031);
/* Semantic mappings */
--color-primary: var(--color-blue-600);
--color-surface: var(--color-slate-800);
}
/* Usage: bg-primary, bg-surface */Custom Font Configuration
@theme {
--font-display: 'Inter Variable', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--font-display--font-variation-settings: 'wght' 400;
--font-display--font-feature-settings: 'cv02', 'cv03', 'cv04';
}
/* Usage: font-display, font-mono */Animation Keyframes
@theme inline {
--animate-beacon: beacon 2s ease-in-out infinite;
@keyframes beacon {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.05);
}
}
}
/* Usage: animate-beacon */Dark Mode Strategies
Contents
- Media Query Strategy
- Class-Based Strategy
- Attribute-Based Strategy
- Theme Switching Implementation
- Respecting User Preferences
---
Media Query Strategy
Use the system preference for dark mode detection.
Configuration
Default behavior (v4):
/* No configuration needed - dark: variant works by default */
@import 'tailwindcss';Generated CSS:
@media (prefers-color-scheme: dark) {
.dark\:bg-slate-900 {
background-color: oklch(20.8% 0.042 265.755);
}
}Usage
export function Card({ children }: { children: React.ReactNode }) {
return (
<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-50">
{children}
</div>
);
}Pros & Cons
Pros:
- Respects system preference automatically
- No JavaScript needed
- Simple implementation
- No FOUC (flash of unstyled content)
Cons:
- Users can't override system preference
- No manual toggle control
- Changes when system setting changes
When to Use
- Documentation sites
- Content-focused websites
- Apps where system preference is preferred
- No need for manual theme switching
Class-Based Strategy
Toggle dark mode with a .dark class on the root element.
Configuration
Pure v4 approach: Use a v3 config file with darkMode setting:
// tailwind.config.js (for v3 compatibility)
module.exports = {
darkMode: 'class', // or 'selector' (same as 'class')
};Note: In pure v4, the default dark: variant uses media queries (prefers-color-scheme: dark). To use class-based dark mode, you need to either: 1. Use a v3 config file with darkMode: 'class' (shown above) 2. Use @import "tailwindcss/compat" and provide a config 3. Define a custom variant with @custom-variant
Generated CSS
.dark .dark\:bg-slate-900 {
background-color: oklch(20.8% 0.042 265.755);
}Usage
export function App() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDark]);
return (
<div className="bg-white dark:bg-slate-900">
<button onClick={() => setIsDark(!isDark)}>
Toggle Theme
</button>
</div>
);
}Pros & Cons
Pros:
- Full JavaScript control
- User can override system preference
- Easy to implement manual toggle
- Widely supported pattern
Cons:
- Requires JavaScript
- Potential FOUC without SSR handling
- Class management overhead
When to Use
- Applications with theme toggle
- User preference override needed
- Dashboard/admin interfaces
- Apps with per-user theme settings
Attribute-Based Strategy
Use a data-theme attribute for more semantic theming.
Configuration (v3 compat)
// tailwind.config.js (v3 compat mode)
module.exports = {
darkMode: ['class', '[data-theme="dark"]'],
};Generated CSS
[data-theme="dark"] .dark\:bg-slate-900 {
background-color: oklch(20.8% 0.042 265.755);
}Usage
export function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<div className="bg-white dark:bg-slate-900">
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
</div>
);
}Multiple Themes
Extend beyond light/dark with multiple theme attributes:
type Theme = 'light' | 'dark' | 'aviation' | 'high-contrast';
export function App() {
const [theme, setTheme] = useState<Theme>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<div className="bg-white dark:bg-slate-900 [&[data-theme='aviation']]:bg-blue-950">
<select value={theme} onChange={(e) => setTheme(e.target.value as Theme)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="aviation">Aviation</option>
<option value="high-contrast">High Contrast</option>
</select>
</div>
);
}Pros & Cons
Pros:
- Semantic HTML attribute
- Supports multiple themes (not just light/dark)
- Easy to inspect in DevTools
- Clear intent
Cons:
- Requires JavaScript
- More verbose selector in CSS
- Less common pattern
When to Use
- Multi-theme applications
- Semantic HTML preferences
- Complex theming systems
- Better DevTools debugging
Theme Switching Implementation
Complete implementation with persistence and SSR support.
React Hook
// hooks/use-theme.ts
import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
export function useTheme() {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as Theme) || 'system';
});
useEffect(() => {
const root = document.documentElement;
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
const effectiveTheme = theme === 'system' ? systemTheme : theme;
root.classList.remove('light', 'dark');
root.classList.add(effectiveTheme);
localStorage.setItem('theme', theme);
}, [theme]);
// Listen for system theme changes
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(systemTheme);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
return { theme, setTheme };
}Theme Provider Component
// components/theme-provider.tsx
import { createContext, useContext, type ReactNode } from 'react';
import { useTheme } from '@/hooks/use-theme';
type ThemeContextValue = ReturnType<typeof useTheme>;
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const value = useTheme();
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
export function useThemeContext() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useThemeContext must be used within ThemeProvider');
}
return context;
}Theme Toggle Component
// components/theme-toggle.tsx
import { Moon, Sun, Monitor } from 'lucide-react';
import { useThemeContext } from '@/components/theme-provider';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useThemeContext();
const cycleTheme = () => {
const themes: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system'];
const currentIndex = themes.indexOf(theme);
const nextIndex = (currentIndex + 1) % themes.length;
setTheme(themes[nextIndex]);
};
const Icon = theme === 'light' ? Sun : theme === 'dark' ? Moon : Monitor;
return (
<Button
variant="outline"
size="icon"
onClick={cycleTheme}
aria-label={`Current theme: ${theme}. Click to cycle themes.`}
>
<Icon className="h-4 w-4" />
</Button>
);
}SSR Script (Prevent FOUC)
Inject this script before any styled content to prevent flash:
// app/layout.tsx (Next.js example)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const theme = localStorage.getItem('theme') || 'system';
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const effectiveTheme = theme === 'system' ? systemTheme : theme;
document.documentElement.classList.add(effectiveTheme);
})();
`,
}}
/>
</head>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}Respecting User Preferences
Reduced Motion
Always respect prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}Usage in components:
export function Card() {
return (
<div className="transition-all duration-300 motion-reduce:transition-none">
Content
</div>
);
}High Contrast
Support high contrast mode:
@media (prefers-contrast: high) {
.button {
border-width: 2px;
}
}Tailwind utilities:
<button class="border contrast-more:border-2">
High Contrast Button
</button>Forced Colors
Respect forced colors mode (Windows High Contrast):
export function Card() {
return (
<div className="bg-white dark:bg-slate-900 forced-colors:bg-[Canvas] forced-colors:border forced-colors:border-[CanvasText]">
Content
</div>
);
}Combined Example
export function AccessibleCard({ children }: { children: React.ReactNode }) {
return (
<div
className={`
bg-white dark:bg-slate-900
text-slate-900 dark:text-slate-50
rounded-lg
transition-colors duration-200
motion-reduce:transition-none
border border-transparent
contrast-more:border-slate-300
forced-colors:bg-[Canvas]
forced-colors:border-[CanvasText]
`}
>
{children}
</div>
);
}Setup & Installation
Contents
- Package Installation
- Vite Plugin Configuration
- TypeScript Configuration
- CSS Entry Point
- Why No Config Files
---
Package Installation
Install Tailwind CSS v4 with the Vite plugin:
pnpm add -D tailwindcss@next @tailwindcss/vite@nextComplete package.json example:
{
"name": "amelia-dashboard",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22.0.0",
"@vitejs/plugin-react": "^5.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.6.0",
"vite": "^6.0.0"
}
}Vite Plugin Configuration
Use the @tailwindcss/vite plugin (NOT the PostCSS plugin):
// vite.config.ts
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
});Plugin options:
export type PluginOptions = {
/**
* Optimize and minify the output CSS.
* Default: true in build mode, false in dev mode
*/
optimize?: boolean | { minify?: boolean };
};
// Example with options
tailwindcss({
optimize: {
minify: true,
},
});How it works:
- Scans source files for Tailwind class candidates
- Intercepts CSS files containing
@import 'tailwindcss' - Generates utilities based on detected classes
- Watches for file changes in dev mode
- Optimizes and minifies in build mode
TypeScript Configuration
Add @types/node for path resolution in Vite config:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path resolution */
"types": ["node"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}Why `@types/node` is needed:
- Vite uses Node.js path resolution APIs
- Required for
import.meta.envtypes - Enables
path.resolve()in config files
CSS Entry Point
Create a single CSS file that imports Tailwind:
/* src/index.css */
@import 'tailwindcss';That's it. No other imports or configuration needed.
Import in your app:
// src/main.tsx
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);Advanced: Multiple entry points:
/* src/index.css */
@import 'tailwindcss';
/* Custom theme for this entry point */
@theme {
--color-primary: oklch(60% 0.24 262);
}
/* Custom utilities */
@layer utilities {
.content-auto {
content-visibility: auto;
}
}Why No Config Files
Tailwind v4 eliminates separate configuration files in favor of CSS-first configuration.
No tailwind.config.js
v3 approach (separate JS config):
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
},
},
},
};v4 approach (CSS-first):
@theme {
--color-primary: oklch(60% 0.24 262);
}Benefits:
- Configuration lives with styles
- No build-time JS evaluation
- Better CSS tooling support (syntax highlighting, autocomplete)
- Easier to understand what CSS gets generated
- No context switching between files
No postcss.config.js
v3 approach (PostCSS plugin):
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};v4 approach (Vite plugin):
// vite.config.ts
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [tailwindcss()],
});Benefits:
- Faster builds (no PostCSS overhead)
- Integrated with Vite's dev server
- Better HMR (Hot Module Replacement)
- Automatic source map generation
- Native ES modules support
Content Detection
v3 approach (manual content paths):
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
};v4 approach (automatic scanning):
/* Auto-scans all files by default */
@import 'tailwindcss';
/* Optional: Custom source patterns */
@source "src/**/*.{js,ts,jsx,tsx}";
@source "components/**/*.vue";Benefits:
- Zero configuration by default
- Explicit control when needed
- CSS-based configuration
- Easier to understand and debug
Theming & Design Tokens
Contents
- @theme Directive Modes
- CSS Variable Naming Conventions
- OKLCH Color System
- Aviation Theme Example
- Two-Tier Variable System
- Custom Font Configuration
- Animation Keyframes
---
@theme Directive Modes
Tailwind v4 provides multiple modes for defining theme values. Modes can be combined (e.g., @theme default inline, @theme inline reference).
@theme (default mode)
Generates CSS variables that can be referenced in custom CSS:
@theme {
--color-brand: oklch(60% 0.24 262);
--spacing: 0.25rem;
}Generated CSS:
:root {
--color-brand: oklch(60% 0.24 262);
--spacing: 0.25rem;
}Usage in utilities:
<div class="text-brand">Uses var(--color-brand)</div>Usage in custom CSS:
.custom-element {
color: var(--color-brand);
padding: calc(var(--spacing) * 4);
}@theme inline
Inlines values directly without CSS variable indirection:
@theme inline {
--color-brand: oklch(60% 0.24 262);
}Generated CSS (when text-brand is used):
.text-brand {
color: oklch(60% 0.24 262);
}When to use:
- Better performance (no
var()lookups) - Static values that won't change
- Utilities with multiple values (animations, shadows)
- Production builds with no runtime theming
@theme reference
Inlines values as fallbacks without emitting CSS variables to :root:
@theme reference {
--color-internal: oklch(50% 0.1 180);
}Generated CSS (when bg-internal is used):
.bg-internal {
background-color: var(--color-internal, oklch(50% 0.1 180));
}Key behavior: No :root variable is created, but the utility still works by using the value as a fallback in var().
When to use:
- Provide fallback values without CSS variable overhead
- Reduce :root bloat while maintaining utility functionality
- Values that should work even if the variable isn't defined elsewhere
- Combine with
inlinefor direct value substitution (e.g.,@theme reference inline)
@theme default
Explicitly marks theme values as defaults that can be overridden:
@theme default {
--color-primary: oklch(60% 0.24 262);
}
/* Later in the file or another file */
@theme {
--color-primary: oklch(70% 0.20 180); /* This overrides the default */
}Generated CSS:
:root, :host {
--color-primary: oklch(70% 0.20 180);
}When to use:
- Providing base theme values that can be customized
- Library or framework default themes
- Creating overridable design systems
- Used extensively in Tailwind's built-in
theme.css
Mode combinations:
@theme default inline- Default values, inlined directly@theme default reference- Default fallbacks without :root emission@theme default inline reference- All three combined
CSS Variable Naming Conventions
Tailwind v4 uses consistent naming patterns for theme variables:
Colors
--color-{name}-{shade}Examples:
@theme {
--color-primary-500: oklch(60% 0.24 262);
--color-surface-900: oklch(21% 0.006 286);
--color-success-600: oklch(62.7% 0.194 149);
}
/* Usage: text-primary-500, bg-surface-900, border-success-600 */Spacing
--spacing: {base-unit}Example:
@theme {
--spacing: 0.25rem; /* Base unit (4px at 16px root) */
}
/* Generated scale:
p-1 → padding: calc(0.25rem * 1) → 4px
p-4 → padding: calc(0.25rem * 4) → 16px
p-12 → padding: calc(0.25rem * 12) → 48px
*/Fonts
--font-{family}
--font-{family}--{feature}Examples:
@theme {
--font-sans: ui-sans-serif, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--font-display: 'Inter Variable', system-ui;
--font-display--font-variation-settings: 'wght' 400;
--font-display--font-feature-settings: 'cv02', 'cv03';
}
/* Usage: font-sans, font-mono, font-display */Breakpoints
--breakpoint-{size}: {value}Examples:
@theme {
--breakpoint-sm: 40rem; /* 640px */
--breakpoint-md: 48rem; /* 768px */
--breakpoint-lg: 64rem; /* 1024px */
--breakpoint-xl: 80rem; /* 1280px */
--breakpoint-2xl: 96rem; /* 1536px */
}Animations
--animate-{name}: {animation-value}Examples:
@theme inline {
--animate-spin: spin 1s linear infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-beacon: beacon 2s ease-in-out infinite;
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse {
50% { opacity: 0.5; }
}
@keyframes beacon {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.05); }
}
}
/* Usage: animate-spin, animate-pulse, animate-beacon */OKLCH Color System
OKLCH (Oklab LCH) provides perceptually uniform colors with consistent lightness across all hues.
Syntax
oklch(L% C H / A)- L (Lightness): 0% (black) to 100% (white)
- C (Chroma): 0 (gray) to ~0.4 (vibrant)
- H (Hue): 0-360 degrees
- A (Alpha): Optional, 0-1
Hue Wheel
0° / 360° - Red
30° - Orange
60° - Yellow
120° - Green
180° - Cyan
240° - Blue
270° - Indigo
300° - MagentaComplete Color Palette
@theme {
/* Blue scale (H ≈ 260) */
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-400: oklch(70.7% 0.165 254.624);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-blue-950: oklch(28.2% 0.091 267.935);
/* Slate scale (neutral with slight blue tint) */
--color-slate-50: oklch(98.4% 0.003 247.858);
--color-slate-100: oklch(96.8% 0.007 247.896);
--color-slate-200: oklch(92.9% 0.013 255.508);
--color-slate-300: oklch(86.9% 0.022 252.894);
--color-slate-400: oklch(70.4% 0.04 256.788);
--color-slate-500: oklch(55.4% 0.046 257.417);
--color-slate-600: oklch(44.6% 0.043 257.281);
--color-slate-700: oklch(37.2% 0.044 257.287);
--color-slate-800: oklch(27.9% 0.041 260.031);
--color-slate-900: oklch(20.8% 0.042 265.755);
--color-slate-950: oklch(12.9% 0.042 264.695);
}Chroma Guidelines
- 0: Pure gray (achromatic)
- 0.01-0.05: Subtle tint (slate, zinc)
- 0.10-0.15: Muted colors (good for backgrounds)
- 0.15-0.25: Vibrant colors (good for UI elements)
- 0.25-0.40: Maximum saturation (use sparingly)
Aviation Theme Example
Custom color palette for an aviation-themed dashboard:
@theme {
/* Flight status colors */
--color-on-time: oklch(72.3% 0.219 149.579); /* Green-600 */
--color-delayed: oklch(76.9% 0.188 70.08); /* Amber-500 */
--color-cancelled: oklch(63.7% 0.237 25.331); /* Red-500 */
--color-diverted: oklch(68.5% 0.169 237.323); /* Sky-500 */
/* Navigation colors */
--color-runway: oklch(87.1% 0.006 286.286); /* Zinc-300 */
--color-taxiway: oklch(70.5% 0.015 286.067); /* Zinc-400 */
--color-apron: oklch(55.2% 0.016 285.938); /* Zinc-500 */
/* Radar colors */
--color-primary-radar: oklch(74.6% 0.16 232.661); /* Sky-400 */
--color-secondary-radar: oklch(76.5% 0.177 163.223); /* Emerald-400 */
/* Map layers */
--color-airspace-class-a: oklch(70.7% 0.165 254.624); /* Blue-400 */
--color-airspace-class-b: oklch(84.1% 0.238 128.85); /* Lime-400 */
--color-airspace-class-c: oklch(71.8% 0.202 349.761); /* Pink-400 */
}Two-Tier Variable System
Separate design tokens from semantic naming:
@theme {
/* Tier 1: Design tokens (OKLCH primitives) */
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-slate-50: oklch(98.4% 0.003 247.858);
--color-slate-800: oklch(27.9% 0.041 260.031);
--color-slate-900: oklch(20.8% 0.042 265.755);
--color-emerald-500: oklch(69.6% 0.17 162.48);
/* Tier 2: Semantic mappings */
--color-primary: var(--color-blue-600);
--color-surface: var(--color-slate-900);
--color-surface-raised: var(--color-slate-800);
--color-text: var(--color-slate-50);
--color-success: var(--color-emerald-500);
}
/* Usage in components */
.button-primary {
background-color: var(--color-primary);
color: var(--color-text);
}
.card {
background-color: var(--color-surface-raised);
}Benefits:
- Design tokens maintain consistency
- Semantic names convey intent
- Easy theme switching (just remap tier 2)
- Clear separation of concerns
Custom Font Configuration
Configure custom fonts with variable font features:
@theme {
/* Font families */
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--font-display: 'Manrope Variable', system-ui, sans-serif;
/* Variable font settings for --font-display */
--font-display--font-variation-settings: 'wght' 600;
/* OpenType features for --font-display */
--font-display--font-feature-settings: 'ss01', 'ss02', 'cv05';
/* Font weights */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* Letter spacing */
--tracking-tight: -0.025em;
--tracking-normal: 0em;
--tracking-wide: 0.025em;
}Load fonts in HTML:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">Usage:
<h1 class="font-display font-semibold tracking-tight">Aviation Dashboard</h1>
<code class="font-mono text-sm">ATC-1234</code>Animation Keyframes
Define custom animations with @theme inline and @keyframes:
@theme inline {
/* Simple animations */
--animate-fade-in: fade-in 0.3s ease-out;
--animate-slide-up: slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1);
/* Complex animations */
--animate-beacon: beacon 2s ease-in-out infinite;
--animate-pulse-glow: pulse-glow 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
/* Keyframe definitions */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes beacon {
0%, 100% {
opacity: 1;
transform: scale(1);
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
}
50% {
opacity: 0.9;
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 8px 2px rgba(34, 197, 94, 0.4);
}
50% {
box-shadow: 0 0 16px 4px rgba(34, 197, 94, 0.8);
}
}
}Usage:
<div class="animate-fade-in">Fades in on mount</div>
<div class="animate-beacon">Pulsing beacon effect</div>
<div class="animate-pulse-glow">Glowing status indicator</div>Respecting prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Related skills
FAQ
Does Tailwind CSS v4 need extra config for dark mode?
tailwind-v4 notes that importing `tailwindcss` enables the `dark:` variant by default, using `prefers-color-scheme` media queries unless you switch to class or attribute strategies.
What dark mode strategies does tailwind-v4 cover?
tailwind-v4 documents media-query, class-based, and attribute-based dark mode plus theme-switching implementations that respect user system preferences in Tailwind CSS v4 projects.
Is Tailwind V4 safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.