@itsarises/theme-control
v0.4.0
Published
Dev-mode floating control panel for Next.js + Tailwind v4: live-tweak colors, fonts and radius, then write changes back to your @theme block.
Maintainers
Readme
@itsarises/theme-control
Dev-mode floating control panel for Next.js + Tailwind CSS v4, by arises.com. Live-tweak your design tokens — colors, fonts, border radius — directly in the browser, then hit Apply to write the changes back into your @theme block at the code level.
- 3–12 color tokens: lock, remove, add, copy, per-tile regenerate, regenerate-all (OKLCH harmony algorithm)
- Edit any color as hex,
oklch(), or a CSS color name ("tomato"); toggle display format hex ↔ oklch - Font swapping from a curated Google Fonts list (live preview,
--font-*written on Apply) - Border radius sliders for every
--radius*token - Undo (100 steps) and Revert (back to last applied file state)
- Real-time preview via CSS variable overrides — zero rebuild, works with everything that uses your Tailwind tokens
- Production-safe: the component renders
nulland the API returns 404 outsideNODE_ENV=development
How it works
- GET
/api/theme-controlreads your CSS file, parses the@theme { ... }block, returns all--color-*,--font-*,--radius*tokens. - Every panel change sets the variable on
document.documentElement→ instant live preview. - Apply POSTs the token set; the server rewrites only the values inside
@theme(rest of the file untouched, byte-for-byte). Tailwind's dev server hot-reloads the CSS.
Install
npm i -D @itsarises/theme-control1. API route (App Router)
// app/api/theme-control/route.ts
export { GET, POST } from "@itsarises/theme-control/server"Custom CSS location:
import { createHandlers } from "@itsarises/theme-control/server"
export const { GET, POST } = createHandlers({ cssPath: "src/styles/theme.css" })By default these paths are tried: app/globals.css, src/app/globals.css, styles/globals.css, src/styles/globals.css.
2. Mount the panel (zero production bytes)
// app/layout.tsx
import dynamic from "next/dynamic"
const ThemeControl =
process.env.NODE_ENV === "development"
? dynamic(() => import("@itsarises/theme-control").then((m) => m.ThemeControl))
: () => null
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<ThemeControl />
</body>
</html>
)
}A plain import { ThemeControl } from "@itsarises/theme-control" also works (it renders null in production), but the dynamic pattern above keeps the code out of production bundles entirely.
3. Have tokens in @theme
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-background: oklch(0.985 0.005 250);
--color-foreground: oklch(0.21 0.02 250);
--color-primary: oklch(0.65 0.15 165);
--color-muted: oklch(0.9 0.01 250);
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, monospace;
--radius: 0.625rem;
}Use them as normal Tailwind classes (bg-background, text-primary, font-sans, shadcn rounded-lg), and the whole site responds to the panel in real time.
Options
| Prop / option | Where | Default | Description |
|---|---|---|---|
| endpoint | <ThemeControl /> | /api/theme-control | API route the panel calls |
| cssPath | createHandlers() | auto-detected | CSS file containing the @theme block, relative to project root |
Notes & limits (v0.x)
- Only values inside
@themeare written. Hardcoded hexes inclassNamestrings are out of scope by design. - Font Apply writes the
--font-*stack. For production you should still load the font properly (next/fontor a<link>); the panel's Google-Fonts<link>injection is preview-only. - Removing a token deletes its declaration on Apply — make sure nothing still references it.
- Commit before big Apply sessions; the file write is surgical but git is your real undo.
v0.4 — shadcn fonts, bulletproof preview, version badge
- Fonts section restored for shadcn projects:
--font-sans: var(--font-geist-sans)tokens are now managed. Preview overrides the underlying variable; Apply writes a literal font stack into@theme(replacing the next/font link for that token — a "Project default" option switches back). - Live preview fixed for
@theme inline: Tailwind can bake the underlying variable into utilities, so the panel now overrides both the theme key and the underlying var. Preview works in every Tailwind emit mode. - Version badge: the panel header shows its version (e.g. v0.4.0), so you can instantly confirm an update took. When updating: bump
src/version.tstogether with package.json.
v0.3 — undo fixes + version history
- Undo/Revert fixed: history snapshots were being duplicated by React StrictMode's double-invoked updaters (undo appeared to do nothing). Snapshots now happen outside the render cycle.
- Coalesced undo steps: rapid changes to the same control (slider drags, repeated edits within 800ms) collapse into one undo step instead of one per pixel.
- Version history (v1, v2, v3…): every successful Apply saves a version to localStorage (per CSS file, capped at 20). The History section lists them — Restore loads any version as a live preview, then Apply writes it back. Git remains your durable history; restore covers token values, not adds/removes.
v0.2 — shadcn/ui support
- Full shadcn structure support:
@theme inline { --color-x: var(--x) }is resolved; edits are written to:rootand.dark(where the real values live), not the mapping block. Dark/light theming keeps working after Apply. - Light / Dark editing: when a
.darkblock exists, a mode toggle appears — edit and preview each mode's palette separately. Removing a token cleans it out of@theme,:root, and.dark. - Grouped tokens: with 10+ colors, tiles are grouped into Core / Charts / Sidebar (Charts and Sidebar start collapsed) so shadcn's ~27 tokens stay manageable.
- Overrides clear on Apply: after a successful Apply the live-preview overrides are removed, so the page reflects exactly what's in the file (previously it looked like "nothing happened").
- Portal rendering: the panel portals into
document.body, so<ThemeControl />can be mounted anywhere — even directly under<html>— without invalid-nesting hydration errors. - Values the panel can't manage safely (e.g.
calc()-derived radii,var()-based font stacks fromnext/font) are left untouched.
Development
npm i
npm run typecheck
npm run build # tsup → dist/ (ESM + d.ts)Releasing
npm version patch # or minor / major
npm publish # publishConfig.access is already "public"
git push --follow-tagsMIT © itsace (arises.com)
