Claude Skill

tailwind-ops

Tailwind CSS utility patterns, responsive design, component patterns, v4 migration, and configuration. Use for: tailwind, tailwindcss, utility classes, responsive design, dark mode, tailwind v4, tailwind config, tw, container queries, @apply, prose, typography, animation.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_tailwind-ops-3dfaf0b.zip · 34 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/tailwind-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Tailwind Operations

Comprehensive Tailwind CSS patterns covering layout, responsive design, components, dark mode, animations, and v4 migration.

Tailwind v4 ecosystem facts verified as of 2026-07.

Layout Decision Tree

Which layout approach?
│
├─ Items in a single row or column?
│  └─ Use Flexbox
│     ├─ Row:    class="flex items-center gap-4"
│     ├─ Column: class="flex flex-col gap-4"
│     ├─ Wrap:   class="flex flex-wrap gap-4"
│     └─ Push item to end: class="flex" + child class="ml-auto"
│
├─ Items in a 2D grid (rows AND columns)?
│  └─ Use CSS Grid
│     ├─ Equal columns:   class="grid grid-cols-3 gap-6"
│     ├─ Responsive grid:  class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
│     ├─ Sidebar layout:  class="grid grid-cols-[250px_1fr] gap-6"
│     ├─ Spanning:         child class="col-span-2" or "row-span-2"
│     └─ Auto-fill:        class="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-6"
│
├─ Component should adapt to its CONTAINER size (not viewport)?
│  └─ Use Container Queries (v3.2+ / v4 native)
│     ├─ Parent:  class="@container"
│     ├─ Child:   class="@sm:flex-row @lg:grid-cols-3"
│     └─ Named:   class="@container/sidebar" → child: "@sm/sidebar:flex-row"
│
├─ Centering something?
│  ├─ Horizontal text:  class="text-center"
│  ├─ Horizontal block: class="mx-auto" (needs width)
│  ├─ Flex center:      class="flex items-center justify-center"
│  ├─ Grid center:      class="grid place-items-center"
│  └─ Absolute center:  class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
│
└─ Full-page layout (header/sidebar/content/footer)?
   └─ Use Grid with named areas or template rows
      ├─ Sticky header:  class="grid grid-rows-[auto_1fr_auto] min-h-screen"
      └─ Sidebar + main: class="grid grid-cols-[250px_1fr] min-h-screen"

Responsive Design Quick Reference

Breakpoints (Mobile-First)

Prefix Min Width Typical Target
(none) 0px Mobile (default)
sm: 640px Large phones, landscape
md: 768px Tablets
lg: 1024px Small laptops
xl: 1280px Desktops
2xl: 1536px Large screens

Mobile-first means: base styles apply to mobile, add breakpoint prefixes to override upward.

<!-- Stack on mobile, 2 columns on tablet, 3 on desktop -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <div>Card 1</div>
  <div>Card 2</div>
  <div>Card 3</div>
</div>

<!-- Hide on mobile, show on desktop -->
<nav class="hidden lg:flex items-center gap-6">...</nav>

<!-- Full width on mobile, constrained on desktop -->
<div class="w-full max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">...</div>

Container Queries

<!-- Parent declares itself as a container -->
<div class="@container">
  <!-- Children respond to PARENT width, not viewport -->
  <div class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3 gap-4">
    <div>Adapts to container</div>
  </div>
</div>

<!-- Named container (useful when nesting) -->
<div class="@container/card">
  <h2 class="text-sm @md/card:text-lg">Responds to card container</h2>
</div>

Fluid Typography with clamp()

<!-- Fluid heading: 1.5rem at small, 3rem at large, scales between -->
<h1 class="text-[clamp(1.5rem,4vw,3rem)]">Fluid Heading</h1>

<!-- Fluid body text -->
<p class="text-[clamp(0.875rem,1.5vw,1.125rem)] leading-relaxed">
  Body text that scales smoothly.
</p>

Dark Mode Decision Tree

Which dark mode strategy?
│
├─ Manual toggle (user preference stored)?
│  └─ class strategy (v3) / selector strategy (v4)
│
│     v3: tailwind.config.js
│     module.exports = { darkMode: 'class' }
│     → Add class="dark" to <html> element
│
│     v4: CSS @custom-variant or default behavior
│     @custom-variant dark (&:where(.dark, .dark *));
│     → Same toggle, add class="dark" to <html>
│
├─ Follow system preference only?
│  └─ media strategy
│
│     v3: tailwind.config.js
│     module.exports = { darkMode: 'media' }
│     → Uses prefers-color-scheme automatically
│
│     v4: Default behavior (no config needed)
│     → Uses prefers-color-scheme out of the box
│
└─ Custom selector (data attribute, etc.)?
   └─ selector strategy (v4 only)

      v4: @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
      → Add data-theme="dark" to <html>

Dark Mode Patterns

<!-- Background and text -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">

  <!-- Card with dark variant -->
  <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
    <h3 class="text-gray-900 dark:text-white font-semibold">Card Title</h3>
    <p class="text-gray-600 dark:text-gray-400">Card content adapts to dark mode.</p>
  </div>

  <!-- Input with dark variant -->
  <input type="text"
    class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
           text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500
           focus:ring-2 focus:ring-blue-500 rounded-lg px-4 py-2"
    placeholder="Type here...">
</div>

Component Patterns Quick Reference

<!-- Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
  <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Title</h3>
  <p class="text-gray-600 dark:text-gray-400">Content here.</p>
</div>

<!-- Button variants -->
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors">Primary</button>
<button class="bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition-colors">Secondary</button>
<button class="border border-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors">Outline</button>
<button class="text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors">Ghost</button>

<!-- Form input -->
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
<input type="email"
  class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
         bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
         focus:ring-2 focus:ring-blue-500 focus:border-transparent"
  placeholder="you@example.com">

<!-- Navbar -->
<nav class="bg-white dark:bg-gray-900 shadow">
  <div class="max-w-7xl mx-auto px-4 flex items-center justify-between h-16">
    <a href="/" class="text-xl font-bold text-gray-900 dark:text-white">Logo</a>
    <div class="hidden md:flex items-center gap-6">
      <a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">Home</a>
      <a href="#" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">CTA</a>
    </div>
  </div>
</nav>

<!-- Modal overlay -->
<div class="fixed inset-0 z-50 flex items-center justify-center">
  <div class="fixed inset-0 bg-black/50" aria-hidden="true"></div>
  <div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true">
    <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Modal Title</h2>
    <p class="text-gray-600 dark:text-gray-400 mb-6">Modal content goes here.</p>
    <div class="flex justify-end gap-3">
      <button class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg">Cancel</button>
      <button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Confirm</button>
    </div>
  </div>
</div>

<!-- Badge -->
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">Active</span>

<!-- Alert -->
<div class="flex items-start gap-3 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800" role="alert">
  <span class="text-red-600 dark:text-red-400 mt-0.5" aria-hidden="true">&#10007;</span>
  <div>
    <h4 class="text-sm font-medium text-red-800 dark:text-red-300">Error</h4>
    <p class="text-sm text-red-700 dark:text-red-400 mt-1">Something went wrong. Please try again.</p>
  </div>
</div>

Tailwind v4 Quick Reference

Major Changes from v3

