An interactive SVG color wheel that visualizes the complete Tailwind CSS v4.2 default palette — all 26 color families × 11 shades = 286 colors — with four interaction modes, live preview, and full cross-device responsiveness.
- Overview
- Features
- Tech Stack
- Architecture
- Project Structure
- Getting Started
- Interaction Modes
- Design Decisions
- Color System
- Responsive Layout
- Performance
- Tooling
- Scripts
- Contributing
The Tailwind Color Wheel renders all 26 Tailwind v4.2 color families as concentric ring segments in an SVG wheel. Each ring represents one shade level (50 → 950). Clicking any segment performs an action depending on the active mode — changing the page background, previewing text color on a live demo strip, or copying the Tailwind utility class name to the clipboard.
| Feature | Description |
|---|---|
| 286-color SVG wheel | All 26 Tailwind v4.2 color families × 11 shades rendered as interactive SVG path segments |
| 4 interaction modes | Normal, Background, Text Color, Copy Name — switched via a shadcn Select dropdown |
| Live text preview | A demo strip appears in bg and text modes showing real typography in the chosen color |
| Color notification card | Fixed top-right card shows both applied background and text color simultaneously; auto-closes after 4 s with a GSAP slide-out |
| Toast notifications | Sonner-powered toasts confirm every action with mode-aware messaging |
| GSAP animations | Entrance spin, ambient ring pulse, per-segment click flash, card slide-in/out |
| Fully responsive | Single-column on mobile/tablet, two-column on desktop (≥ lg) — SVG never shifts position |
| OKLCH color system | Uses Tailwind v4 CSS custom properties (--color-red-500) in OKLCH color space natively |
| Zero JS color conversion | All colors are expressed as Tailwind utility classes; no hex map, no tailwindcss/colors import |
| Cross-device height fix | html, body, #root { height: 100% } chain ensures correct layout on all mobile browsers |
| Layer | Technology | Version |
|---|---|---|
| Framework | React | 19.x |
| Language | TypeScript | 5.9.x |
| Bundler | Vite | 8.x |
| Styling | Tailwind CSS | 4.2.x |
| Tailwind Vite plugin | @tailwindcss/vite | 4.2.x |
| UI components | shadcn/ui | 4.x |
| Primitives | Radix UI | 1.4.x |
| Animation | GSAP | 3.14.x |
| Toast | Sonner | 2.x |
| Font | Geist Variable | 5.x |
| Linting | ESLint (v9 flat config) + typescript-eslint | 9.x / 8.x |
| Formatting | Prettier + prettier-plugin-tailwindcss | 3.x |
main.tsx
└── ThemeProvider (next-themes — required by shadcn Toaster)
└── App.tsx ← Central hub: ALL state + ALL handlers
│
├── lib/colors.ts COLOR_NAMES, SHADES, DEFAULT_BG_CLASS
├── lib/geometry.ts SEGMENTS — pre-computed SVG path d-strings
└── lib/modes.ts MODES, ModeKey type
│
├── <Backdrop /> Fixed atmospheric glow (no props, memo)
├── <Header /> Brand + shadcn Select mode picker (memo)
│
├── Content area (flex-col → flex-row on lg+)
│ ├── <WheelSVG /> SVG wheel, GSAP animations (memo)
│ └── <aside> Desktop-only DemoText panel
│ └── <DemoText />
│
└── Fixed overlays (outside shell, z-indexed)
├── <DemoText /> Mobile fixed-bottom overlay (<lg only)
├── <ColorCard /> Notification card top-right (forwardRef)
└── <Toaster /> Sonner toast container
All application state lives exclusively in App.tsx. Components are purely presentational — they
receive props and emit events upward. No component manages its own color or mode state.
| State | Type | Purpose |
|---|---|---|
mode |
ModeKey |
Active interaction mode |
bgClass |
string |
Tailwind bg-* class for shell background |
bgFillClass |
string |
Tailwind fill-* class for SVG center cap |
textClass |
string |
Tailwind text-* class for DemoText |
prevBgName |
string | null |
Last applied background name (for combined toast) |
prevTextName |
string | null |
Last applied text name (for combined toast) |
card |
CardData |
{ bgName, textName } — both channels |
cardVisible |
boolean |
Mounts/unmounts ColorCard |
.
├── components/
│ ├── ColorWheel/
│ │ ├── Backdrop.tsx Atmospheric glow layers — no props, React.memo
│ │ ├── ColorCard.tsx Notification card (forwardRef + React.memo)
│ │ ├── DemoText.tsx Live color preview strip — React.memo
│ │ ├── Header.tsx Top bar: brand + shadcn Select — React.memo
│ │ └── WheelSVG.tsx SVG wheel + all GSAP animations — React.memo
│ └── ui/
│ ├── button.tsx shadcn Button
│ ├── select.tsx shadcn Select
│ └── sonner.tsx shadcn Toaster
│
├── lib/
│ ├── colors.ts 26 COLOR_NAMES, 11 SHADES, class helpers
│ ├── geometry.ts SEGMENTS — pre-computed at module load
│ ├── modes.ts MODES config, ModeKey type
│ └── utils.ts shadcn cn() utility
│
├── src/
│ ├── App.tsx Central hub — all state, all handlers
│ ├── index.css Tailwind import, @source inline() safelists,
│ │ @theme variables, shadcn theme tokens
│ └── main.tsx Entry point — ThemeProvider + App
│
├── public/
│ ├── favicon.svg
│ └── icons.svg
│
├── .prettierrc Prettier config with tailwindcss plugin
├── .prettierignore
├── components.json shadcn configuration
├── eslint.config.js ESLint v9 flat config
├── tsconfig.json Root TS config (references app + node)
├── tsconfig.app.json App compiler options + @ path alias
├── tsconfig.node.json Node/Vite compiler options
└── vite.config.ts Vite + React + Tailwind plugins, @ alias
- Node.js ≥ 20
- npm ≥ 10
# 1. Clone or extract the project
cd color-wheel
# 2. Install dependencies
npm install
# 3. Start the development server
npm run devOpen http://localhost:5173 in your browser.
npm run build
npm run previewThe build output goes to dist/. Source maps are included (sourcemap: true in vite.config.ts).
Select a mode from the dropdown in the top-right of the header, then click any color segment on the wheel.
| Mode | Icon | Action on click |
|---|---|---|
| Normal | 👁 | No action — inspect the wheel freely |
| Background | 🎨 | Sets the page background to the clicked color |
| Text Color | ✍️ | Sets the text preview color in the DemoText strip |
| Copy Name | 📋 | Copies the Tailwind utility name (e.g. red-500) to clipboard |
- First selection — toast shows just the color name:
🎨 red-500 - Both channels set — toast shows both:
🎨 BG: red-500 · Text: sky-300
- Appears top-right below the header when a color is applied in
bgortextmode - Shows both the background channel and text channel simultaneously (each with a color swatch)
- Auto-dismisses after 4 seconds with a GSAP slide-out animation
- Can be manually dismissed with the × button
Tailwind v4 removed the safelist config option. The official replacement is @source inline()
with brace expansion in the CSS file. Since all color class names are built dynamically at runtime
(e.g. `fill-${twName}`), the Tailwind scanner cannot detect them in source files.
@source inline() forces generation of every permutation at build time.
/* src/index.css */
@source inline('fill-{red,orange,...,mist}-{50,100,...,950}');
@source inline('stroke-{red,orange,...,mist}-{50,100,...,950}');
@source inline('bg-{red,orange,...,mist}-{50,100,...,950}');
@source inline('text-{red,orange,...,mist}-{50,100,...,950}');Tailwind v4 exposes every color as a CSS custom property in OKLCH (e.g.
--color-red-500: oklch(63.7% 0.237 25.331)). A previous iteration used var(--color-red-500) in
inline styles. This was replaced because:
- Tailwind utility classes are the official intended API —
fill-red-500,bg-red-500,text-red-500 transition-colorson a Tailwind class animates correctly;transitionon an inlinebackground-colorstyle does not integrate with Tailwind's generated transitions- Purging is predictable —
@source inline()is the documented safelist mechanism
h-screen (100vh) includes the browser chrome on mobile — the content shifts when the address bar
hides/shows. h-svh (100svh) fixes this but is not supported on older Android WebView and some
mobile browsers, causing the shell to collapse to zero height.
The correct cross-browser solution is to propagate height through the DOM chain:
html,
body,
#root {
height: 100%;
overflow: hidden;
}Then the App shell uses h-full, which resolves reliably on every browser and device.
The SVG wrapper is the largest square that fits its container. This works on every screen size with
zero JavaScript measurement. The wheel is decoupled from layout shifts via absolute inset-0 on the
inner container, so adding or removing DemoText, ColorCard, or toast notifications never causes the
SVG to move.
All paths originate from the center point (250, 250). Because SVG paints later elements on top,
color segments must render before cap paths. SEGMENTS is ordered with color rings first
(indices 0–10) and center cap last (index 11), so a single SEGMENTS.map() preserves the correct
visual layering without any filtering or reordering.
26 color families, each with 11 shades (50, 100, 200 … 950):
red · orange · amber · yellow · lime · green · emerald · teal
cyan · sky · blue · indigo · violet · purple · fuchsia · pink · rose
slate · gray · zinc · neutral · stone
mauve · olive · taupe · mist ← added in v4.2
The 26 innermost path segments (center cap) use fill-* and stroke-* classes that are both
derived from bgFillClass:
className={`
transition-colors duration-700 stroke-1
${bgFillClass}
${bgFillClass.replace('fill-', 'stroke-')}
`}When Background mode is active and a color is clicked, bgFillClass updates to e.g. fill-red-500,
and the derived stroke becomes stroke-red-500. Both transition simultaneously with Tailwind's
transition-colors duration-700.
| Breakpoint | Layout | DemoText |
|---|---|---|
< lg (mobile, tablet) |
Single column — SVG fills content area | Fixed bottom overlay (fixed inset-x-3 bottom-4) |
≥ lg (desktop) |
Two columns — SVG left, DemoText right panel (w-80 xl:w-96) |
Right panel sidebar (normal flow) |
The SVG column always uses relative flex-1 min-h-0 min-w-0 with absolute inset-0 inside,
ensuring:
- The SVG claims its share of the flex layout
- The visual position is completely decoupled from sibling elements
- No JavaScript resize observers or measurement needed
| Optimization | Implementation |
|---|---|
React.memo |
All 5 components wrapped — prevents re-renders from App state changes that don't affect the component |
| Pre-computed geometry | SEGMENTS is computed once at module load (not per render) — 312 path d-strings calculated at startup |
| GSAP GPU compositing | Entrance animation uses rotation and scale — composited on GPU, no layout reflow |
| Vite pre-bundling | react and react-dom explicitly listed in optimizeDeps.include for faster cold starts |
| CSS custom properties | Tailwind v4 color transitions use CSS vars — no JavaScript involved in color interpolation |
| File watcher | usePolling: true with interval: 100 in vite.config.ts prevents ENOSPC errors in large monorepos |
ESLint v9 flat config with:
@eslint/jsrecommended rulestypescript-eslintstrict type checkingeslint-plugin-react-hooks— enforces rules of hooks and exhaustive depseslint-plugin-react-refresh— warns on non-refresh-safe component exportseslint-plugin-prettier— surfaces Prettier formatting violations as ESLint errorseslint-config-prettier— disables ESLint rules that conflict with Prettier
{
"semi": false,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/index.css"
}prettier-plugin-tailwindcss automatically sorts Tailwind classes in the canonical order defined by
the official Tailwind stylesheet.
@ resolves to the project root (not src/) because shadcn places components/ and lib/ at
the root. Configured in both vite.config.ts and tsconfig.app.json:
// vite.config.ts
alias: { '@': path.resolve(__dirname, '.') }
// tsconfig.app.json
"paths": { "@/*": ["./*"] }# Start development server (hot reload)
npm run dev
# Production build (TypeScript check + Vite bundle)
npm run build
# Preview production build locally
npm run preview
# Run ESLint
npm run lint
# Run ESLint and auto-fix issues
npm run lint:fix
# Format all files with Prettier
npm run format
# Check formatting without writing (CI-safe)
npm run format:check
# TypeScript type check without emitting files
npm run type-check- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Make changes — run
npm run lintandnpm run format:checkbefore committing - Commit using conventional commits:
git commit -m "feat: add color export" - Push and open a pull request
- All color values must use Tailwind utility classes — no inline
style={{ color: ... }}, no hex strings, novar(--color-*)in JS - Dynamic classes must be safelisted in
src/index.cssvia@source inline() - All components should be wrapped in
React.memo - Business logic belongs in
App.tsx— components are purely presentational - New color families added to Tailwind must be added to
COLOR_NAMESinlib/colors.tsand to the@source inline()lists insrc/index.css
Private project. All rights reserved.