tailwind-theming

Tailwind CSS theming system for this Next.js application. Covers CSS variables, semantic tokens, dark mode, buildSectionClasses, and theme build process. Use this skill when implementing UI styling or

SKILL.md

GitHub

Tailwind Theming Skill

Patterns for working with the Tailwind CSS v4 theming system, CSS variables, and semantic tokens.

Architecture Overview

TAILWIND THEMING (v4):

Source Files:

├── styles/
│   ├── globals.css         # CSS variables (light + dark)
│   └── components.css      # Component-specific styles
├── config/
│   └── theme.config.ts     # Theme settings
└── public/
    └── theme-assets/       # Images, fonts

Generated (DO NOT EDIT):
└── src/app/layout.tsx        # imports @/styles/globals.css (written by nextspark prepare)

When to Use This Skill

  • Styling components with semantic tokens
  • Understanding CSS variable system
  • Implementing dark mode support
  • Using buildSectionClasses in blocks
  • Working with background color presets

CSS Variables (Design Tokens)

Core Semantic Colors

/* Light Mode (:root) */
:root {
  /* Backgrounds */
  --background: oklch(1 0 0);              /* Page background */
  --card: oklch(1 0 0);                    /* Card surfaces */
  --popover: oklch(1 0 0);                 /* Dropdowns, popovers */

  /* Foregrounds (text) */
  --foreground: oklch(0.145 0 0);          /* Primary text */
  --card-foreground: oklch(0.145 0 0);     /* Card text */
  --popover-foreground: oklch(0.145 0 0);  /* Popover text */

  /* Interactive */
  --primary: oklch(0.205 0 0);             /* Primary buttons/links */
  --primary-foreground: oklch(0.985 0 0);  /* Text on primary */
  --secondary: oklch(0.97 0 0);            /* Secondary elements */
  --secondary-foreground: oklch(0.205 0 0);

  /* States */
  --muted: oklch(0.97 0 0);                /* Muted backgrounds */
  --muted-foreground: oklch(0.556 0 0);    /* Muted/placeholder text */
  --accent: oklch(0.97 0 0);               /* Accent highlights */
  --accent-foreground: oklch(0.205 0 0);

  /* Feedback */
  --destructive: oklch(0.577 0.245 27.325); /* Error/danger */
  --destructive-foreground: oklch(1 0 0);

  /* Borders & Inputs */
  --border: oklch(0.922 0 0);              /* Border color */
  --input: oklch(0.922 0 0);               /* Input borders */
  --ring: oklch(0.708 0 0);                /* Focus rings */

  /* Radius */
  --radius: 0.5rem;                        /* Base border radius */
}

Dark Mode Variables

/* Dark Mode (.dark selector) */
.dark {
  /* Inverted - dark backgrounds, light text */
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);

  --card: oklch(0.145 0 0);
  --card-foreground: oklch(0.985 0 0);

  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);

  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);

  --border: oklch(0.269 0 0);
  --input: oklch(0.269 0 0);

  /* Destructive stays similar but adjusted for dark */
  --destructive: oklch(0.396 0.141 25.723);
  --destructive-foreground: oklch(0.985 0 0);
}

Sidebar Variables

:root {
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}

Chart Colors

:root {
  --chart-1: oklch(0.81 0.1 252);
  --chart-2: oklch(0.62 0.19 260);
  --chart-3: oklch(0.55 0.22 263);
  --chart-4: oklch(0.49 0.22 264);
  --chart-5: oklch(0.42 0.18 266);
}

Semantic Token Usage

Correct Usage

// ✅ ALWAYS use semantic tokens
<div className="bg-background text-foreground">
<div className="bg-card text-card-foreground">
<p className="text-muted-foreground">
<button className="bg-primary text-primary-foreground">
<span className="text-destructive">

// ✅ Borders and inputs
<div className="border border-border">
<input className="border-input focus:ring-ring">