Area v3 v4
Configuration tailwind.config.js CSS-first: @theme in CSS
Theme values JS theme.extend.colors @theme { --color-brand: #3b82f6; }
Plugins JS plugin() function @plugin "my-plugin" in CSS
Config file module.exports = {...} @config "./legacy.config.js" (compat)
PostCSS tailwindcss package @tailwindcss/postcss
Vite PostCSS plugin @tailwindcss/vite (faster)
Colors Named scales (gray-50..950) Same + OKLCH support
Container queries Plugin required Native @container, @sm:, @md:
Entry animations JS needed @starting-style (CSS native)

v4 CSS-First Config

/* v4: Define theme in CSS */
@import "tailwindcss";

@theme {
  --color-brand: #3b82f6;
  --color-brand-dark: #1d4ed8;
  --font-display: "Inter", sans-serif;
  --breakpoint-3xl: 1920px;
  --spacing-18: 4.5rem;
}

/* v4: Import a plugin */
@plugin "@tailwindcss/typography";

/* v4: Use legacy JS config as fallback */
@config "./tailwind.config.js";

v4 New Utilities

<!-- Container queries (native in v4) -->
<div class="@container">
  <div class="@sm:flex @md:grid @md:grid-cols-2">Adapts to container</div>
</div>

<!-- @starting-style: entry animations without JS -->
<!-- Applied via CSS - Tailwind v4 supports it natively -->

<!-- Anchor positioning (experimental) -->
<!-- Position elements relative to an anchor element via CSS -->

<!-- New shadow and ring defaults -->
<div class="shadow-sm ring ring-blue-500/20">Improved defaults</div>

Animation Patterns

Transition Utilities

<!-- Color transition (most common) -->
<button class="bg-blue-600 hover:bg-blue-700 transition-colors duration-150">
  Hover me
</button>

<!-- Multiple properties -->
<div class="transform hover:scale-105 hover:shadow-lg transition-all duration-200 ease-in-out">
  Scale and shadow on hover
</div>

<!-- Specific properties -->
<div class="transition-[transform,opacity] duration-300 ease-out">
  Only transform and opacity animate
</div>

Built-in Animations

<!-- Spin (loading spinners) -->
<svg class="animate-spin h-5 w-5 text-blue-600" viewBox="0 0 24 24">
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>

<!-- Pulse (skeleton loaders) -->
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-4 rounded w-3/4"></div>

<!-- Ping (notification indicator) -->
<span class="relative flex h-3 w-3">
  <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
  <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>

<!-- Bounce -->
<div class="animate-bounce">&#8595;</div>

Custom Keyframes (v3 Config)

// tailwind.config.js (v3)
module.exports = {
  theme: {
    extend: {
      keyframes: {
        'fade-in': {
          '0%': { opacity: '0', transform: 'translateY(10px)' },
          '100%': { opacity: '1', transform: 'translateY(0)' },
        },
        'slide-in-right': {
          '0%': { transform: 'translateX(100%)' },
          '100%': { transform: 'translateX(0)' },
        },
      },
      animation: {
        'fade-in': 'fade-in 0.3s ease-out',
        'slide-in-right': 'slide-in-right 0.3s ease-out',
      },
    },
  },
}

Custom Keyframes (v4 CSS)

/* v4: Define in CSS with @theme */
@theme {
  --animate-fade-in: fade-in 0.3s ease-out;
  --animate-slide-in-right: slide-in-right 0.3s ease-out;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes slide-in-right {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

Entry Animations with @starting-style (v4)

/* Dialog that animates in from transparent/translated */
dialog[open] {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s, transform 0.3s;

  @starting-style {
    opacity: 0;
    transform: translateY(10px);
  }
}

State Modifiers Quick Reference

Modifier Triggers On Example
hover: Mouse hover hover:bg-blue-700
focus: Element focused (all focus) focus:ring-2
focus-visible: Keyboard focus only focus-visible:outline-2
focus-within: Child is focused focus-within:ring-2
active: Being clicked/pressed active:scale-95
disabled: disabled attribute disabled:opacity-50 disabled:cursor-not-allowed
group-hover: Parent .group hovered group-hover:text-blue-600
group-focus: Parent .group focused group-focus:ring-2
peer-checked: Sibling .peer checked peer-checked:bg-blue-600
peer-invalid: Sibling .peer invalid peer-invalid:text-red-500
data-[state=open]: Custom data attribute data-[state=open]:rotate-180
aria-expanded: aria-expanded="true" aria-expanded:bg-gray-100
aria-selected: aria-selected="true" aria-selected:font-bold
open: <details> or <dialog> open open:bg-gray-50
first: First child first:rounded-t-lg
last: Last child last:rounded-b-lg
odd: Odd children odd:bg-gray-50
even: Even children even:bg-white
placeholder: Placeholder text placeholder:text-gray-400
motion-reduce: Prefers reduced motion motion-reduce:transition-none
motion-safe: No motion preference motion-safe:animate-bounce
print: Print media print:hidden

Group and Peer Patterns

<!-- Group: parent state affects children -->
<a href="#" class="group flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100">
  <div class="w-10 h-10 bg-gray-200 group-hover:bg-blue-100 rounded-lg"></div>
  <span class="text-gray-700 group-hover:text-blue-600">Hover the whole card</span>
</a>

<!-- Named groups (nested groups) -->
<div class="group/card p-4">
  <div class="group/button">
    <span class="group-hover/card:text-blue-600 group-hover/button:underline">
      Responds to specific parent
    </span>
  </div>
</div>

<!-- Peer: sibling state affects next sibling -->
<input type="checkbox" class="peer sr-only" id="toggle">
<label for="toggle" class="peer-checked:bg-blue-600 peer-checked:text-white px-4 py-2 rounded-lg cursor-pointer">
  Toggle me
</label>

<!-- Form validation with peer -->
<input type="email" class="peer" required>
<p class="hidden peer-invalid:block text-sm text-red-500 mt-1">
  Please enter a valid email.
</p>

Common Gotchas

Gotcha Why Fix
Dynamic class names don't work: `bg-${color}-500` Tailwind scans source for complete class strings at build time. String interpolation produces classes it never sees. Use complete classes: const colors = { red: 'bg-red-500', blue: 'bg-blue-500' } and select by key.
Styles not applying (specificity) Another CSS rule or @apply has higher specificity. Use !important modifier: !text-red-500. Or restructure to avoid conflicts.
@apply breaks with component libraries @apply resolves at build time and can't access runtime theme values or conflict with scoped styles. Prefer inline utility classes. Reserve @apply for base styles or markdown content.
Prose plugin styles leak @tailwindcss/typography prose applies broad element selectors (h1, p, a, etc.). Scope with prose only on content wrappers. Use not-prose class to exclude sections.
Classes missing in production JIT content detection didn't scan the file containing the class. Ensure content paths in config cover all template files including component libraries.
Dark mode flash (FOUC) Class-based dark mode renders light first until JS adds dark class. Add inline <script> in <head> that reads localStorage and sets dark class before paint.
Container queries not scoped Child @sm: responds to nearest @container ancestor, which may not be the intended one. Use named containers: @container/card and @sm/card:flex.
Arbitrary values vs config w-[137px] works but creates one-off values. Repeated arbitrary values signal missing design tokens. Add recurring values to theme config: spacing: { '137': '137px' }.
group / peer naming collisions Nested groups without names cause children to respond to wrong ancestor. Use named groups: group/card, group/button.
Responsive order matters Adding lg:flex without base block or hidden can cause unexpected behavior on smaller screens. Always define the mobile-first base, then override upward: hidden lg:flex.
Transition on display: none hidden to block can't be transitioned because display isn't animatable. Use opacity-0/opacity-100 with invisible/visible, or use @starting-style (v4).
Purge removes dynamic classes Tailwind purges classes not found as complete strings in scanned files. Add classes to safelist in config, or use a safelist comment in the source file.

Reference Files

File Content Lines
references/component-patterns.md Cards, buttons, forms, navigation, modals, tables, alerts, badges, avatars, dropdowns, tooltips, skeleton loaders, accessibility ~700
references/v4-migration.md CSS-first config, @theme, @plugin, removed utilities, container queries, @starting-style, migration steps, breaking changes ~500
references/configuration.md Theme config (v3+v4), colors, spacing, typography, plugins, @layer, @apply, custom variants, dark mode, container queries ~500

Staleness Verifier

This skill encodes fast-moving facts (the Tailwind v4 CSS-first directives, the @tailwindcss/* package set, the v3→v4 migration). scripts/check-tailwind-facts.py guards them against silent drift — internal consistency in PR CI, live major-version drift in the scheduled freshness job:

# Structural (PR CI, no network): every catalogued package + v4 directive gate
# is still named in this skill's prose, and the currency note carries a year.
python3 skills/tailwind-ops/scripts/check-tailwind-facts.py --offline        # exit 0 consistent, 10 drift

# Live (weekly freshness job, never blocks a PR): is any documented major now
# behind npm's latest dist-tag? (e.g. tailwindcss 5 while the prose says v4.)
python3 skills/tailwind-ops/scripts/check-tailwind-facts.py --live           # exit 10 a major moved ahead, 7 npm unreachable

The canonical fact list lives in assets/tailwind-facts.json; when you add or drop a package or the prose stops naming one, update it to match or --offline fails CI.

See Also

Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
    • tailwind-facts.json 2 KB
      {
        "_comment": "Canonical fast-moving facts the tailwind-ops skill encodes. scripts/check-tailwind-facts.py asserts SKILL.md + references name these consistently (--offline) and probes the npm registry for major-version drift (--live). Edit deliberately: a change here is a skill-content decision, not housekeeping. documented_major is the major the skill's prose commits to (tailwindcss 4) or the current tracked major for ecosystem packages the prose names without pinning a version.",
        "schema": "claude-mods.tailwind-ops.facts/v1",
        "as_of": "2026-07-05",
        "tailwind_major": 4,
        "version_gates": {
          "_comment": "Tailwind v4 CSS-first directives the skill centers on. --offline asserts each token is named in SKILL.md/references prose; a missing token means the prose stopped teaching a v4 feature it claims.",
          "css_first_theme": "@theme",
          "plugin_directive": "@plugin",
          "config_directive": "@config",
          "v4_import": "@import \"tailwindcss\"",
          "v4_major": "v4"
        },
        "packages": {
          "tailwindcss":                     { "documented_major": 4, "prose": ["tailwindcss"],                    "role": "core (v4 CSS-first engine)" },
          "@tailwindcss/postcss":            { "documented_major": 4, "prose": ["@tailwindcss/postcss"],           "role": "PostCSS plugin (v4)" },
          "@tailwindcss/vite":               { "documented_major": 4, "prose": ["@tailwindcss/vite"],              "role": "Vite plugin (v4, faster)" },
          "@tailwindcss/upgrade":            { "documented_major": 4, "prose": ["@tailwindcss/upgrade"],           "role": "v3->v4 codemod CLI" },
          "@tailwindcss/typography":         { "documented_major": 0, "prose": ["@tailwindcss/typography"],        "role": "prose content plugin" },
          "@tailwindcss/forms":              { "documented_major": 0, "prose": ["@tailwindcss/forms"],             "role": "form reset plugin" },
          "@tailwindcss/container-queries":  { "documented_major": 0, "prose": ["@tailwindcss/container-queries"], "role": "container-queries plugin (v3; native in v4)" }
        }
      }
      
  • references
    • component-patterns.md 46.9 KB
      # Tailwind Component Patterns
      
      Complete, accessible component patterns with Tailwind CSS. All examples include dark mode support and accessibility attributes.
      
      ## Cards
      
      ### Basic Card
      
      ```html
      <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
        <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Card Title</h3>
        <p class="text-gray-600 dark:text-gray-400">Card content goes here with a reasonable amount of text.</p>
      </div>
      ```
      
      ### Card with Image
      
      ```html
      <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
        <img src="image.jpg" alt="Description of image" class="w-full h-48 object-cover">
        <div class="p-6">
          <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Card Title</h3>
          <p class="text-gray-600 dark:text-gray-400 mb-4">Supporting text for this card.</p>
          <a href="#" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">Read more &rarr;</a>
        </div>
      </div>
      ```
      
      ### Horizontal Card
      
      ```html
      <div class="flex flex-col sm:flex-row bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
        <img src="image.jpg" alt="Description" class="w-full sm:w-48 h-48 sm:h-auto object-cover">
        <div class="p-6 flex flex-col justify-center">
          <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Horizontal Card</h3>
          <p class="text-gray-600 dark:text-gray-400">Image sits beside the content on larger screens, stacks on mobile.</p>
        </div>
      </div>
      ```
      
      ### Interactive Card (Clickable)
      
      ```html
      <a href="#" class="block bg-white dark:bg-gray-800 rounded-lg shadow-md p-6
                         hover:shadow-lg hover:ring-2 hover:ring-blue-500/20
                         focus-visible:outline-2 focus-visible:outline-blue-600
                         transition-all duration-200">
        <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Clickable Card</h3>
        <p class="text-gray-600 dark:text-gray-400">The entire card is a link with hover and focus states.</p>
      </a>
      ```
      
      ### Pricing Card
      
      ```html
      <div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 border-2 border-blue-500 relative">
        <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-blue-500 text-white px-4 py-1 rounded-full text-sm font-medium">
          Most Popular
        </div>
        <h3 class="text-xl font-bold text-gray-900 dark:text-white">Pro Plan</h3>
        <div class="mt-4">
          <span class="text-4xl font-bold text-gray-900 dark:text-white">$29</span>
          <span class="text-gray-500 dark:text-gray-400">/month</span>
        </div>
        <ul class="mt-6 space-y-3" role="list">
          <li class="flex items-center text-gray-600 dark:text-gray-400">
            <svg class="w-5 h-5 text-green-500 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
            </svg>
            Unlimited projects
          </li>
          <li class="flex items-center text-gray-600 dark:text-gray-400">
            <svg class="w-5 h-5 text-green-500 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
            </svg>
            Advanced analytics
          </li>
          <li class="flex items-center text-gray-600 dark:text-gray-400">
            <svg class="w-5 h-5 text-green-500 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
            </svg>
            Priority support
          </li>
        </ul>
        <button class="w-full mt-8 bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors">
          Get Started
        </button>
      </div>
      ```
      
      ### Feature Card
      
      ```html
      <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
        <div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mb-4">
          <svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
          </svg>
        </div>
        <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Feature Name</h3>
        <p class="text-gray-600 dark:text-gray-400">Description of this feature and its value to users.</p>
      </div>
      ```
      
      ## Buttons
      
      ### Primary Button
      
      ```html
      <button class="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium
                     hover:bg-blue-700 active:bg-blue-800
                     focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600
                     transition-colors">
        Primary Action
      </button>
      ```
      
      ### Secondary Button
      
      ```html
      <button class="bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-4 py-2 rounded-lg font-medium
                     hover:bg-gray-300 dark:hover:bg-gray-600 active:bg-gray-400
                     focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500
                     transition-colors">
        Secondary Action
      </button>
      ```
      
      ### Outline Button
      
      ```html
      <button class="border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 px-4 py-2 rounded-lg font-medium
                     hover:bg-gray-50 dark:hover:bg-gray-800 active:bg-gray-100
                     focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500
                     transition-colors">
        Outline Action
      </button>
      ```
      
      ### Ghost Button
      
      ```html
      <button class="text-blue-600 dark:text-blue-400 px-4 py-2 rounded-lg font-medium
                     hover:bg-blue-50 dark:hover:bg-blue-900/20 active:bg-blue-100
                     focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600
                     transition-colors">
        Ghost Action
      </button>
      ```
      
      ### Icon Button
      
      ```html
      <button class="p-2 rounded-lg text-gray-500 dark:text-gray-400
                     hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200
                     focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500
                     transition-colors"
              aria-label="Close">
        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
        </svg>
      </button>
      ```
      
      ### Loading Button
      
      ```html
      <button class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg font-medium
                     disabled:opacity-60 disabled:cursor-not-allowed transition-colors" disabled>
        <svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" aria-hidden="true">
          <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
          <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
        </svg>
        Processing...
      </button>
      ```
      
      ### Button Group
      
      ```html
      <div class="inline-flex rounded-lg shadow-sm" role="group">
        <button class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800
                       border border-gray-300 dark:border-gray-600 rounded-l-lg
                       hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-10 focus-visible:outline-2 focus-visible:outline-blue-600">
          Left
        </button>
        <button class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800
                       border-t border-b border-gray-300 dark:border-gray-600
                       hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-10 focus-visible:outline-2 focus-visible:outline-blue-600">
          Center
        </button>
        <button class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800
                       border border-gray-300 dark:border-gray-600 rounded-r-lg
                       hover:bg-gray-50 dark:hover:bg-gray-700 focus:z-10 focus-visible:outline-2 focus-visible:outline-blue-600">
          Right
        </button>
      </div>
      ```
      
      ### Button Sizes
      
      ```html
      <!-- Small -->
      <button class="bg-blue-600 text-white px-3 py-1.5 text-sm rounded-md font-medium hover:bg-blue-700 transition-colors">Small</button>
      <!-- Medium (default) -->
      <button class="bg-blue-600 text-white px-4 py-2 text-base rounded-lg font-medium hover:bg-blue-700 transition-colors">Medium</button>
      <!-- Large -->
      <button class="bg-blue-600 text-white px-6 py-3 text-lg rounded-lg font-medium hover:bg-blue-700 transition-colors">Large</button>
      ```
      
      ## Forms
      
      ### Text Input
      
      ```html
      <div>
        <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Full Name</label>
        <input type="text" id="name"
          class="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                 text-gray-900 dark:text-gray-100 rounded-lg
                 placeholder:text-gray-400 dark:placeholder:text-gray-500
                 focus:ring-2 focus:ring-blue-500 focus:border-transparent
                 disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:cursor-not-allowed"
          placeholder="John Doe">
      </div>
      ```
      
      ### Textarea
      
      ```html
      <div>
        <label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message</label>
        <textarea id="message" rows="4"
          class="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                 text-gray-900 dark:text-gray-100 rounded-lg resize-y
                 placeholder:text-gray-400 dark:placeholder:text-gray-500
                 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          placeholder="Write your message..."></textarea>
        <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Max 500 characters.</p>
      </div>
      ```
      
      ### Select
      
      ```html
      <div>
        <label for="country" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Country</label>
        <select id="country"
          class="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                 text-gray-900 dark:text-gray-100 rounded-lg
                 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
          <option value="">Select a country</option>
          <option value="us">United States</option>
          <option value="uk">United Kingdom</option>
          <option value="ca">Canada</option>
        </select>
      </div>
      ```
      
      ### Checkbox
      
      ```html
      <div class="flex items-start">
        <input type="checkbox" id="terms"
          class="mt-1 h-4 w-4 rounded border-gray-300 dark:border-gray-600
                 text-blue-600 focus:ring-blue-500 dark:bg-gray-800">
        <label for="terms" class="ml-2 text-sm text-gray-700 dark:text-gray-300">
          I agree to the <a href="#" class="text-blue-600 dark:text-blue-400 hover:underline">terms and conditions</a>
        </label>
      </div>
      ```
      
      ### Radio Group
      
      ```html
      <fieldset>
        <legend class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notification preference</legend>
        <div class="space-y-2">
          <label class="flex items-center">
            <input type="radio" name="notification" value="email"
              class="h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-800">
            <span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Email</span>
          </label>
          <label class="flex items-center">
            <input type="radio" name="notification" value="sms"
              class="h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-800">
            <span class="ml-2 text-sm text-gray-700 dark:text-gray-300">SMS</span>
          </label>
          <label class="flex items-center">
            <input type="radio" name="notification" value="push"
              class="h-4 w-4 border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-800">
            <span class="ml-2 text-sm text-gray-700 dark:text-gray-300">Push notification</span>
          </label>
        </div>
      </fieldset>
      ```
      
      ### Toggle Switch
      
      ```html
      <label class="relative inline-flex items-center cursor-pointer">
        <input type="checkbox" class="sr-only peer">
        <div class="w-11 h-6 bg-gray-200 dark:bg-gray-700 rounded-full
                    peer-checked:bg-blue-600 peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500
                    after:content-[''] after:absolute after:top-0.5 after:left-[2px]
                    after:bg-white after:rounded-full after:h-5 after:w-5
                    after:transition-all peer-checked:after:translate-x-full"></div>
        <span class="ml-3 text-sm text-gray-700 dark:text-gray-300">Enable notifications</span>
      </label>
      ```
      
      ### File Upload
      
      ```html
      <div>
        <label for="file-upload" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Upload file</label>
        <div class="flex items-center justify-center w-full">
          <label for="file-upload"
            class="flex flex-col items-center justify-center w-full h-32
                   border-2 border-dashed border-gray-300 dark:border-gray-600
                   rounded-lg cursor-pointer
                   hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/10
                   transition-colors">
            <svg class="w-8 h-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                    d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
            </svg>
            <p class="text-sm text-gray-500 dark:text-gray-400"><span class="font-medium text-blue-600 dark:text-blue-400">Click to upload</span> or drag and drop</p>
            <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">PNG, JPG, PDF up to 10MB</p>
            <input type="file" id="file-upload" class="hidden">
          </label>
        </div>
      </div>
      ```
      
      ### Search Input
      
      ```html
      <div class="relative">
        <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
          <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
          </svg>
        </div>
        <input type="search"
          class="w-full pl-10 pr-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                 text-gray-900 dark:text-gray-100 rounded-lg
                 placeholder:text-gray-400 dark:placeholder:text-gray-500
                 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          placeholder="Search...">
      </div>
      ```
      
      ### Input with Icon (Addon)
      
      ```html
      <div>
        <label for="website" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Website</label>
        <div class="flex">
          <span class="inline-flex items-center px-3 rounded-l-lg border border-r-0 border-gray-300 dark:border-gray-600
                       bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-sm">
            https://
          </span>
          <input type="text" id="website"
            class="flex-1 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                   text-gray-900 dark:text-gray-100 rounded-r-lg
                   focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            placeholder="example.com">
        </div>
      </div>
      ```
      
      ### Input Group (Button Addon)
      
      ```html
      <div class="flex">
        <input type="email"
          class="flex-1 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                 text-gray-900 dark:text-gray-100 rounded-l-lg
                 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          placeholder="you@example.com">
        <button class="px-6 py-2 bg-blue-600 text-white rounded-r-lg font-medium
                       hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-600
                       transition-colors">
          Subscribe
        </button>
      </div>
      ```
      
      ### Validation States
      
      ```html
      <!-- Error state -->
      <div>
        <label for="email-err" class="block text-sm font-medium text-red-700 dark:text-red-400 mb-1">Email</label>
        <input type="email" id="email-err"
          class="w-full px-3 py-2 bg-white dark:bg-gray-800
                 border border-red-500 text-gray-900 dark:text-gray-100 rounded-lg
                 focus:ring-2 focus:ring-red-500 focus:border-transparent"
          value="invalid-email" aria-describedby="email-error" aria-invalid="true">
        <p id="email-error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">
          Please enter a valid email address.
        </p>
      </div>
      
      <!-- Success state -->
      <div>
        <label for="email-ok" class="block text-sm font-medium text-green-700 dark:text-green-400 mb-1">Email</label>
        <input type="email" id="email-ok"
          class="w-full px-3 py-2 bg-white dark:bg-gray-800
                 border border-green-500 text-gray-900 dark:text-gray-100 rounded-lg
                 focus:ring-2 focus:ring-green-500 focus:border-transparent"
          value="user@example.com" aria-describedby="email-success">
        <p id="email-success" class="mt-1 text-sm text-green-600 dark:text-green-400">
          Email address is valid.
        </p>
      </div>
      ```
      
      ## Navigation
      
      ### Horizontal Navbar
      
      ```html
      <nav class="bg-white dark:bg-gray-900 shadow" aria-label="Main navigation">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div class="flex items-center justify-between h-16">
            <a href="/" class="text-xl font-bold text-gray-900 dark:text-white">Brand</a>
            <div class="hidden md:flex items-center gap-6">
              <a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors">Home</a>
              <a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors">Features</a>
              <a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors">Pricing</a>
              <a href="#" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">Get Started</a>
            </div>
            <!-- Mobile menu button -->
            <button class="md:hidden p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
                    aria-label="Toggle menu" aria-expanded="false">
              <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
              </svg>
            </button>
          </div>
        </div>
      </nav>
      ```
      
      ### Mobile Hamburger Menu (Expanded State)
      
      ```html
      <!-- Mobile menu panel (toggle with JS) -->
      <div class="md:hidden bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
        <div class="px-4 py-3 space-y-1">
          <a href="#" class="block px-3 py-2 rounded-lg text-gray-900 dark:text-white bg-gray-100 dark:bg-gray-800 font-medium">Home</a>
          <a href="#" class="block px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800">Features</a>
          <a href="#" class="block px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800">Pricing</a>
        </div>
        <div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
          <a href="#" class="block w-full text-center bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">Get Started</a>
        </div>
      </div>
      ```
      
      ### Sidebar Navigation
      
      ```html
      <aside class="w-64 bg-gray-900 text-white min-h-screen flex flex-col" aria-label="Sidebar navigation">
        <div class="p-4 border-b border-gray-800">
          <h2 class="text-lg font-semibold">Dashboard</h2>
        </div>
        <nav class="flex-1 p-4 space-y-1">
          <a href="#" class="flex items-center gap-3 px-3 py-2 bg-gray-800 rounded-lg text-white font-medium"
             aria-current="page">
            <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
            </svg>
            Home
          </a>
          <a href="#" class="flex items-center gap-3 px-3 py-2 text-gray-400 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
            <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
            </svg>
            Users
          </a>
          <a href="#" class="flex items-center gap-3 px-3 py-2 text-gray-400 hover:bg-gray-800 hover:text-white rounded-lg transition-colors">
            <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
            </svg>
            Settings
          </a>
        </nav>
      </aside>
      ```
      
      ### Breadcrumbs
      
      ```html
      <nav aria-label="Breadcrumb">
        <ol class="flex items-center gap-2 text-sm">
          <li><a href="#" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200">Home</a></li>
          <li class="text-gray-400 dark:text-gray-500" aria-hidden="true">/</li>
          <li><a href="#" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200">Products</a></li>
          <li class="text-gray-400 dark:text-gray-500" aria-hidden="true">/</li>
          <li class="text-gray-900 dark:text-white font-medium" aria-current="page">Widget Pro</li>
        </ol>
      </nav>
      ```
      
      ### Tabs
      
      ```html
      <div>
        <div class="border-b border-gray-200 dark:border-gray-700" role="tablist">
          <nav class="flex gap-0 -mb-px">
            <button role="tab" aria-selected="true"
              class="px-4 py-3 text-sm font-medium text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400">
              General
            </button>
            <button role="tab" aria-selected="false"
              class="px-4 py-3 text-sm font-medium text-gray-500 dark:text-gray-400 border-b-2 border-transparent
                     hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300">
              Security
            </button>
            <button role="tab" aria-selected="false"
              class="px-4 py-3 text-sm font-medium text-gray-500 dark:text-gray-400 border-b-2 border-transparent
                     hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300">
              Billing
            </button>
          </nav>
        </div>
        <div role="tabpanel" class="p-4">
          Tab content goes here.
        </div>
      </div>
      ```
      
      ### Pagination
      
      ```html
      <nav aria-label="Pagination">
        <ul class="inline-flex items-center gap-1">
          <li>
            <a href="#" class="px-3 py-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800" aria-label="Previous page">
              &laquo;
            </a>
          </li>
          <li><a href="#" class="px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">1</a></li>
          <li><a href="#" class="px-3 py-2 rounded-lg bg-blue-600 text-white font-medium" aria-current="page">2</a></li>
          <li><a href="#" class="px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">3</a></li>
          <li><span class="px-3 py-2 text-gray-400">...</span></li>
          <li><a href="#" class="px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">12</a></li>
          <li>
            <a href="#" class="px-3 py-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800" aria-label="Next page">
              &raquo;
            </a>
          </li>
        </ul>
      </nav>
      ```
      
      ## Modals
      
      ### Centered Modal with Overlay
      
      ```html
      <div class="fixed inset-0 z-50 flex items-center justify-center" role="dialog" aria-modal="true" aria-labelledby="modal-title">
        <!-- Backdrop -->
        <div class="fixed inset-0 bg-black/50 transition-opacity" aria-hidden="true"></div>
        <!-- Panel -->
        <div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-lg mx-4 p-6">
          <div class="flex items-center justify-between mb-4">
            <h2 id="modal-title" class="text-lg font-semibold text-gray-900 dark:text-white">Edit Profile</h2>
            <button class="p-1 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
                    aria-label="Close modal">
              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
              </svg>
            </button>
          </div>
          <div class="mb-6">
            <p class="text-gray-600 dark:text-gray-400">Modal body content goes here.</p>
          </div>
          <div class="flex justify-end gap-3">
            <button class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">Cancel</button>
            <button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">Save Changes</button>
          </div>
        </div>
      </div>
      ```
      
      ### Slide-Over Panel
      
      ```html
      <div class="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true" aria-labelledby="slideover-title">
        <div class="fixed inset-0 bg-black/50" aria-hidden="true"></div>
        <div class="relative w-full max-w-md bg-white dark:bg-gray-800 shadow-xl flex flex-col h-full">
          <div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
            <h2 id="slideover-title" class="text-lg font-semibold text-gray-900 dark:text-white">Panel Title</h2>
            <button class="p-1 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" aria-label="Close panel">
              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
              </svg>
            </button>
          </div>
          <div class="flex-1 overflow-y-auto px-6 py-4">
            <p class="text-gray-600 dark:text-gray-400">Slide-over content with vertical scroll.</p>
          </div>
          <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
            <button class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">Cancel</button>
            <button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Apply</button>
          </div>
        </div>
      </div>
      ```
      
      ### Confirmation Dialog
      
      ```html
      <div class="fixed inset-0 z-50 flex items-center justify-center" role="alertdialog" aria-modal="true" aria-labelledby="confirm-title" aria-describedby="confirm-desc">
        <div class="fixed inset-0 bg-black/50" aria-hidden="true"></div>
        <div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-sm mx-4 p-6 text-center">
          <div class="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
            <svg class="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
            </svg>
          </div>
          <h2 id="confirm-title" class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Delete Item?</h2>
          <p id="confirm-desc" class="text-gray-600 dark:text-gray-400 mb-6">This action cannot be undone. The item will be permanently removed.</p>
          <div class="flex gap-3 justify-center">
            <button class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">Cancel</button>
            <button class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">Delete</button>
          </div>
        </div>
      </div>
      ```
      
      ## Tables
      
      ### Basic Table
      
      ```html
      <div class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
        <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
          <thead class="bg-gray-50 dark:bg-gray-800">
            <tr>
              <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</th>
              <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Email</th>
              <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role</th>
              <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
            </tr>
          </thead>
          <tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
            <tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
              <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">Jane Doe</td>
              <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">jane@example.com</td>
              <td class="px-6 py-4 whitespace-nowrap">
                <span class="px-2 py-1 text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300 rounded-full">Admin</span>
              </td>
              <td class="px-6 py-4 whitespace-nowrap text-right text-sm">
                <button class="text-blue-600 dark:text-blue-400 hover:underline">Edit</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      ```
      
      ### Striped Table
      
      ```html
      <table class="min-w-full">
        <thead>
          <tr class="border-b border-gray-200 dark:border-gray-700">
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Product</th>
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Price</th>
            <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Stock</th>
          </tr>
        </thead>
        <tbody>
          <tr class="odd:bg-white even:bg-gray-50 dark:odd:bg-gray-900 dark:even:bg-gray-800">
            <td class="px-6 py-4 text-sm text-gray-900 dark:text-white">Widget A</td>
            <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">$9.99</td>
            <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">142</td>
          </tr>
          <tr class="odd:bg-white even:bg-gray-50 dark:odd:bg-gray-900 dark:even:bg-gray-800">
            <td class="px-6 py-4 text-sm text-gray-900 dark:text-white">Widget B</td>
            <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">$19.99</td>
            <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">85</td>
          </tr>
        </tbody>
      </table>
      ```
      
      ### Sortable Table Header
      
      ```html
      <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
        <button class="group inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200">
          Name
          <svg class="w-4 h-4 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11l5-5m0 0l5 5m-5-5v12"/>
          </svg>
        </button>
      </th>
      ```
      
      ### Responsive Table (Stacked on Mobile)
      
      ```html
      <!-- Horizontal scroll approach -->
      <div class="overflow-x-auto -mx-4 sm:mx-0">
        <div class="inline-block min-w-full align-middle">
          <table class="min-w-full"><!-- table content --></table>
        </div>
      </div>
      
      <!-- Stacked approach (card-like on mobile) -->
      <div class="sm:hidden space-y-4">
        <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 space-y-2">
          <div class="flex justify-between">
            <span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Name</span>
            <span class="text-sm text-gray-900 dark:text-white font-medium">Jane Doe</span>
          </div>
          <div class="flex justify-between">
            <span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Email</span>
            <span class="text-sm text-gray-500 dark:text-gray-400">jane@example.com</span>
          </div>
          <div class="flex justify-between">
            <span class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Role</span>
            <span class="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">Admin</span>
          </div>
        </div>
      </div>
      <!-- Desktop table (hidden on mobile) -->
      <div class="hidden sm:block">
        <table class="min-w-full"><!-- full table --></table>
      </div>
      ```
      
      ## Alerts
      
      ### Info Alert
      
      ```html
      <div class="flex items-start gap-3 p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800" role="status">
        <svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
          <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
        </svg>
        <p class="text-sm text-blue-800 dark:text-blue-300">A new version is available. Please update to continue.</p>
      </div>
      ```
      
      ### Success Alert
      
      ```html
      <div class="flex items-start gap-3 p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800" role="status">
        <svg class="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
        </svg>
        <p class="text-sm text-green-800 dark:text-green-300">Changes saved successfully.</p>
      </div>
      ```
      
      ### Warning Alert
      
      ```html
      <div class="flex items-start gap-3 p-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800" role="alert">
        <svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
          <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
        </svg>
        <p class="text-sm text-yellow-800 dark:text-yellow-300">Your trial expires in 3 days. Upgrade to keep access.</p>
      </div>
      ```
      
      ### Error Alert
      
      ```html
      <div class="flex items-start gap-3 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800" role="alert">
        <svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
          <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
        </svg>
        <p class="text-sm text-red-800 dark:text-red-300">Failed to save changes. Please try again.</p>
      </div>
      ```
      
      ### Dismissible Alert
      
      ```html
      <div class="flex items-start gap-3 p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800" role="status">
        <svg class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
          <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
        </svg>
        <p class="flex-1 text-sm text-blue-800 dark:text-blue-300">Tip: You can drag items to reorder them.</p>
        <button class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 p-0.5" aria-label="Dismiss">
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
          </svg>
        </button>
      </div>
      ```
      
      ## Badges
      
      ### Inline Badge
      
      ```html
      <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-300">
        New
      </span>
      ```
      
      ### Pill Badge
      
      ```html
      <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300">
        Default
      </span>
      <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
        Active
      </span>
      <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-300">
        Removed
      </span>
      ```
      
      ### Badge with Dot Indicator
      
      ```html
      <span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-300">
        <span class="w-1.5 h-1.5 bg-green-500 rounded-full" aria-hidden="true"></span>
        Online
      </span>
      <span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-300">
        <span class="w-1.5 h-1.5 bg-yellow-500 rounded-full" aria-hidden="true"></span>
        Idle
      </span>
      <span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
        <span class="w-1.5 h-1.5 bg-gray-400 rounded-full" aria-hidden="true"></span>
        Offline
      </span>
      ```
      
      ### Notification Count Badge
      
      ```html
      <div class="relative inline-block">
        <button class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" aria-label="Notifications (3 unread)">
          <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
          </svg>
        </button>
        <span class="absolute -top-1 -right-1 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">
          3
        </span>
      </div>
      ```
      
      ## Avatars
      
      ### Image Avatar
      
      ```html
      <img src="user.jpg" alt="Jane Doe" class="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-800">
      ```
      
      ### Initials Avatar
      
      ```html
      <div class="w-10 h-10 rounded-full bg-blue-600 flex items-center justify-center">
        <span class="text-sm font-medium text-white">JD</span>
      </div>
      ```
      
      ### Avatar Group (Stacked)
      
      ```html
      <div class="flex -space-x-3">
        <img src="user1.jpg" alt="User 1" class="w-10 h-10 rounded-full border-2 border-white dark:border-gray-800 object-cover">
        <img src="user2.jpg" alt="User 2" class="w-10 h-10 rounded-full border-2 border-white dark:border-gray-800 object-cover">
        <img src="user3.jpg" alt="User 3" class="w-10 h-10 rounded-full border-2 border-white dark:border-gray-800 object-cover">
        <div class="w-10 h-10 rounded-full border-2 border-white dark:border-gray-800 bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
          <span class="text-xs font-medium text-gray-600 dark:text-gray-400">+5</span>
        </div>
      </div>
      ```
      
      ## Dropdowns
      
      ### Basic Dropdown
      
      ```html
      <div class="relative inline-block text-left">
        <button class="inline-flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
                       rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300
                       hover:bg-gray-50 dark:hover:bg-gray-700 focus-visible:outline-2 focus-visible:outline-blue-600"
                aria-expanded="true" aria-haspopup="true">
          Options
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
          </svg>
        </button>
        <div class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-10"
             role="menu">
          <a href="#" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Edit</a>
          <a href="#" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Duplicate</a>
          <div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
          <a href="#" class="block px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">Delete</a>
        </div>
      </div>
      ```
      
      ### Dropdown with Dividers and Icons
      
      ```html
      <div class="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-10" role="menu">
        <a href="#" class="flex items-center gap-3 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">
          <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
          </svg>
          Edit
        </a>
        <a href="#" class="flex items-center gap-3 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" role="menuitem">
          <svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
          </svg>
          Duplicate
        </a>
        <div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
        <a href="#" class="flex items-center gap-3 px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20" role="menuitem">
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
          </svg>
          Delete
        </a>
      </div>
      ```
      
      ## Tooltips
      
      ### CSS-Only Tooltip
      
      ```html
      <div class="relative group inline-block">
        <button class="px-3 py-1.5 text-sm bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg">
          Hover me
        </button>
        <div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5
                    bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg
                    opacity-0 group-hover:opacity-100 transition-opacity duration-200
                    pointer-events-none whitespace-nowrap"
             role="tooltip">
          Tooltip text here
          <div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
        </div>
      </div>
      ```
      
      ## Skeleton Loaders
      
      ### Text Skeleton
      
      ```html
      <div class="animate-pulse space-y-3">
        <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
        <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full"></div>
        <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
        <div class="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
      </div>
      ```
      
      ### Image Skeleton
      
      ```html
      <div class="animate-pulse">
        <div class="w-full h-48 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
          <svg class="w-10 h-10 text-gray-300 dark:text-gray-600" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
            <path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/>
          </svg>
        </div>
      </div>
      ```
      
      ### Card Skeleton
      
      ```html
      <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 animate-pulse">
        <div class="w-full h-40 bg-gray-200 dark:bg-gray-700 rounded-lg mb-4"></div>
        <div class="h-5 bg-gray-200 dark:bg-gray-700 rounded w-2/3 mb-3"></div>
        <div class="space-y-2">
          <div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-full"></div>
          <div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-4/5"></div>
        </div>
        <div class="flex items-center gap-3 mt-4">
          <div class="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded-full"></div>
          <div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>
        </div>
      </div>
      ```
      
      ## Accessibility Patterns
      
      ### Focus-Visible Rings
      
      ```html
      <!-- Keyboard-only focus ring (no ring on mouse click) -->
      <button class="px-4 py-2 bg-blue-600 text-white rounded-lg
                     focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
        Keyboard Focus Only
      </button>
      
      <!-- Custom focus ring for dark backgrounds -->
      <a href="#" class="text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white rounded">
        Link on dark background
      </a>
      ```
      
      ### Screen Reader Only Text
      
      ```html
      <!-- Visually hidden but read by screen readers -->
      <button aria-label="Close">
        <svg class="w-5 h-5" aria-hidden="true"><!-- icon --></svg>
        <span class="sr-only">Close dialog</span>
      </button>
      
      <!-- Skip to main content link -->
      <a href="#main-content"
         class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
                focus:z-50 focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white focus:rounded-lg">
        Skip to main content
      </a>
      ```
      
      ### ARIA Attributes with Tailwind
      
      ```html
      <!-- aria-expanded toggle indicator -->
      <button class="flex items-center gap-2" aria-expanded="false">
        Menu
        <svg class="w-4 h-4 transition-transform aria-expanded:rotate-180" aria-hidden="true"
             fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
        </svg>
      </button>
      
      <!-- aria-selected for tab panels -->
      <button role="tab" aria-selected="true"
        class="px-4 py-2 aria-selected:bg-blue-600 aria-selected:text-white rounded-lg">
        Selected Tab
      </button>
      
      <!-- aria-current for navigation -->
      <a href="#" aria-current="page"
         class="text-gray-600 aria-[current=page]:text-blue-600 aria-[current=page]:font-bold">
        Current Page
      </a>
      ```
      
      ### Reduced Motion
      
      ```html
      <!-- Disable animations for users who prefer reduced motion -->
      <div class="animate-bounce motion-reduce:animate-none">
        Bouncing content (static for reduced-motion users)
      </div>
      
      <!-- Only animate for users who haven't set a preference -->
      <div class="motion-safe:animate-pulse">
        Pulses only when safe
      </div>
      
      <!-- Disable transitions globally -->
      <div class="transition-transform hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100">
        Scales on hover (but not for reduced-motion users)
      </div>
      ```
      
    • configuration.md 20.8 KB
      # Tailwind CSS Configuration
      
      Complete configuration reference covering both Tailwind v3 (JS config) and v4 (CSS-first config).
      
      ## Tailwind v3 Configuration (tailwind.config.js)
      
      ### Minimal Config
      
      ```js
      // tailwind.config.js (v3)
      /** @type {import('tailwindcss').Config} */
      module.exports = {
        content: [
          './src/**/*.{html,js,jsx,ts,tsx,vue,astro}',
          './public/index.html',
        ],
        theme: {
          extend: {},
        },
        plugins: [],
      }
      ```
      
      ### theme.extend vs theme Override
      
      ```js
      module.exports = {
        theme: {
          // OVERRIDE: replaces ALL default colors (only these 3 exist)
          colors: {
            primary: '#3b82f6',
            secondary: '#6b7280',
            white: '#ffffff',
          },
      
          // EXTEND: adds to defaults (all defaults + these custom values)
          extend: {
            colors: {
              brand: '#3b82f6',        // Adds brand color, keeps slate/gray/red/etc.
              primary: {
                50: '#eff6ff',
                100: '#dbeafe',
                500: '#3b82f6',
                600: '#2563eb',
                700: '#1d4ed8',
                900: '#1e3a5f',
              },
            },
            spacing: {
              '18': '4.5rem',
              '88': '22rem',
              '128': '32rem',
            },
            borderRadius: {
              '4xl': '2rem',
            },
          },
        },
      }
      ```
      
      **Rule**: Almost always use `theme.extend`. Only use direct `theme` override when you want to eliminate defaults entirely.
      
      ### Screens (Breakpoints)
      
      ```js
      module.exports = {
        theme: {
          // Override ALL breakpoints
          screens: {
            'sm': '640px',
            'md': '768px',
            'lg': '1024px',
            'xl': '1280px',
            '2xl': '1536px',
          },
      
          // Or extend with additional breakpoints
          extend: {
            screens: {
              '3xl': '1920px',
              'tall': { 'raw': '(min-height: 800px)' },  // Height-based
            },
          },
        },
      }
      ```
      
      ## Tailwind v4 Configuration (@theme in CSS)
      
      ### Basic @theme Block
      
      ```css
      @import "tailwindcss";
      
      @theme {
        /* Colors: generate bg-*, text-*, border-*, ring-*, etc. */
        --color-brand: #3b82f6;
        --color-brand-50: #eff6ff;
        --color-brand-100: #dbeafe;
        --color-brand-500: #3b82f6;
        --color-brand-600: #2563eb;
        --color-brand-700: #1d4ed8;
        --color-brand-900: #1e3a5f;
      
        /* Semantic colors */
        --color-surface: #ffffff;
        --color-surface-dark: #1f2937;
        --color-on-surface: #111827;
        --color-on-surface-dark: #f9fafb;
      
        /* Fonts */
        --font-sans: "Inter", system-ui, sans-serif;
        --font-mono: "JetBrains Mono", ui-monospace, monospace;
      
        /* Spacing */
        --spacing-18: 4.5rem;
        --spacing-128: 32rem;
      
        /* Breakpoints */
        --breakpoint-3xl: 1920px;
      
        /* Border radius */
        --radius-4xl: 2rem;
      
        /* Animations */
        --animate-fade-in: fade-in 0.3s ease-out forwards;
      }
      ```
      
      ### Clearing Default Values
      
      ```css
      @theme {
        /* Clear all default colors, only keep what you define */
        --color-*: initial;
      
        --color-white: #ffffff;
        --color-black: #000000;
        --color-gray-50: #f9fafb;
        --color-gray-100: #f3f4f6;
        --color-gray-200: #e5e7eb;
        --color-gray-300: #d1d5db;
        --color-gray-400: #9ca3af;
        --color-gray-500: #6b7280;
        --color-gray-600: #4b5563;
        --color-gray-700: #374151;
        --color-gray-800: #1f2937;
        --color-gray-900: #111827;
        --color-blue-500: #3b82f6;
        --color-blue-600: #2563eb;
        --color-blue-700: #1d4ed8;
      }
      ```
      
      ## Custom Colors
      
      ### Color Palette Definition (v3)
      
      ```js
      // tailwind.config.js
      const colors = require('tailwindcss/colors')
      
      module.exports = {
        theme: {
          extend: {
            colors: {
              // Reference built-in palette
              gray: colors.slate,
              primary: colors.blue,
              success: colors.green,
              warning: colors.amber,
              danger: colors.red,
      
              // Custom palette with full scale
              brand: {
                50: '#faf5ff',
                100: '#f3e8ff',
                200: '#e9d5ff',
                300: '#d8b4fe',
                400: '#c084fc',
                500: '#a855f7',
                600: '#9333ea',
                700: '#7e22ce',
                800: '#6b21a8',
                900: '#581c87',
                950: '#3b0764',
              },
            },
          },
        },
      }
      ```
      
      ### Color Palette Definition (v4)
      
      ```css
      @theme {
        --color-brand-50: #faf5ff;
        --color-brand-100: #f3e8ff;
        --color-brand-200: #e9d5ff;
        --color-brand-300: #d8b4fe;
        --color-brand-400: #c084fc;
        --color-brand-500: #a855f7;
        --color-brand-600: #9333ea;
        --color-brand-700: #7e22ce;
        --color-brand-800: #6b21a8;
        --color-brand-900: #581c87;
        --color-brand-950: #3b0764;
      }
      ```
      
      ### Opacity Variants
      
      ```html
      <!-- Opacity modifier works with any color (v3.1+ and v4) -->
      <div class="bg-brand-500/75">75% opacity</div>
      <div class="bg-brand-500/50">50% opacity</div>
      <div class="bg-brand-500/25">25% opacity</div>
      <div class="text-brand-700/90">90% text opacity</div>
      <div class="border-brand-300/50">50% border opacity</div>
      ```
      
      ### Semantic Colors (Design Tokens)
      
      ```css
      /* v4: Define semantic tokens that reference palette values */
      @theme {
        --color-primary: var(--color-brand-600);
        --color-primary-hover: var(--color-brand-700);
        --color-secondary: var(--color-gray-600);
        --color-secondary-hover: var(--color-gray-700);
        --color-accent: var(--color-amber-500);
      
        --color-surface: var(--color-white);
        --color-surface-elevated: var(--color-gray-50);
        --color-on-surface: var(--color-gray-900);
        --color-on-surface-muted: var(--color-gray-500);
      
        --color-destructive: var(--color-red-600);
        --color-destructive-hover: var(--color-red-700);
      }
      ```
      
      ```html
      <!-- Usage with semantic tokens -->
      <button class="bg-primary text-white hover:bg-primary-hover">Primary Action</button>
      <button class="bg-destructive text-white hover:bg-destructive-hover">Delete</button>
      <div class="bg-surface text-on-surface">Card on surface</div>
      ```
      
      ## Custom Spacing
      
      ### Extending the Spacing Scale
      
      ```js
      // v3: tailwind.config.js
      module.exports = {
        theme: {
          extend: {
            spacing: {
              '13': '3.25rem',   // 52px
              '15': '3.75rem',   // 60px
              '18': '4.5rem',    // 72px
              '88': '22rem',     // 352px
              '128': '32rem',    // 512px
              'header': '64px',  // Named spacing
              'sidebar': '280px',
            },
          },
        },
      }
      ```
      
      ```css
      /* v4: @theme */
      @theme {
        --spacing-13: 3.25rem;
        --spacing-15: 3.75rem;
        --spacing-18: 4.5rem;
        --spacing-88: 22rem;
        --spacing-128: 32rem;
        --spacing-header: 64px;
        --spacing-sidebar: 280px;
      }
      ```
      
      ### Arbitrary Spacing Values
      
      ```html
      <!-- One-off values (use sparingly, prefer config for repeated values) -->
      <div class="p-[13px]">Arbitrary padding</div>
      <div class="mt-[clamp(1rem,3vw,2rem)]">Fluid margin</div>
      <div class="w-[calc(100%-250px)]">Calculated width</div>
      <div class="h-[calc(100vh-var(--spacing-header))]">Dynamic height</div>
      ```
      
      ## Typography
      
      ### Font Families
      
      ```js
      // v3: tailwind.config.js
      module.exports = {
        theme: {
          extend: {
            fontFamily: {
              sans: ['Inter', 'system-ui', 'sans-serif'],
              display: ['Poppins', 'system-ui', 'sans-serif'],
              body: ['Source Sans Pro', 'system-ui', 'sans-serif'],
              mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
            },
          },
        },
      }
      ```
      
      ```css
      /* v4: @theme */
      @theme {
        --font-sans: "Inter", system-ui, sans-serif;
        --font-display: "Poppins", system-ui, sans-serif;
        --font-body: "Source Sans Pro", system-ui, sans-serif;
        --font-mono: "JetBrains Mono", "Fira Code", monospace;
      }
      ```
      
      ```html
      <h1 class="font-display text-4xl font-bold">Display Heading</h1>
      <p class="font-body text-base">Body text with Source Sans Pro</p>
      <code class="font-mono text-sm">Code block</code>
      ```
      
      ### Font Sizes
      
      ```js
      // v3: Custom font sizes with line-height
      module.exports = {
        theme: {
          extend: {
            fontSize: {
              'xs': ['0.75rem', { lineHeight: '1rem' }],
              'tiny': ['0.625rem', { lineHeight: '0.875rem' }],
              'hero': ['4rem', { lineHeight: '1.1', letterSpacing: '-0.02em', fontWeight: '800' }],
            },
          },
        },
      }
      ```
      
      ```css
      /* v4: Font size with associated properties */
      @theme {
        --text-tiny: 0.625rem;
        --text-tiny--line-height: 0.875rem;
      
        --text-hero: 4rem;
        --text-hero--line-height: 1.1;
        --text-hero--letter-spacing: -0.02em;
        --text-hero--font-weight: 800;
      }
      ```
      
      ```html
      <h1 class="text-hero">Hero Heading</h1>
      <span class="text-tiny uppercase tracking-wider">Label</span>
      ```
      
      ### Line Heights
      
      ```html
      <!-- Relative line heights -->
      <p class="leading-none">1.0 line-height</p>
      <p class="leading-tight">1.25 line-height</p>
      <p class="leading-snug">1.375 line-height</p>
      <p class="leading-normal">1.5 line-height (default)</p>
      <p class="leading-relaxed">1.625 line-height</p>
      <p class="leading-loose">2.0 line-height</p>
      
      <!-- Fixed line heights -->
      <p class="leading-4">1rem (16px)</p>
      <p class="leading-6">1.5rem (24px)</p>
      <p class="leading-8">2rem (32px)</p>
      ```
      
      ### @tailwindcss/typography Plugin (Prose Classes)
      
      ```bash
      npm install @tailwindcss/typography
      ```
      
      ```js
      // v3
      module.exports = {
        plugins: [require('@tailwindcss/typography')],
      }
      ```
      
      ```css
      /* v4 */
      @plugin "@tailwindcss/typography";
      ```
      
      ```html
      <!-- Apply prose to markdown/CMS content wrappers -->
      <article class="prose dark:prose-invert lg:prose-lg max-w-none">
        <h1>Article Title</h1>
        <p>Rendered markdown with beautiful typography defaults.</p>
        <blockquote>Styled blockquotes.</blockquote>
        <pre><code>Styled code blocks</code></pre>
      
        <!-- Exclude sections from prose -->
        <div class="not-prose">
          <button class="bg-blue-600 text-white px-4 py-2 rounded-lg">
            Not affected by prose
          </button>
        </div>
      </article>
      ```
      
      **Prose modifiers**: `prose-sm`, `prose-base`, `prose-lg`, `prose-xl`, `prose-2xl`, `prose-invert` (dark mode), `prose-gray`, `prose-slate`, `prose-zinc`.
      
      ## Plugins
      
      ### Writing Plugins (v3)
      
      ```js
      const plugin = require('tailwindcss/plugin')
      
      module.exports = {
        plugins: [
          // addUtilities: generate utility classes
          plugin(function ({ addUtilities }) {
            addUtilities({
              '.text-balance': { 'text-wrap': 'balance' },
              '.text-pretty': { 'text-wrap': 'pretty' },
              '.content-auto': { 'content-visibility': 'auto' },
            })
          }),
      
          // addComponents: generate component classes
          plugin(function ({ addComponents, theme }) {
            addComponents({
              '.btn': {
                padding: `${theme('spacing.2')} ${theme('spacing.4')}`,
                borderRadius: theme('borderRadius.lg'),
                fontWeight: theme('fontWeight.medium'),
                fontSize: theme('fontSize.sm'),
                lineHeight: theme('lineHeight.5'),
              },
            })
          }),
      
          // matchUtilities: generate dynamic utilities with values
          plugin(function ({ matchUtilities, theme }) {
            matchUtilities(
              {
                'grid-area': (value) => ({ gridArea: value }),
              },
              { values: { header: 'header', main: 'main', sidebar: 'sidebar', footer: 'footer' } }
            )
          }),
        ],
      }
      ```
      
      ### Popular Plugins
      
      | Plugin | Purpose | Install |
      |--------|---------|---------|
      | `@tailwindcss/typography` | Prose classes for rich content | `npm i @tailwindcss/typography` |
      | `@tailwindcss/forms` | Better default form styles | `npm i @tailwindcss/forms` |
      | `@tailwindcss/container-queries` | Container queries (v3) | `npm i @tailwindcss/container-queries` |
      | `tailwindcss-animate` | Animation utilities (shadcn) | `npm i tailwindcss-animate` |
      | `@tailwindcss/aspect-ratio` | Aspect ratio (pre-native) | `npm i @tailwindcss/aspect-ratio` |
      
      ```js
      // v3: Using plugins
      module.exports = {
        plugins: [
          require('@tailwindcss/typography'),
          require('@tailwindcss/forms'),
          require('@tailwindcss/container-queries'),
          require('tailwindcss-animate'),
        ],
      }
      ```
      
      ```css
      /* v4: Using plugins */
      @plugin "@tailwindcss/typography";
      @plugin "@tailwindcss/forms";
      /* container-queries not needed in v4 (native) */
      @plugin "tailwindcss-animate";
      ```
      
      ## Content Configuration
      
      ### Template Paths (v3)
      
      ```js
      // v3: Tell Tailwind where to find class usage
      module.exports = {
        content: [
          './src/**/*.{html,js,jsx,ts,tsx,vue,svelte,astro}',
          './public/index.html',
          './content/**/*.md',
          // Include component libraries
          './node_modules/@acme/ui/dist/**/*.js',
        ],
      }
      ```
      
      ### Automatic Detection (v4)
      
      v4 automatically scans your project for template files. Override with `@source` if needed:
      
      ```css
      /* v4: Explicit source paths (usually not needed) */
      @source "../content/**/*.md";
      @source "../node_modules/@acme/ui/dist/**/*.js";
      ```
      
      ### Safelisting
      
      ```js
      // v3: Safelist classes that can't be detected
      module.exports = {
        safelist: [
          'bg-red-500',
          'bg-green-500',
          'bg-blue-500',
          // Pattern-based
          { pattern: /bg-(red|green|blue)-(100|500|900)/ },
          // With variants
          { pattern: /text-(red|green|blue)-500/, variants: ['hover', 'dark'] },
        ],
      }
      ```
      
      ```css
      /* v4: Safelist via CSS comment */
      @source "safelist:bg-red-500,bg-green-500,bg-blue-500";
      ```
      
      ## @layer Directive
      
      ### Layer Hierarchy
      
      ```css
      @import "tailwindcss";
      
      /* BASE: Reset, HTML element defaults, @font-face */
      @layer base {
        html {
          scroll-behavior: smooth;
          -webkit-font-smoothing: antialiased;
        }
      
        body {
          font-family: var(--font-body);
          color: var(--color-on-surface);
          background-color: var(--color-surface);
        }
      
        h1, h2, h3, h4, h5, h6 {
          font-family: var(--font-display);
          font-weight: 700;
        }
      
        a {
          color: var(--color-primary);
          text-decoration-line: underline;
          text-underline-offset: 2px;
        }
      }
      
      /* COMPONENTS: Reusable multi-property classes */
      @layer components {
        .card {
          @apply rounded-lg bg-white p-6 shadow-md dark:bg-gray-800;
        }
      
        .btn {
          @apply inline-flex items-center justify-center rounded-lg px-4 py-2
                 text-sm font-medium transition-colors
                 focus-visible:outline-2 focus-visible:outline-offset-2;
        }
      
        .btn-primary {
          @apply btn bg-blue-600 text-white hover:bg-blue-700
                 focus-visible:outline-blue-600;
        }
      
        .input {
          @apply w-full rounded-lg border border-gray-300 bg-white px-3 py-2
                 text-gray-900 placeholder:text-gray-400
                 focus:border-transparent focus:ring-2 focus:ring-blue-500
                 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100;
        }
      }
      
      /* UTILITIES: Single-property overrides */
      @layer utilities {
        .text-balance {
          text-wrap: balance;
        }
      
        .content-auto {
          content-visibility: auto;
        }
      
        .scrollbar-hidden {
          scrollbar-width: none;
          &::-webkit-scrollbar { display: none; }
        }
      }
      ```
      
      ### When to Use Each Layer
      
      | Layer | Use For | Example |
      |-------|---------|---------|
      | `base` | HTML element defaults, resets, `@font-face` | Body font, link colors, heading styles |
      | `components` | Multi-property reusable patterns | `.card`, `.btn`, `.input`, `.badge` |
      | `utilities` | Single-purpose utility classes | `.text-balance`, `.content-auto` |
      
      ## @apply
      
      ### When @apply is Appropriate
      
      ```css
      /* GOOD: Component libraries where utility classes aren't available */
      @layer components {
        .prose-custom h2 {
          @apply text-2xl font-bold text-gray-900 dark:text-white mt-8 mb-4;
        }
      }
      
      /* GOOD: Markdown content styling */
      @layer base {
        .markdown-content h1 { @apply text-3xl font-bold mb-4; }
        .markdown-content p { @apply text-gray-600 dark:text-gray-400 mb-4; }
        .markdown-content a { @apply text-blue-600 hover:underline; }
      }
      
      /* GOOD: Repeated pattern across many elements */
      @layer components {
        .btn {
          @apply px-4 py-2 rounded-lg font-medium transition-colors
                 focus-visible:outline-2 focus-visible:outline-offset-2;
        }
      }
      ```
      
      ### When NOT to Use @apply
      
      ```html
      <!-- BAD: Using @apply when inline utilities work fine -->
      <!-- .my-div { @apply flex items-center gap-4 p-6; } -->
      
      <!-- GOOD: Inline utilities - easier to read, change, and delete -->
      <div class="flex items-center gap-4 p-6">
      ```
      
      **Rule of thumb**: Use inline utilities by default. Only reach for `@apply` when:
      1. You need to style elements you don't control (CMS content, markdown)
      2. You're building a component library with `.btn`, `.card` classes
      3. A combination of 5+ utilities is repeated identically in 5+ places
      
      ## Custom Variants
      
      ### addVariant (v3)
      
      ```js
      const plugin = require('tailwindcss/plugin')
      
      module.exports = {
        plugins: [
          plugin(function ({ addVariant }) {
            // Simple variant
            addVariant('hocus', ['&:hover', '&:focus'])
            addVariant('supports-grid', '@supports (display: grid)')
            addVariant('optional', '&:optional')
      
            // Parent state
            addVariant('group-sidebar', ':merge(.group-sidebar):hover &')
          }),
        ],
      }
      ```
      
      ### @custom-variant (v4)
      
      ```css
      /* v4: Define custom variants in CSS */
      @custom-variant hocus (&:hover, &:focus);
      @custom-variant optional (&:optional);
      @custom-variant supports-grid (@supports (display: grid));
      
      /* Dark mode with custom selector */
      @custom-variant dark (&:where(.dark, .dark *));
      
      /* Dark mode with data attribute */
      @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
      ```
      
      ```html
      <!-- Usage -->
      <button class="bg-blue-600 hocus:bg-blue-700">Hover or focus</button>
      <input class="border-gray-300 optional:border-dashed" type="text">
      ```
      
      ### Data Attribute Variants
      
      ```html
      <!-- Built-in data-* variant -->
      <div data-state="open" class="data-[state=open]:bg-blue-50 data-[state=closed]:bg-gray-50">
        Responds to data-state attribute
      </div>
      
      <div data-size="lg" class="data-[size=sm]:text-sm data-[size=lg]:text-lg">
        Responds to data-size
      </div>
      
      <!-- With boolean data attributes -->
      <div data-loading class="data-[loading]:animate-pulse">
        Loading state
      </div>
      ```
      
      ## Prefix Configuration
      
      ### v3: Avoiding Conflicts
      
      ```js
      // v3: Add prefix to all Tailwind classes
      module.exports = {
        prefix: 'tw-',
      }
      ```
      
      ```html
      <!-- All classes get tw- prefix -->
      <div class="tw-flex tw-items-center tw-gap-4 tw-bg-blue-500">
      ```
      
      ### v4: Prefix
      
      ```css
      /* v4: Prefix via @import option */
      @import "tailwindcss" prefix(tw);
      ```
      
      ## Important Configuration
      
      ### v3: Important Selector Strategy
      
      ```js
      // v3: Make all utilities important
      module.exports = {
        // Option 1: All utilities get !important
        important: true,
      
        // Option 2: Selector strategy (recommended)
        important: '#app',
      }
      ```
      
      ### Per-Utility Important
      
      ```html
      <!-- Add ! prefix for individual important override -->
      <div class="!text-red-500">This text is red regardless of other styles</div>
      <div class="bg-blue-500 !bg-red-500">Background is red</div>
      ```
      
      ## Dark Mode Configuration
      
      ### Class Strategy (v3)
      
      ```js
      // v3: tailwind.config.js
      module.exports = {
        darkMode: 'class',  // Toggle via class="dark" on <html>
      }
      ```
      
      ```html
      <html class="dark">
        <body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
      ```
      
      ### Media Strategy (v3 and v4 Default)
      
      ```js
      // v3: Uses system preference (prefers-color-scheme)
      module.exports = {
        darkMode: 'media',  // Default in v4
      }
      ```
      
      ### Selector Strategy (v4)
      
      ```css
      /* v4: Class-based dark mode */
      @custom-variant dark (&:where(.dark, .dark *));
      
      /* v4: Data attribute dark mode */
      @custom-variant dark (&:where([data-mode="dark"], [data-mode="dark"] *));
      ```
      
      ### Dark Mode Toggle Script
      
      ```html
      <!-- Prevent flash of wrong theme: add to <head> before CSS -->
      <script>
        const theme = localStorage.getItem('theme');
        if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
          document.documentElement.classList.add('dark');
        }
      </script>
      ```
      
      ## Container Queries
      
      ### Setup (v3)
      
      ```bash
      npm install @tailwindcss/container-queries
      ```
      
      ```js
      // v3: tailwind.config.js
      module.exports = {
        plugins: [require('@tailwindcss/container-queries')],
      }
      ```
      
      ### Setup (v4)
      
      Container queries are native in v4. No plugin needed.
      
      ### Usage
      
      ```html
      <!-- Unnamed container -->
      <div class="@container">
        <div class="flex flex-col @sm:flex-row @md:grid @md:grid-cols-2 gap-4">
          <div class="p-4">Responds to parent width</div>
          <div class="p-4">Not viewport width</div>
        </div>
      </div>
      
      <!-- Named container -->
      <div class="@container/sidebar">
        <nav class="flex flex-col @md/sidebar:flex-row gap-2">
          <a href="#">Link 1</a>
          <a href="#">Link 2</a>
        </nav>
      </div>
      ```
      
      ### Container Query Breakpoints
      
      | Variant | Min Width |
      |---------|-----------|
      | `@xs:` | 320px (20rem) |
      | `@sm:` | 384px (24rem) |
      | `@md:` | 448px (28rem) |
      | `@lg:` | 512px (32rem) |
      | `@xl:` | 576px (36rem) |
      | `@2xl:` | 672px (42rem) |
      | `@3xl:` | 768px (48rem) |
      | `@4xl:` | 896px (56rem) |
      | `@5xl:` | 1024px (64rem) |
      
      ### Container Query Units
      
      ```html
      <!-- Container query units: relative to container size -->
      <div class="@container">
        <div class="w-[50cqw] h-[30cqh]">
          50% of container width, 30% of container height
        </div>
      </div>
      ```
      
      | Unit | Description |
      |------|-------------|
      | `cqw` | 1% of container width |
      | `cqh` | 1% of container height |
      | `cqi` | 1% of container inline size |
      | `cqb` | 1% of container block size |
      | `cqmin` | Smaller of `cqi` / `cqb` |
      | `cqmax` | Larger of `cqi` / `cqb` |
      
      ### Nested Containers
      
      ```html
      <div class="@container/page">
        <div class="grid @lg/page:grid-cols-[250px_1fr]">
          <aside class="@container/sidebar">
            <nav class="flex flex-col @md/sidebar:flex-row">Sidebar nav</nav>
          </aside>
          <main class="@container/content">
            <div class="grid @md/content:grid-cols-2 @xl/content:grid-cols-3 gap-6">
              <div>Card</div>
            </div>
          </main>
        </div>
      </div>
      ```
      
    • v4-migration.md 13.3 KB
      # Tailwind CSS v4 Migration Guide
      
      Comprehensive guide for migrating from Tailwind CSS v3 to v4. Tailwind v4 is a ground-up rewrite with CSS-first configuration, new engine, and native support for modern CSS features.
      
      ## What Changed: Overview
      
      ### Architecture Shift
      
      Tailwind v4 replaces JavaScript-based configuration with CSS-first configuration. The `tailwind.config.js` file is no longer required (but supported via compatibility layer).
      
      | Concept | v3 | v4 |
      |---------|----|----|
      | Config | `tailwind.config.js` (JS) | `@theme {}` block in CSS |
      | Entry point | `@tailwind base/components/utilities` | `@import "tailwindcss"` |
      | Plugins | JS `plugin()` API | `@plugin "package"` in CSS |
      | PostCSS | `tailwindcss` package | `@tailwindcss/postcss` |
      | Vite | PostCSS plugin | `@tailwindcss/vite` (faster) |
      | Content detection | `content: [...]` in config | Automatic (scans project) |
      | Theme values | JS objects | CSS custom properties |
      | Directives | `@tailwind`, `@screen`, `@variants` | `@import`, `@theme`, `@variant` |
      
      ## @theme Directive
      
      The `@theme` directive replaces `theme.extend` in `tailwind.config.js`. Values defined here become both CSS custom properties and Tailwind utilities.
      
      ### Basic Usage
      
      ```css
      /* v4: CSS-first configuration */
      @import "tailwindcss";
      
      @theme {
        /* Colors: creates bg-brand, text-brand, border-brand, etc. */
        --color-brand: #3b82f6;
        --color-brand-light: #60a5fa;
        --color-brand-dark: #1d4ed8;
      
        /* Semantic colors */
        --color-success: #22c55e;
        --color-warning: #f59e0b;
        --color-danger: #ef4444;
      
        /* Typography */
        --font-display: "Inter", "system-ui", sans-serif;
        --font-body: "Source Sans Pro", "system-ui", sans-serif;
        --font-mono: "JetBrains Mono", "Fira Code", monospace;
      
        /* Custom spacing */
        --spacing-18: 4.5rem;
        --spacing-88: 22rem;
        --spacing-128: 32rem;
      
        /* Custom breakpoints */
        --breakpoint-3xl: 1920px;
      
        /* Animations */
        --animate-fade-in: fade-in 0.3s ease-out;
        --animate-slide-up: slide-up 0.4s ease-out;
      }
      
      @keyframes fade-in {
        from { opacity: 0; transform: translateY(8px); }
        to { opacity: 1; transform: translateY(0); }
      }
      
      @keyframes slide-up {
        from { opacity: 0; transform: translateY(100%); }
        to { opacity: 1; transform: translateY(0); }
      }
      ```
      
      ### Overriding vs Extending
      
      ```css
      /* EXTENDING: add to existing scale (use --color-* namespace) */
      @theme {
        --color-brand: #3b82f6;
        /* All default colors (slate, gray, red, etc.) still available */
      }
      
      /* OVERRIDING: replace entire namespace */
      @theme {
        --color-*: initial;  /* Clear all default colors */
        --color-primary: #3b82f6;
        --color-secondary: #6b7280;
        --color-accent: #f59e0b;
        /* Only these 3 colors available now */
      }
      ```
      
      ### Accessing Theme Values in CSS
      
      ```css
      /* Theme values are CSS custom properties, usable anywhere */
      .custom-element {
        color: var(--color-brand);
        font-family: var(--font-display);
        padding: var(--spacing-18);
      }
      ```
      
      ## @config: Compatibility Layer
      
      For projects with existing `tailwind.config.js` files, v4 provides a compatibility layer.
      
      ```css
      @import "tailwindcss";
      
      /* Load existing JS config */
      @config "./tailwind.config.js";
      
      /* Can combine with @theme (theme overrides config) */
      @theme {
        --color-brand: #3b82f6;
      }
      ```
      
      **Migration path**: Start with `@config`, then gradually move values into `@theme`, then remove the JS config.
      
      ## @plugin Directive
      
      Plugins are now imported directly in CSS.
      
      ```css
      @import "tailwindcss";
      
      /* v4: Import plugins in CSS */
      @plugin "@tailwindcss/typography";
      @plugin "@tailwindcss/forms";
      @plugin "@tailwindcss/container-queries";
      
      /* Local plugin */
      @plugin "./plugins/my-custom-plugin.js";
      ```
      
      ### Plugin API Changes
      
      ```js
      // v4 plugin API (similar to v3 but with changes)
      export default function ({ addUtilities, addComponents, matchUtilities, theme }) {
        addUtilities({
          '.content-auto': { 'content-visibility': 'auto' },
          '.content-hidden': { 'content-visibility': 'hidden' },
        })
      }
      ```
      
      ## New Default Scales
      
      ### Spacing
      
      v4 uses a simplified spacing scale based on multiples of `0.25rem` (4px). The existing numeric scale (0-96) remains, but new scales are rationalized.
      
      ### Colors
      
      v4 retains the same color palette names but adds:
      - OKLCH color support for more perceptually uniform colors
      - Automatic color opacity via `bg-blue-500/75` (unchanged syntax, improved output)
      
      ### Typography
      
      ```css
      @theme {
        /* v4 uses --text-* for font-size + line-height combos */
        --text-base: 1rem;           /* 16px */
        --text-base--line-height: 1.5rem;  /* 24px */
      
        /* Custom text scale */
        --text-hero: 4rem;
        --text-hero--line-height: 1.1;
        --text-hero--letter-spacing: -0.02em;
        --text-hero--font-weight: 800;
      }
      ```
      
      ## Removed Utilities and Replacements
      
      | v3 Utility | Status in v4 | Replacement |
      |------------|-------------|-------------|
      | `bg-opacity-*` | Removed | `bg-blue-500/75` (opacity modifier) |
      | `text-opacity-*` | Removed | `text-blue-500/75` |
      | `border-opacity-*` | Removed | `border-blue-500/75` |
      | `divide-opacity-*` | Removed | `divide-blue-500/75` |
      | `ring-opacity-*` | Removed | `ring-blue-500/75` |
      | `placeholder-opacity-*` | Removed | `placeholder:text-gray-400/75` |
      | `flex-shrink` | Renamed | `shrink` (already available in v3) |
      | `flex-grow` | Renamed | `grow` (already available in v3) |
      | `overflow-ellipsis` | Renamed | `text-ellipsis` |
      | `decoration-slice` | Renamed | `box-decoration-slice` |
      | `decoration-clone` | Renamed | `box-decoration-clone` |
      
      ### Opacity Modifier Migration
      
      ```html
      <!-- v3: Separate opacity utilities -->
      <div class="bg-blue-500 bg-opacity-75">
      
      <!-- v4: Opacity modifier on the color -->
      <div class="bg-blue-500/75">
      
      <!-- Both opacity and color in one -->
      <div class="bg-blue-500/50 text-white/90 border-gray-300/30">
      ```
      
      ## Variant Changes
      
      ### New Variants
      
      | Variant | Usage | Description |
      |---------|-------|-------------|
      | `@sm:` / `@md:` / `@lg:` | `@md:flex` | Container query breakpoints |
      | `@min-*:` / `@max-*:` | `@min-[400px]:flex` | Arbitrary container queries |
      | `starting:` | `starting:opacity-0` | `@starting-style` for entry animations |
      | `not-*:` | `not-last:mb-4` | Negation pseudo-class |
      | `in-*:` | `in-[.dark]:text-white` | Match within ancestor |
      | `has-*:` | `has-[input:focus]:ring-2` | `:has()` pseudo-class |
      | `nth-*:` | `nth-3:bg-gray-100` | `:nth-child()` |
      | `nth-last-*:` | `nth-last-2:mb-0` | `:nth-last-child()` |
      
      ### Removed / Changed Variants
      
      | v3 | v4 | Notes |
      |----|----| ------|
      | `@screen sm` | `@sm` or `@media (width >= 640px)` | `@screen` directive removed |
      | `@variants hover, focus` | Removed | Not needed, variants auto-generated |
      
      ### Container Query Variants
      
      ```html
      <!-- v4: Native container queries -->
      <div class="@container">
        <div class="flex flex-col @sm:flex-row @md:grid @md:grid-cols-2 @lg:grid-cols-3 gap-4">
          <div>Card</div>
        </div>
      </div>
      
      <!-- Named containers -->
      <div class="@container/main">
        <div class="@sm/main:flex">Responds to main container</div>
      </div>
      
      <!-- Arbitrary container query values -->
      <div class="@container">
        <div class="@min-[400px]:flex @max-[800px]:flex-col">
          Custom breakpoints
        </div>
      </div>
      ```
      
      ## @starting-style: Entry Animations
      
      v4 supports `@starting-style` for CSS-native entry animations, eliminating the need for JavaScript animation libraries in many cases.
      
      ```css
      /* Dialog with entry animation */
      dialog[open] {
        opacity: 1;
        transform: scale(1);
        transition: opacity 0.3s ease, transform 0.3s ease,
                    display 0.3s ease allow-discrete,
                    overlay 0.3s ease allow-discrete;
      
        @starting-style {
          opacity: 0;
          transform: scale(0.95);
        }
      }
      
      /* Popover entry */
      [popover]:popover-open {
        opacity: 1;
        transform: translateY(0);
        transition: opacity 0.2s, transform 0.2s,
                    display 0.2s allow-discrete,
                    overlay 0.2s allow-discrete;
      
        @starting-style {
          opacity: 0;
          transform: translateY(-8px);
        }
      }
      ```
      
      ### Using with Tailwind v4 Classes
      
      ```html
      <!-- The starting: variant maps to @starting-style -->
      <dialog class="opacity-100 scale-100 transition-all duration-300
                     starting:opacity-0 starting:scale-95
                     backdrop:bg-black/50">
        <div class="p-6">Dialog content</div>
      </dialog>
      ```
      
      ## Anchor Positioning
      
      v4 supports CSS Anchor Positioning for tooltips, popovers, and dropdowns without JavaScript positioning libraries.
      
      ```css
      /* Anchor a tooltip to a button */
      .trigger {
        anchor-name: --my-trigger;
      }
      
      .tooltip {
        position: absolute;
        position-anchor: --my-trigger;
        top: anchor(bottom);
        left: anchor(center);
        transform: translateX(-50%);
      }
      ```
      
      ## PostCSS Changes
      
      ### v3 PostCSS Setup
      
      ```js
      // postcss.config.js (v3)
      module.exports = {
        plugins: {
          tailwindcss: {},
          autoprefixer: {},
        },
      }
      ```
      
      ### v4 PostCSS Setup
      
      ```js
      // postcss.config.js (v4)
      module.exports = {
        plugins: {
          '@tailwindcss/postcss': {},
          // autoprefixer no longer needed - handled by Tailwind v4
        },
      }
      ```
      
      ### Package Changes
      
      ```bash
      # Remove v3 packages
      npm uninstall tailwindcss postcss autoprefixer
      
      # Install v4 packages
      npm install @tailwindcss/postcss
      ```
      
      ## Vite Plugin
      
      v4 provides a dedicated Vite plugin that is significantly faster than the PostCSS plugin.
      
      ```bash
      npm install @tailwindcss/vite
      ```
      
      ```js
      // vite.config.js
      import tailwindcss from '@tailwindcss/vite'
      
      export default {
        plugins: [
          tailwindcss(),
        ],
      }
      ```
      
      ```css
      /* app.css - no postcss config needed */
      @import "tailwindcss";
      
      @theme {
        --color-brand: #3b82f6;
      }
      ```
      
      ## CSS Entry Point Changes
      
      ### v3 Entry Point
      
      ```css
      /* v3 */
      @tailwind base;
      @tailwind components;
      @tailwind utilities;
      
      /* Custom styles */
      @layer base { ... }
      @layer components { ... }
      @layer utilities { ... }
      ```
      
      ### v4 Entry Point
      
      ```css
      /* v4 */
      @import "tailwindcss";
      
      /* Layers still work the same way */
      @layer base {
        body {
          font-family: var(--font-body);
        }
      }
      
      @layer components {
        .card {
          @apply rounded-lg bg-white p-6 shadow-md dark:bg-gray-800;
        }
      }
      
      @layer utilities {
        .content-auto {
          content-visibility: auto;
        }
      }
      ```
      
      ## Migration Steps
      
      ### 1. Automated Migration Tool
      
      ```bash
      # Run the official migration tool
      npx @tailwindcss/upgrade
      ```
      
      This tool will:
      - Update `package.json` dependencies
      - Convert `tailwind.config.js` to `@theme` block (where possible)
      - Update CSS entry points (`@tailwind` to `@import`)
      - Replace removed utilities with modern equivalents
      - Update PostCSS config
      
      ### 2. Manual Migration Checklist
      
      ```
      [ ] Update packages: tailwindcss -> @tailwindcss/postcss (or @tailwindcss/vite)
      [ ] Remove autoprefixer (built into v4)
      [ ] Replace @tailwind directives with @import "tailwindcss"
      [ ] Move tailwind.config.js theme values to @theme block
          OR use @config "./tailwind.config.js" as compatibility layer
      [ ] Update opacity utilities: bg-opacity-50 -> bg-blue-500/50
      [ ] Update deprecated utility names (flex-shrink -> shrink, etc.)
      [ ] Replace @screen with @media or container queries
      [ ] Update plugins: require() -> @plugin directive
      [ ] Test dark mode (default is now media-based)
      [ ] Verify content detection (automatic in v4, no content config needed)
      [ ] Remove postcss.config.js if using @tailwindcss/vite
      ```
      
      ### 3. Testing Checklist
      
      ```
      [ ] All pages render correctly
      [ ] Dark mode toggle works
      [ ] Responsive breakpoints behave correctly
      [ ] Custom colors / spacing / typography render
      [ ] Animations and transitions work
      [ ] Form styles render (if using @tailwindcss/forms)
      [ ] Prose/typography content renders (if using @tailwindcss/typography)
      [ ] No missing classes in production build
      [ ] No console errors related to CSS
      [ ] Accessibility: focus rings, sr-only text still work
      ```
      
      ## Breaking Changes by Category
      
      ### Configuration
      
      | Change | Impact | Migration |
      |--------|--------|-----------|
      | `tailwind.config.js` no longer auto-detected | Config not loaded | Add `@config "./tailwind.config.js"` or migrate to `@theme` |
      | `content` paths removed | Not needed | v4 automatically detects template files |
      | `safelist` moved | Classes not preserved | Use `@source` directive or CSS comments |
      | `prefix` option | Not directly supported | Use CSS layers or namespacing |
      | `important` option | Changed behavior | Use `@layer` strategy instead |
      
      ### Utilities
      
      | Change | Impact | Migration |
      |--------|--------|-----------|
      | `bg-opacity-*` removed | Transparent backgrounds break | Use `/` opacity modifier: `bg-blue-500/75` |
      | `flex-shrink-*` renamed | Warning/removal | Use `shrink-*` |
      | `flex-grow-*` renamed | Warning/removal | Use `grow-*` |
      | Default border color | Was `gray-200`, now `currentColor` | Explicitly set `border-gray-200` |
      | Default ring width | Was `3px`, now `1px` | Explicitly set `ring-3` |
      | Shadow color handling | Different cascade behavior | Test shadow utilities |
      
      ### Variants
      
      | Change | Impact | Migration |
      |--------|--------|-----------|
      | `@screen` removed | Compilation error | Use `@media (width >= Xpx)` |
      | `@variants` removed | Compilation error | Remove directive (auto-generated) |
      | `@responsive` removed | Compilation error | Remove directive (auto-generated) |
      | Dark mode default | Was opt-in, now `media` by default | Set `@custom-variant dark` if using class strategy |
      
      ### Plugins
      
      | Change | Impact | Migration |
      |--------|--------|-----------|
      | `require()` not supported in CSS | Plugin not loaded | Use `@plugin "package"` |
      | Some plugin APIs changed | Plugin errors | Check plugin compatibility with v4 |
      | `addBase()` behavior | Different layer ordering | Test base styles |
      | `theme()` function | Returns CSS custom properties | Update if doing string comparison |
      
  • scripts
    • .gitkeep 0 B · in bundle
    • check-tailwind-facts.py 11.1 KB
      #!/usr/bin/env python3
      """Staleness verifier for tailwind-ops: the Tailwind v4 facts the skill encodes
      must stay real and named in the prose.
      
      tailwind-ops centers on Tailwind CSS v4 (CSS-first config: @theme, @plugin,
      @config, @import "tailwindcss"; @tailwindcss/postcss + @tailwindcss/vite) and
      the v3->v4 migration. That is exactly the fact that drifts silently
      (SKILL-RESOURCE-PROTOCOL.md §7): Tailwind ships a new major, a v4 directive is
      renamed, or the prose stops mentioning a package the catalog lists, and nobody
      notices for months. Two modes guard it:
      
        --offline (default, safe for PR CI): structural consistency, no network.
          * assets/tailwind-facts.json parses and every package + v4 directive gate is
            named somewhere in the skill prose (SKILL.md / references/*.md) — the
            catalog can't drift from the docs
          * SKILL.md still carries a dated "as of <year>" currency note
        --live (scheduled freshness.yml, never a PR gate): query the npm registry for
          each package's latest dist-tag; flag DRIFT when the live major is newer than
          the documented major (the skill is now behind — e.g. tailwindcss 5 while the
          prose still says v4), or when a package is gone (404). Transient registry
          failure is UNAVAILABLE (exit 7), never a failure.
      
      Usage:   check-tailwind-facts.py [--offline | --live] [--facts FILE] [--skill DIR] [--json] [--timeout S]
      Input:   argv flags only (no stdin).
      Output:  stdout = findings (plain rows, or a --json envelope). Data only.
      Stderr:  the verdict line, notices, errors.
      Exit:    0 ok, 2 usage, 3 facts/skill missing, 4 facts unparseable,
               7 npm registry unreachable (live, advisory — never a real failure),
               10 drift (offline: uncited/undocumented/missing note; live: major ahead or gone)
      
      Examples:
        check-tailwind-facts.py --offline                 # PR CI: catalog ⇆ prose consistency
        check-tailwind-facts.py --live                    # weekly: is any documented major behind npm?
        check-tailwind-facts.py --offline --json | jq '.data[]'
      """
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import re
      import sys
      import urllib.error
      import urllib.parse
      import urllib.request
      from pathlib import Path
      
      EX_OK = 0
      EX_USAGE = 2
      EX_NOTFOUND = 3
      EX_UNPARSEABLE = 4
      EX_UNAVAILABLE = 7
      EX_DRIFT = 10
      
      SCHEMA = "claude-mods.tailwind-ops.facts/v1"
      HERE = Path(__file__).resolve().parent
      DEFAULT_FACTS = HERE.parent / "assets" / "tailwind-facts.json"
      DEFAULT_SKILL = HERE.parent
      REGISTRY = "https://registry.npmjs.org"
      CURRENCY_RE = re.compile(r"as of 20\d\d")
      
      
      class Term:
          """Minimal ANSI helper. Honors FORCE_COLOR / NO_COLOR / TERM_ASCII and the
          bound stream's TTY + encoding so piped data stays plain ASCII."""
      
          _C = {"green": "\033[32m", "red": "\033[31m", "dim": "\033[2m", "off": "\033[0m"}
      
          def __init__(self, stream=sys.stderr):
              enc = (getattr(stream, "encoding", "") or "").lower()
              self.ascii = os.environ.get("TERM_ASCII") == "1" or "utf" not in enc
              if os.environ.get("FORCE_COLOR"):
                  self.color = True
              elif (os.environ.get("NO_COLOR") is not None
                    or os.environ.get("TERM") == "dumb"
                    or not getattr(stream, "isatty", lambda: False)()):
                  self.color = False
              else:
                  self.color = True
      
          def c(self, name, text):
              return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text
      
          def mark(self, ok):
              g = ("+" if self.ascii else "✓") if ok else ("x" if self.ascii else "✗")
              return self.c("green" if ok else "red", g)
      
      
      def load_facts(path: Path) -> dict:
          if not path.is_file():
              print(f"error: facts catalog not found: {path}", file=sys.stderr)
              raise SystemExit(EX_NOTFOUND)
          try:
              data = json.loads(path.read_text(encoding="utf-8"))
              if data.get("schema") != SCHEMA:
                  raise ValueError(f"schema {data.get('schema')!r} != {SCHEMA!r}")
              if not isinstance(data.get("packages"), dict) or not data["packages"]:
                  raise ValueError("'packages' must be a non-empty object")
              for name, info in data["packages"].items():
                  if not isinstance(info, dict) or "documented_major" not in info:
                      raise ValueError(f"package {name!r} missing documented_major")
                  if not isinstance(info.get("prose"), list) or not info["prose"]:
                      raise ValueError(f"package {name!r} missing prose tokens")
              return data
          except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
              print(f"error: could not parse facts {path}: {exc}", file=sys.stderr)
              raise SystemExit(EX_UNPARSEABLE)
      
      
      def read_corpus(skill_dir: Path) -> tuple[str, str]:
          """Returns (skill_md_text, all_prose_text) across SKILL.md + references/*.md."""
          doc = skill_dir / "SKILL.md"
          if not doc.is_file():
              print(f"error: SKILL.md not found under {skill_dir}", file=sys.stderr)
              raise SystemExit(EX_NOTFOUND)
          skill_md = doc.read_text(encoding="utf-8", errors="replace")
          parts = [skill_md]
          for ref in sorted((skill_dir / "references").glob("*.md")):
              parts.append(ref.read_text(encoding="utf-8", errors="replace"))
          return skill_md, "\n".join(parts)
      
      
      def check_offline(facts: dict, skill_dir: Path) -> list[dict]:
          skill_md, corpus = read_corpus(skill_dir)
          findings: list[dict] = []
          for name, info in facts["packages"].items():
              for token in info["prose"]:
                  if token not in corpus:
                      findings.append({"package": name, "issue": f"prose token {token!r} not named in skill"})
          for key, token in facts.get("version_gates", {}).items():
              if key == "_comment":
                  continue
              if str(token) not in corpus:
                  findings.append({"package": "(gate)", "issue": f"version gate {key}={token!r} not stated in skill prose"})
          if not CURRENCY_RE.search(skill_md):
              findings.append({"package": "(SKILL.md)", "issue": "no dated 'as of <year>' currency note"})
          return findings
      
      
      def npm_latest(name: str, timeout: float) -> tuple[str, object]:
          """Return (resolved|notfound|unavailable, version-string-or-status)."""
          url = f"{REGISTRY}/{urllib.parse.quote(name, safe='')}/latest"
          req = urllib.request.Request(url, method="GET",
                                       headers={"User-Agent": "claude-mods-tailwind-ops-check/1",
                                                "Accept": "application/json"})
          try:
              with urllib.request.urlopen(req, timeout=timeout) as resp:
                  manifest = json.loads(resp.read().decode("utf-8"))
                  return ("resolved", manifest.get("version", ""))
          except urllib.error.HTTPError as exc:
              if exc.code in (404, 410):
                  return ("notfound", exc.code)
              return ("unavailable", exc.code)
          except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
              return ("unavailable", str(getattr(exc, "reason", exc)))
      
      
      def major_of(version: str) -> int | None:
          m = re.match(r"\d+", version.strip())
          return int(m.group(0)) if m else None
      
      
      def check_live(facts: dict, timeout: float) -> tuple[list[dict], list[dict]]:
          drift: list[dict] = []
          unreachable: list[dict] = []
          for name, info in facts["packages"].items():
              documented = info["documented_major"]
              status, info2 = npm_latest(name, timeout)
              if status == "notfound":
                  drift.append({"package": name, "issue": "no longer resolves on npm (404) — renamed/removed"})
              elif status == "unavailable":
                  unreachable.append({"package": name, "issue": f"registry unreachable: {info2}"})
              else:
                  live = major_of(str(info2))
                  if live is None:
                      unreachable.append({"package": name, "issue": f"could not parse version {info2!r}"})
                  elif live > documented:
                      drift.append({"package": name,
                                    "issue": f"live major {live} ({info2}) ahead of documented major {documented}"})
          return drift, unreachable
      
      
      def main(argv: list[str]) -> int:
          p = argparse.ArgumentParser(
              prog="check-tailwind-facts.py",
              description="Verify tailwind-ops' Tailwind v4 facts stay named (offline) and current on npm (live).",
          )
          mode = p.add_mutually_exclusive_group()
          mode.add_argument("--offline", action="store_true", help="structural consistency, no network (default)")
          mode.add_argument("--live", action="store_true", help="check each package's npm major vs documented")
          p.add_argument("--facts", default=str(DEFAULT_FACTS), help="facts catalog JSON")
          p.add_argument("--skill", default=str(DEFAULT_SKILL), help="skill directory (SKILL.md + references/)")
          p.add_argument("--timeout", type=float, default=10.0, help="per-request timeout seconds (live)")
          p.add_argument("--json", action="store_true", help="emit a JSON envelope")
          try:
              args = p.parse_args(argv)
          except SystemExit as exc:
              return EX_USAGE if exc.code not in (0, None) else (exc.code or EX_OK)
      
          facts = load_facts(Path(args.facts))
          live = args.live and not args.offline
          t = Term(sys.stderr)
      
          if live:
              drift, unreachable = check_live(facts, args.timeout)
              findings = drift + unreachable
              if args.json:
                  print(json.dumps({
                      "data": findings,
                      "meta": {"mode": "live", "packages_checked": len(facts["packages"]),
                               "drift": len(drift), "unreachable": len(unreachable),
                               "registry": REGISTRY, "schema": SCHEMA},
                  }, indent=2))
              else:
                  for f in findings:
                      kind = "DRIFT" if f in drift else "UNREACH"
                      print(f"{kind}  {f['package']}: {f['issue']}")
              if drift:
                  print(f"{t.mark(False)} tailwind-facts/live: {len(drift)} package(s) drifted "
                        f"{t.c('dim', '(' + REGISTRY + ')')}", file=sys.stderr)
                  return EX_DRIFT
              if unreachable:
                  print(f"{t.mark(False)} tailwind-facts/live: npm unreachable for "
                        f"{len(unreachable)}/{len(facts['packages'])} {t.c('dim', '(advisory - retry next run)')}",
                        file=sys.stderr)
                  return EX_UNAVAILABLE
              print(f"{t.mark(True)} tailwind-facts/live: all {len(facts['packages'])} package(s) "
                    f"at or below documented major", file=sys.stderr)
              return EX_OK
      
          # offline (default)
          findings = check_offline(facts, Path(args.skill))
          if args.json:
              print(json.dumps({
                  "data": findings,
                  "meta": {"mode": "offline", "packages_checked": len(facts["packages"]),
                           "drift": len(findings), "consistent": not findings, "schema": SCHEMA},
              }, indent=2))
          else:
              for f in findings:
                  print(f"DRIFT  {f['package']}: {f['issue']}")
          ok = not findings
          print(f"{t.mark(ok)} tailwind-facts/offline: {len(facts['packages'])} package(s) + "
                f"{sum(1 for k in facts.get('version_gates', {}) if k != '_comment')} gate(s) checked, "
                f"{len(findings)} inconsistency {t.c('dim', '(catalog vs skill prose)')}", file=sys.stderr)
          return EX_DRIFT if findings else EX_OK
      
      
      if __name__ == "__main__":
          sys.exit(main(sys.argv[1:]))
      
  • tests
    • run.sh 3.5 KB
      #!/usr/bin/env bash
      # Offline self-test for the tailwind-ops skill — structure, frontmatter, and the
      # staleness-verifier contract (SKILL-RESOURCE-PROTOCOL.md §7, §10).
      #
      # Offline-deterministic (no network, no Tailwind install). Resolves paths
      # relative to itself so it works in the repo and once installed to ~/.claude/.
      #
      # Usage:   bash tests/run.sh
      # Input:   none (self-contained; no network)
      # Output:  TAP-ish progress on stderr; final PASS/FAIL line.
      # Exit:    0 all pass, 1 any failure
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      DOC="$SKILL/SKILL.md"
      
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1" >&2; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1" >&2; }
      
      # Resolve a *working* python (python3, else python). The bare `command -v` is
      # not enough on Windows, where `python3` is a Microsoft Store stub that exits
      # nonzero. Skip the whole verifier block if none works.
      PY=""
      for c in python3 python py; do
        if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PY="$c"; break; fi
      done
      
      echo "=== tailwind-ops self-test ===" >&2
      
      # ── SKILL.md frontmatter ───────────────────────────────────────────────────
      [[ -f "$DOC" ]] && ok "SKILL.md present" || { no "SKILL.md missing"; echo "=== $PASS passed, $FAIL failed ===" >&2; exit 1; }
      doc="$(cat "$DOC")"
      case "$doc" in *"name: tailwind-ops"*) ok "frontmatter declares name: tailwind-ops";; *) no "frontmatter name != tailwind-ops";; esac
      case "$doc" in *"license: MIT"*) ok "frontmatter declares license: MIT";; *) no "missing license: MIT";; esac
      case "$doc" in *"as of 20"*) ok "currency note carries a year";; *) no "no dated 'as of <year>' currency note";; esac
      
      # ── resources present + cited ──────────────────────────────────────────────
      for res in assets/tailwind-facts.json scripts/check-tailwind-facts.py; do
        [[ -f "$SKILL/$res" ]] && ok "resource present: $res" || no "missing resource: $res"
      done
      case "$doc" in *"scripts/check-tailwind-facts.py"*) ok "verifier cited from SKILL.md";; *) no "verifier uncited";; esac
      
      # ── staleness verifier: offline contract (§7) ───────────────────────────────
      if [[ -n "$PY" ]]; then
        V="$SKILL/scripts/check-tailwind-facts.py"
        ec() { local want="$1" lbl="$2"; shift 2; "$@" >/dev/null 2>&1; local got=$?
               [[ "$got" == "$want" ]] && ok "$lbl (exit $got)" || no "$lbl (want $want got $got)"; }
        TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
        ec 0 "py_compile"            "$PY" -m py_compile "$V"
        ec 0 "--help"                "$PY" "$V" --help
        ec 0 "--offline consistent"  "$PY" "$V" --offline
        ec 2 "bad flag -> 2"         "$PY" "$V" --bogus
        ec 2 "conflicting modes -> 2" "$PY" "$V" --offline --live
        jout="$("$PY" "$V" --offline --json 2>/dev/null)"
        case "$jout" in *"claude-mods.tailwind-ops.facts/v1"*) ok "--json envelope schema";; *) no "--json envelope schema missing";; esac
        ec 3 "missing facts -> 3"    "$PY" "$V" --offline --facts "$TMP/nope.json"
        printf '{"schema":"claude-mods.tailwind-ops.facts/v1","packages":{"zzz":{"documented_major":1,"prose":["zzznotreal"]}}}' > "$TMP/drift.json"
        ec 10 "uncited package -> 10" "$PY" "$V" --offline --facts "$TMP/drift.json"
      else
        no "no working python to exercise the verifier"
      fi
      
      echo "=== $PASS passed, $FAIL failed ===" >&2
      [[ "$FAIL" -eq 0 ]] || exit 1
      
  • SKILL.md 19.8 KB
    ---
    name: tailwind-ops
    description: "Tailwind CSS utility patterns, responsive design, component patterns, v4 migration, and configuration. Use for: tailwind, tailwindcss, utility classes, responsive design, dark mode, tailwind v4, tailwind config, tw, container queries, @apply, prose, typography, animation."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: react-ops, vue-ops, astro-ops
    ---
    
    # Tailwind Operations
    
    Comprehensive Tailwind CSS patterns covering layout, responsive design, components, dark mode, animations, and v4 migration.
    
    > Tailwind v4 ecosystem facts verified as of 2026-07.
    
    ## Layout Decision Tree
    
    ```
    Which layout approach?
    │
    ├─ Items in a single row or column?
    │  └─ Use Flexbox
    │     ├─ Row:    class="flex items-center gap-4"
    │     ├─ Column: class="flex flex-col gap-4"
    │     ├─ Wrap:   class="flex flex-wrap gap-4"
    │     └─ Push item to end: class="flex" + child class="ml-auto"
    │
    ├─ Items in a 2D grid (rows AND columns)?
    │  └─ Use CSS Grid
    │     ├─ Equal columns:   class="grid grid-cols-3 gap-6"
    │     ├─ Responsive grid:  class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
    │     ├─ Sidebar layout:  class="grid grid-cols-[250px_1fr] gap-6"
    │     ├─ Spanning:         child class="col-span-2" or "row-span-2"
    │     └─ Auto-fill:        class="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-6"
    │
    ├─ Component should adapt to its CONTAINER size (not viewport)?
    │  └─ Use Container Queries (v3.2+ / v4 native)
    │     ├─ Parent:  class="@container"
    │     ├─ Child:   class="@sm:flex-row @lg:grid-cols-3"
    │     └─ Named:   class="@container/sidebar" → child: "@sm/sidebar:flex-row"
    │
    ├─ Centering something?
    │  ├─ Horizontal text:  class="text-center"
    │  ├─ Horizontal block: class="mx-auto" (needs width)
    │  ├─ Flex center:      class="flex items-center justify-center"
    │  ├─ Grid center:      class="grid place-items-center"
    │  └─ Absolute center:  class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
    │
    └─ Full-page layout (header/sidebar/content/footer)?
       └─ Use Grid with named areas or template rows
          ├─ Sticky header:  class="grid grid-rows-[auto_1fr_auto] min-h-screen"
          └─ Sidebar + main: class="grid grid-cols-[250px_1fr] min-h-screen"
    ```
    
    ## Responsive Design Quick Reference
    
    ### Breakpoints (Mobile-First)
    
    | Prefix | Min Width | Typical Target |
    |--------|-----------|----------------|
    | _(none)_ | 0px | Mobile (default) |
    | `sm:` | 640px | Large phones, landscape |
    | `md:` | 768px | Tablets |
    | `lg:` | 1024px | Small laptops |
    | `xl:` | 1280px | Desktops |
    | `2xl:` | 1536px | Large screens |
    
    **Mobile-first means**: base styles apply to mobile, add breakpoint prefixes to override upward.
    
    ```html
    <!-- Stack on mobile, 2 columns on tablet, 3 on desktop -->
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <div>Card 1</div>
      <div>Card 2</div>
      <div>Card 3</div>
    </div>
    
    <!-- Hide on mobile, show on desktop -->
    <nav class="hidden lg:flex items-center gap-6">...</nav>
    
    <!-- Full width on mobile, constrained on desktop -->
    <div class="w-full max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">...</div>
    ```
    
    ### Container Queries
    
    ```html
    <!-- Parent declares itself as a container -->
    <div class="@container">
      <!-- Children respond to PARENT width, not viewport -->
      <div class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3 gap-4">
        <div>Adapts to container</div>
      </div>
    </div>
    
    <!-- Named container (useful when nesting) -->
    <div class="@container/card">
      <h2 class="text-sm @md/card:text-lg">Responds to card container</h2>
    </div>
    ```
    
    ### Fluid Typography with clamp()
    
    ```html
    <!-- Fluid heading: 1.5rem at small, 3rem at large, scales between -->
    <h1 class="text-[clamp(1.5rem,4vw,3rem)]">Fluid Heading</h1>
    
    <!-- Fluid body text -->
    <p class="text-[clamp(0.875rem,1.5vw,1.125rem)] leading-relaxed">
      Body text that scales smoothly.
    </p>
    ```
    
    ## Dark Mode Decision Tree
    
    ```
    Which dark mode strategy?
    │
    ├─ Manual toggle (user preference stored)?
    │  └─ class strategy (v3) / selector strategy (v4)
    │
    │     v3: tailwind.config.js
    │     module.exports = { darkMode: 'class' }
    │     → Add class="dark" to <html> element
    │
    │     v4: CSS @custom-variant or default behavior
    │     @custom-variant dark (&:where(.dark, .dark *));
    │     → Same toggle, add class="dark" to <html>
    │
    ├─ Follow system preference only?
    │  └─ media strategy
    │
    │     v3: tailwind.config.js
    │     module.exports = { darkMode: 'media' }
    │     → Uses prefers-color-scheme automatically
    │
    │     v4: Default behavior (no config needed)
    │     → Uses prefers-color-scheme out of the box
    │
    └─ Custom selector (data attribute, etc.)?
       └─ selector strategy (v4 only)
    
          v4: @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
          → Add data-theme="dark" to <html>
    ```
    
    ### Dark Mode Patterns
    
    ```html
    <!-- Background and text -->
    <div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
    
      <!-- Card with dark variant -->
      <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
        <h3 class="text-gray-900 dark:text-white font-semibold">Card Title</h3>
        <p class="text-gray-600 dark:text-gray-400">Card content adapts to dark mode.</p>
      </div>
    
      <!-- Input with dark variant -->
      <input type="text"
        class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
               text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500
               focus:ring-2 focus:ring-blue-500 rounded-lg px-4 py-2"
        placeholder="Type here...">
    </div>
    ```
    
    ## Component Patterns Quick Reference
    
    ```html
    <!-- Card -->
    <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
      <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Title</h3>
      <p class="text-gray-600 dark:text-gray-400">Content here.</p>
    </div>
    
    <!-- Button variants -->
    <button class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors">Primary</button>
    <button class="bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition-colors">Secondary</button>
    <button class="border border-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors">Outline</button>
    <button class="text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors">Ghost</button>
    
    <!-- Form input -->
    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
    <input type="email"
      class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
             bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
             focus:ring-2 focus:ring-blue-500 focus:border-transparent"
      placeholder="you@example.com">
    
    <!-- Navbar -->
    <nav class="bg-white dark:bg-gray-900 shadow">
      <div class="max-w-7xl mx-auto px-4 flex items-center justify-between h-16">
        <a href="/" class="text-xl font-bold text-gray-900 dark:text-white">Logo</a>
        <div class="hidden md:flex items-center gap-6">
          <a href="#" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">Home</a>
          <a href="#" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700">CTA</a>
        </div>
      </div>
    </nav>
    
    <!-- Modal overlay -->
    <div class="fixed inset-0 z-50 flex items-center justify-center">
      <div class="fixed inset-0 bg-black/50" aria-hidden="true"></div>
      <div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6 w-full max-w-md mx-4" role="dialog" aria-modal="true">
        <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Modal Title</h2>
        <p class="text-gray-600 dark:text-gray-400 mb-6">Modal content goes here.</p>
        <div class="flex justify-end gap-3">
          <button class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg">Cancel</button>
          <button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Confirm</button>
        </div>
      </div>
    </div>
    
    <!-- Badge -->
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">Active</span>
    
    <!-- Alert -->
    <div class="flex items-start gap-3 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800" role="alert">
      <span class="text-red-600 dark:text-red-400 mt-0.5" aria-hidden="true">&#10007;</span>
      <div>
        <h4 class="text-sm font-medium text-red-800 dark:text-red-300">Error</h4>
        <p class="text-sm text-red-700 dark:text-red-400 mt-1">Something went wrong. Please try again.</p>
      </div>
    </div>
    ```
    
    ## Tailwind v4 Quick Reference
    
    ### Major Changes from v3
    
    | Area | v3 | v4 |
    |------|----|----|
    | Configuration | `tailwind.config.js` | CSS-first: `@theme` in CSS |
    | Theme values | JS `theme.extend.colors` | `@theme { --color-brand: #3b82f6; }` |
    | Plugins | JS `plugin()` function | `@plugin "my-plugin"` in CSS |
    | Config file | `module.exports = {...}` | `@config "./legacy.config.js"` (compat) |
    | PostCSS | `tailwindcss` package | `@tailwindcss/postcss` |
    | Vite | PostCSS plugin | `@tailwindcss/vite` (faster) |
    | Colors | Named scales (gray-50..950) | Same + OKLCH support |
    | Container queries | Plugin required | Native `@container`, `@sm:`, `@md:` |
    | Entry animations | JS needed | `@starting-style` (CSS native) |
    
    ### v4 CSS-First Config
    
    ```css
    /* v4: Define theme in CSS */
    @import "tailwindcss";
    
    @theme {
      --color-brand: #3b82f6;
      --color-brand-dark: #1d4ed8;
      --font-display: "Inter", sans-serif;
      --breakpoint-3xl: 1920px;
      --spacing-18: 4.5rem;
    }
    
    /* v4: Import a plugin */
    @plugin "@tailwindcss/typography";
    
    /* v4: Use legacy JS config as fallback */
    @config "./tailwind.config.js";
    ```
    
    ### v4 New Utilities
    
    ```html
    <!-- Container queries (native in v4) -->
    <div class="@container">
      <div class="@sm:flex @md:grid @md:grid-cols-2">Adapts to container</div>
    </div>
    
    <!-- @starting-style: entry animations without JS -->
    <!-- Applied via CSS - Tailwind v4 supports it natively -->
    
    <!-- Anchor positioning (experimental) -->
    <!-- Position elements relative to an anchor element via CSS -->
    
    <!-- New shadow and ring defaults -->
    <div class="shadow-sm ring ring-blue-500/20">Improved defaults</div>
    ```
    
    ## Animation Patterns
    
    ### Transition Utilities
    
    ```html
    <!-- Color transition (most common) -->
    <button class="bg-blue-600 hover:bg-blue-700 transition-colors duration-150">
      Hover me
    </button>
    
    <!-- Multiple properties -->
    <div class="transform hover:scale-105 hover:shadow-lg transition-all duration-200 ease-in-out">
      Scale and shadow on hover
    </div>
    
    <!-- Specific properties -->
    <div class="transition-[transform,opacity] duration-300 ease-out">
      Only transform and opacity animate
    </div>
    ```
    
    ### Built-in Animations
    
    ```html
    <!-- Spin (loading spinners) -->
    <svg class="animate-spin h-5 w-5 text-blue-600" viewBox="0 0 24 24">
      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
      <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
    </svg>
    
    <!-- Pulse (skeleton loaders) -->
    <div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-4 rounded w-3/4"></div>
    
    <!-- Ping (notification indicator) -->
    <span class="relative flex h-3 w-3">
      <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
      <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
    </span>
    
    <!-- Bounce -->
    <div class="animate-bounce">&#8595;</div>
    ```
    
    ### Custom Keyframes (v3 Config)
    
    ```js
    // tailwind.config.js (v3)
    module.exports = {
      theme: {
        extend: {
          keyframes: {
            'fade-in': {
              '0%': { opacity: '0', transform: 'translateY(10px)' },
              '100%': { opacity: '1', transform: 'translateY(0)' },
            },
            'slide-in-right': {
              '0%': { transform: 'translateX(100%)' },
              '100%': { transform: 'translateX(0)' },
            },
          },
          animation: {
            'fade-in': 'fade-in 0.3s ease-out',
            'slide-in-right': 'slide-in-right 0.3s ease-out',
          },
        },
      },
    }
    ```
    
    ### Custom Keyframes (v4 CSS)
    
    ```css
    /* v4: Define in CSS with @theme */
    @theme {
      --animate-fade-in: fade-in 0.3s ease-out;
      --animate-slide-in-right: slide-in-right 0.3s ease-out;
    }
    
    @keyframes fade-in {
      from { opacity: 0; transform: translateY(10px); }
      to { opacity: 1; transform: translateY(0); }
    }
    
    @keyframes slide-in-right {
      from { transform: translateX(100%); }
      to { transform: translateX(0); }
    }
    ```
    
    ### Entry Animations with @starting-style (v4)
    
    ```css
    /* Dialog that animates in from transparent/translated */
    dialog[open] {
      opacity: 1;
      transform: translateY(0);
      transition: opacity 0.3s, transform 0.3s;
    
      @starting-style {
        opacity: 0;
        transform: translateY(10px);
      }
    }
    ```
    
    ## State Modifiers Quick Reference
    
    | Modifier | Triggers On | Example |
    |----------|-------------|---------|
    | `hover:` | Mouse hover | `hover:bg-blue-700` |
    | `focus:` | Element focused (all focus) | `focus:ring-2` |
    | `focus-visible:` | Keyboard focus only | `focus-visible:outline-2` |
    | `focus-within:` | Child is focused | `focus-within:ring-2` |
    | `active:` | Being clicked/pressed | `active:scale-95` |
    | `disabled:` | `disabled` attribute | `disabled:opacity-50 disabled:cursor-not-allowed` |
    | `group-hover:` | Parent `.group` hovered | `group-hover:text-blue-600` |
    | `group-focus:` | Parent `.group` focused | `group-focus:ring-2` |
    | `peer-checked:` | Sibling `.peer` checked | `peer-checked:bg-blue-600` |
    | `peer-invalid:` | Sibling `.peer` invalid | `peer-invalid:text-red-500` |
    | `data-[state=open]:` | Custom data attribute | `data-[state=open]:rotate-180` |
    | `aria-expanded:` | `aria-expanded="true"` | `aria-expanded:bg-gray-100` |
    | `aria-selected:` | `aria-selected="true"` | `aria-selected:font-bold` |
    | `open:` | `<details>` or `<dialog>` open | `open:bg-gray-50` |
    | `first:` | First child | `first:rounded-t-lg` |
    | `last:` | Last child | `last:rounded-b-lg` |
    | `odd:` | Odd children | `odd:bg-gray-50` |
    | `even:` | Even children | `even:bg-white` |
    | `placeholder:` | Placeholder text | `placeholder:text-gray-400` |
    | `motion-reduce:` | Prefers reduced motion | `motion-reduce:transition-none` |
    | `motion-safe:` | No motion preference | `motion-safe:animate-bounce` |
    | `print:` | Print media | `print:hidden` |
    
    ### Group and Peer Patterns
    
    ```html
    <!-- Group: parent state affects children -->
    <a href="#" class="group flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100">
      <div class="w-10 h-10 bg-gray-200 group-hover:bg-blue-100 rounded-lg"></div>
      <span class="text-gray-700 group-hover:text-blue-600">Hover the whole card</span>
    </a>
    
    <!-- Named groups (nested groups) -->
    <div class="group/card p-4">
      <div class="group/button">
        <span class="group-hover/card:text-blue-600 group-hover/button:underline">
          Responds to specific parent
        </span>
      </div>
    </div>
    
    <!-- Peer: sibling state affects next sibling -->
    <input type="checkbox" class="peer sr-only" id="toggle">
    <label for="toggle" class="peer-checked:bg-blue-600 peer-checked:text-white px-4 py-2 rounded-lg cursor-pointer">
      Toggle me
    </label>
    
    <!-- Form validation with peer -->
    <input type="email" class="peer" required>
    <p class="hidden peer-invalid:block text-sm text-red-500 mt-1">
      Please enter a valid email.
    </p>
    ```
    
    ## Common Gotchas
    
    | Gotcha | Why | Fix |
    |--------|-----|-----|
    | Dynamic class names don't work: `` `bg-${color}-500` `` | Tailwind scans source for complete class strings at build time. String interpolation produces classes it never sees. | Use complete classes: `const colors = { red: 'bg-red-500', blue: 'bg-blue-500' }` and select by key. |
    | Styles not applying (specificity) | Another CSS rule or `@apply` has higher specificity. | Use `!important` modifier: `!text-red-500`. Or restructure to avoid conflicts. |
    | `@apply` breaks with component libraries | `@apply` resolves at build time and can't access runtime theme values or conflict with scoped styles. | Prefer inline utility classes. Reserve `@apply` for base styles or markdown content. |
    | Prose plugin styles leak | `@tailwindcss/typography` `prose` applies broad element selectors (h1, p, a, etc.). | Scope with `prose` only on content wrappers. Use `not-prose` class to exclude sections. |
    | Classes missing in production | JIT content detection didn't scan the file containing the class. | Ensure `content` paths in config cover all template files including component libraries. |
    | Dark mode flash (FOUC) | Class-based dark mode renders light first until JS adds `dark` class. | Add inline `<script>` in `<head>` that reads localStorage and sets `dark` class before paint. |
    | Container queries not scoped | Child `@sm:` responds to nearest `@container` ancestor, which may not be the intended one. | Use named containers: `@container/card` and `@sm/card:flex`. |
    | Arbitrary values vs config | `w-[137px]` works but creates one-off values. Repeated arbitrary values signal missing design tokens. | Add recurring values to theme config: `spacing: { '137': '137px' }`. |
    | `group` / `peer` naming collisions | Nested groups without names cause children to respond to wrong ancestor. | Use named groups: `group/card`, `group/button`. |
    | Responsive order matters | Adding `lg:flex` without base `block` or `hidden` can cause unexpected behavior on smaller screens. | Always define the mobile-first base, then override upward: `hidden lg:flex`. |
    | Transition on `display: none` | `hidden` to `block` can't be transitioned because `display` isn't animatable. | Use `opacity-0`/`opacity-100` with `invisible`/`visible`, or use `@starting-style` (v4). |
    | Purge removes dynamic classes | Tailwind purges classes not found as complete strings in scanned files. | Add classes to `safelist` in config, or use a safelist comment in the source file. |
    
    ## Reference Files
    
    | File | Content | Lines |
    |------|---------|-------|
    | `references/component-patterns.md` | Cards, buttons, forms, navigation, modals, tables, alerts, badges, avatars, dropdowns, tooltips, skeleton loaders, accessibility | ~700 |
    | `references/v4-migration.md` | CSS-first config, @theme, @plugin, removed utilities, container queries, @starting-style, migration steps, breaking changes | ~500 |
    | `references/configuration.md` | Theme config (v3+v4), colors, spacing, typography, plugins, @layer, @apply, custom variants, dark mode, container queries | ~500 |
    
    ## Staleness Verifier
    
    This skill encodes fast-moving facts (the Tailwind v4 CSS-first directives, the
    `@tailwindcss/*` package set, the v3→v4 migration). [`scripts/check-tailwind-facts.py`](scripts/check-tailwind-facts.py)
    guards them against silent drift — internal consistency in PR CI, live
    major-version drift in the scheduled freshness job:
    
    ```bash
    # Structural (PR CI, no network): every catalogued package + v4 directive gate
    # is still named in this skill's prose, and the currency note carries a year.
    python3 skills/tailwind-ops/scripts/check-tailwind-facts.py --offline        # exit 0 consistent, 10 drift
    
    # Live (weekly freshness job, never blocks a PR): is any documented major now
    # behind npm's latest dist-tag? (e.g. tailwindcss 5 while the prose says v4.)
    python3 skills/tailwind-ops/scripts/check-tailwind-facts.py --live           # exit 10 a major moved ahead, 7 npm unreachable
    ```
    
    The canonical fact list lives in [`assets/tailwind-facts.json`](assets/tailwind-facts.json); when you add or drop a package or the prose stops naming one, update it to match or `--offline` fails CI.
    
    ## See Also
    
    - `react-ops` - React component patterns using Tailwind
    - `vue-ops` - Vue component patterns using Tailwind
    - `astro-ops` - Astro project patterns with Tailwind integration
    - Tailwind docs: https://tailwindcss.com/docs
    - Tailwind v4 blog: https://tailwindcss.com/blog/tailwindcss-v4
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related