// ✅ Radius
<div className="rounded-lg">  {/* Uses --radius */}
<div className="rounded-sm">  {/* calc(--radius - 4px) */}

Forbidden Patterns

// ❌ NEVER hardcode colors
<div className="bg-white text-black">
<div className="bg-gray-100">
<p className="text-gray-500">
<button className="bg-blue-500 text-white">

// ❌ NEVER use arbitrary color values
<div className="bg-[#ffffff]">
<p className="text-[rgb(107,114,128)]">

Background Color Presets (Blocks)

Available Values

type BackgroundColor =
  | 'transparent'   // bg-transparent
  | 'white'         // bg-white
  | 'gray-50'       // bg-gray-50
  | 'gray-100'      // bg-gray-100
  | 'gray-900'      // bg-gray-900 text-white
  | 'primary'       // bg-primary text-primary-foreground
  | 'primary-light' // bg-primary/10
  | 'primary-dark'  // bg-primary-dark text-white
  | 'secondary'     // bg-secondary text-secondary-foreground
  | 'accent'        // bg-accent text-accent-foreground

getBackgroundClasses Function

// core/types/blocks.ts

export function getBackgroundClasses(backgroundColor?: BackgroundColor): string {
  const bgMap: Record<BackgroundColor, string> = {
    transparent: 'bg-transparent',
    white: 'bg-white',
    'gray-50': 'bg-gray-50',
    'gray-100': 'bg-gray-100',
    'gray-900': 'bg-gray-900 text-white',
    primary: 'bg-primary text-primary-foreground',
    'primary-light': 'bg-primary/10',
    'primary-dark': 'bg-primary-dark text-white',
    secondary: 'bg-secondary text-secondary-foreground',
    accent: 'bg-accent text-accent-foreground',
  }
  return bgMap[backgroundColor || 'transparent']
}

buildSectionClasses Helper

Function Signature

// core/types/blocks.ts

export function buildSectionClasses(
  baseClasses: string,
  props: Partial<{
    backgroundColor?: BackgroundColor
    className?: string
  }>
): string

Usage in Blocks

import { buildSectionClasses } from '@nextsparkjs/core/types/blocks'
import type { HeroProps } from './schema'

export function HeroBlock({
  title,
  content,
  backgroundColor,
  className,
  id,
}: HeroProps) {
  // Combine base classes + background + custom
  const sectionClasses = buildSectionClasses(
    'relative flex min-h-[600px] items-center justify-center px-4 py-20',
    { backgroundColor, className }
  )

  return (
    <section id={id} className={sectionClasses}>
      {/* ... */}
    </section>
  )
}

Output Examples

buildSectionClasses('py-16 px-4', {})
// "py-16 px-4"

buildSectionClasses('py-16 px-4', { backgroundColor: 'gray-900' })
// "py-16 px-4 bg-gray-900 text-white"

buildSectionClasses('py-16 px-4', {
  backgroundColor: 'primary',
  className: 'custom-class'
})
// "py-16 px-4 bg-primary text-primary-foreground custom-class"

Dark Mode Implementation

Architecture

1. CSS Variables → Define light/dark values in :root and .dark
2. @custom-variant → Tell Tailwind to use .dark class (NOT prefers-color-scheme)
3. next-themes  → Manages .dark class on <html>
4. Tailwind     → Reads CSS variables and applies dark: variants via class

CRITICAL: Tailwind v4 Dark Mode Configuration

By default, Tailwind v4 uses @media (prefers-color-scheme: dark) for dark: variants.

This causes a mismatch with next-themes which uses the .dark class on <html>.

Required in every theme's globals.css:

@import "tailwindcss";

/* REQUIRED: Use class selector instead of prefers-color-scheme */
@custom-variant dark (&:where(.dark, .dark *));

/* ... rest of imports */

Without this configuration:

  • User sets theme to "light" in app
  • OS has dark mode enabled
  • Tailwind ignores the HTML class and uses OS preference
  • dark:bg-slate-800 applies even though app should be light

With this configuration:

  • Tailwind respects the .dark class from next-themes
  • App theme setting is honored regardless of OS preference

Theme Provider Setup

// core/providers/theme-provider.tsx
import { ThemeProvider as NextThemeProvider } from 'next-themes'

export function ThemeProvider({ children }) {
  return (
    <NextThemeProvider
      attribute="class"           // Adds .dark to <html>
      defaultTheme={defaultTheme} // light | dark | system
      enableSystem                // Respects OS preference
      disableTransitionOnChange   // No color flash
    >
      {children}
    </NextThemeProvider>
  )
}

Theme Toggle

import { useTheme } from 'next-themes'

function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Toggle theme
    </button>
  )
}

Default Theme Resolution

// Priority order:
// 1. User preference (stored in DB: uiPreferences.theme)
// 2. Theme config default (theme.config.ts → defaultMode)
// 3. System preference
// 4. Fallback: 'light'

How the Styles Load

There is no theme build script. nextspark prepare generates the root layout (src/app/layout.tsx) with import '@/styles/globals.css', so Next.js compiles the stylesheet itself: pnpm dev hot-reloads changes and pnpm build includes them. styles/components.css is only loaded if globals.css imports it. Project assets stay in public/ and are served from /.

@theme Mapping (Tailwind v4)

/* In globals.css - Maps CSS vars to Tailwind */
@theme inline {
  /* Colors */
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-primary: var(--primary);
  /* ... */

  /* Fonts */
  --font-sans: var(--font-sans);
  --font-mono: var(--font-mono);

  /* Radius */
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);

  /* Shadows */
  --shadow-sm: var(--shadow-sm);
  --shadow-md: var(--shadow-md);
}

Theme File Structure


├── config/
│   ├── theme.config.ts       # Theme metadata
│   ├── app.config.ts         # App settings
│   ├── permissions.config.ts # Role permissions
│   └── plans.config.ts       # Billing plans
├── styles/
│   ├── globals.css           # CSS variables (light + dark)
│   └── components.css        # Custom component styles
├── blocks/
│   └── {slug}/
│       └── thumbnail.png     # Block preview image
├── public/
│   └── theme-assets/         # Static assets
└── messages/
    ├── en.json               # English translations
    └── es.json               # Spanish translations

Anti-Patterns

// NEVER: Hardcode colors
<div className="bg-white text-gray-900">

// CORRECT: Use semantic tokens
<div className="bg-background text-foreground">

// NEVER: Use arbitrary colors
<div className="bg-[#1a1a1a]">

// CORRECT: Define in CSS variables if needed
<div className="bg-background">

// NEVER: Hardcode dark mode classes
<div className="bg-white dark:bg-gray-900">

// CORRECT: Use variables that auto-switch
<div className="bg-background">  {/* Switches automatically */}

// NEVER: Skip buildSectionClasses in blocks
<section className={`py-16 ${backgroundColor === 'gray-900' ? 'bg-gray-900 text-white' : ''}`}>

// CORRECT: Use helper function
<section className={buildSectionClasses('py-16', { backgroundColor })}>

// NEVER: Modify generated CSS files
// Anything under src/app/ - generated, DO NOT EDIT

// CORRECT: Modify the source files
// styles/globals.css

Checklist

Before finalizing theme implementation:

  • Using semantic tokens (bg-background, not bg-white)
  • No hardcoded colors
  • Dark mode works automatically (variables switch)
  • Blocks use buildSectionClasses helper
  • Background color uses BackgroundColor type
  • CSS changes visible in pnpm dev
  • No modifications to generated files

Related Skills

  • shadcn-components - Component styling patterns
  • page-builder-blocks - Block styling with buildSectionClasses
  • accessibility - Color contrast requirements