tweakcn-ui
v0.5.8
Published
Reusable Theme Editor UI kit extracted from tweakcn.com. Import ThemeEditor or individual parts.
Downloads
415
Readme
tweakcn-ui
Reusable UI kit extracted from tweakcn.com. It lets you:
- Import a single
ThemeEditorcomponent that behaves like tweakcn’s editor (without auth/AI/save). - Import individual sub-components (ColorPicker, SliderWithInput, FontPicker, ThemePreviewPanel, etc.) to build your own custom UI.
Install
Add the package to your app. Supports React 18 and 19.
Peer deps: react@>=18 <20, react-dom@>=18 <20.
TypeScript: install @types/react that matches your React version.
Runtime deps used by the package: zustand, lucide-react, clsx, tailwind-merge, culori, zod.
Changes in 0.4.3
- Split package into server-safe and client-only entry points to support Next.js RSC without runtime errors.
- Root
tweakcn-uinow exports only RSC-safe utilities and types. - Added
tweakcn-ui/clientfor client components and hooks (ThemeEditor, ThemeProvider, ThemeRoot, ThemeScript, Previews, etc.). - Added
tweakcn-ui/serverfor server components (ServerThemePayload).
- Root
- Fixes errors like
(0, react__WEBPACK_IMPORTED_MODULE_0__.createContext) is not a functionwhen importing the library from Server Components. - Updated documentation with RSC-safe usage examples and a migration guide.
Usage (Next.js / RSC)
This package provides separate entry points to avoid RSC importing client code:
tweakcn-ui(root): RSC-safe utilities onlytweakcn-ui/client: client components and hookstweakcn-ui/server: server-only components
Wrap your app (or the area you edit) with ThemeProvider and include ThemeScript in your HTML <head> to hydrate tokens early. Import these from the client entry.
"use client";
import { ThemeProvider, ThemeScript, ThemeEditor } from 'tweakcn-ui/client';
export default function Page() {
return (
<html>
<head>
<ThemeScript />
</head>
<body>
<ThemeProvider>
<div style={{ height: '80vh' }}>
<ThemeEditor
googleFontsApiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY}
/>
</div>
</ThemeProvider>
</body>
</html>
);
}ThemeScript and ThemeProvider now hydrate font families alongside CSS variables. Use the optional skipGoogleFonts prop when your app already imports certain families (for example via next/font) and fontTargets to override which selectors get each role (sans, serif, mono).
If you prefer to compose your own UI:
"use client";
import { ThemeProvider, ThemeControlPanel, ThemePreviewPanel, useEditorStore } from 'tweakcn-ui/client';
function CustomBuilder() {
const { themeState, setThemeState } = useEditorStore();
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ThemeControlPanel
styles={themeState.styles}
currentMode={themeState.currentMode}
onChange={(styles) => setThemeState({ ...themeState, styles })}
themePromise={Promise.resolve(null)}
/>
<ThemePreviewPanel styles={themeState.styles} currentMode={themeState.currentMode} />
</div>
);
}New: Theme payload viewers (server + client)
ServerThemePayload(server component) prints anyThemeStatePayloadin layouts or server-rendered sections.ClientThemePayload(client component) shows a live-updating payload from the editor store.
Server example (Next.js layout):
// app/layout.tsx (server component)
import { ServerThemePayload } from 'tweakcn-ui/server';
import ThemeRootClient from './ThemeRootClient';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const payload = await loadThemeFromDBOrFile(); // returns ThemeStatePayload
return (
<html lang="en">
<head>{/* render ThemeScript in ThemeRootClient */}</head>
<body>
<ThemeRootClient>
<ServerThemePayload payload={payload} />
{children}
</ThemeRootClient>
</body>
</html>
);
}Client example (live changes):
Client shim used above:
// app/ThemeRootClient.tsx
"use client";
import { ThemeRoot, ThemeScript } from 'tweakcn-ui/client';
export default function ThemeRootClient({ children }: { children: React.ReactNode }) {
return (
<ThemeRoot skipGoogleFonts={["Inter"]}>
<ThemeScript skipGoogleFonts={["Inter"]} />
{children}
</ThemeRoot>
);
}Adjust skipGoogleFonts/fontTargets to match the fonts your app already handles or to retarget typography roles.
"use client";
import { ClientThemePayload } from 'tweakcn-ui/client';
export default function DebugPanel() {
return <ClientThemePayload title="Live Theme Payload" />;
}The payload now includes a fonts array describing which families are active, whether they map to next/font, and the Google import URLs the runtime will load.
Export/Import theme JSON
- Export the current theme state as JSON you can persist:
import { exportThemePayload } from 'tweakcn-ui';
const payload = exportThemePayload(); // plain JSON: styles + fonts + currentMode + hslAdjustments + preset
localStorage.setItem('my-theme', JSON.stringify(payload));- Import a previously saved payload and apply live:
import { importThemePayload, parseThemePayload } from 'tweakcn-ui';
const raw = localStorage.getItem('my-theme');
if (raw) {
const payload = parseThemePayload(raw); // validates structure
importThemePayload(payload); // updates store and applies CSS vars on :root
}Apply or compute CSS variables
- Apply the theme in store to
document.documentElement:
import { applyThemeVars } from 'tweakcn-ui';
applyThemeVars();- Compute a map of CSS variables without mutating the DOM:
import { computeCssVarMap } from 'tweakcn-ui';
// in client code
import { useEditorStore } from 'tweakcn-ui/client';
const state = useEditorStore.getState().themeState;
const vars = computeCssVarMap(state); // { --background: 'hsl(...)', --radius: '...', --shadow-md: '...' }Persist to your DB with hooks
import { useThemePersistence } from 'tweakcn-ui/client';
function Settings() {
const { save, load, loading } = useThemePersistence({
load: async () => {
const r = await fetch('/api/theme');
return await r.json();
},
save: async (payload) => {
await fetch('/api/theme', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) });
},
});
return (
<div>
<button onClick={() => load()} disabled={loading}>Load</button>
<button onClick={() => save()} disabled={loading}>Save</button>
</div>
);
}Richer previews (optional)
Import optional previews for Cards, Pricing, and Shadows without extra deps:
import { Previews } from 'tweakcn-ui/client';
function Showcase() {
return (
<div className="space-y-6 p-4">
<Previews.CardsPreview />
<Previews.PricingPreview />
<Previews.ShadowsPreview />
</div>
);
}You can also enable them inside ThemePreviewPanel via its tabs.
Entry Points
tweakcn-ui(root, RSC-safe)exportThemePayload,importThemePayload,parseThemePayloadapplyThemeVars,computeCssVarMap- Types (
ThemeStatePayload,ThemeStyles, ...)
tweakcn-ui/serverServerThemePayload
tweakcn-ui/clientThemeEditor,ThemeProvider,useTheme,ThemeRoot,ThemeScript- UI building blocks:
ThemeControlPanel,ThemePreviewPanel,ColorPicker,SliderWithInput,FontPicker,HslAdjustmentControls useEditorStore,ClientThemePayload,Previews
Migration (>= 0.4.3)
If you previously imported client components from the root entry (e.g., import { ThemeEditor } from 'tweakcn-ui') and saw errors like (0, react__WEBPACK_IMPORTED_MODULE_0__.createContext) is not a function, switch those imports to the client entry:
- Before:
import { ThemeEditor, ThemeProvider, ThemeRoot, ThemeScript } from 'tweakcn-ui' - After:
import { ThemeEditor, ThemeProvider, ThemeRoot, ThemeScript } from 'tweakcn-ui/client'
And import server-only pieces from tweakcn-ui/server (e.g., ServerThemePayload).
API: Exports overview
Core
ThemeEditor(client) � full editor UIThemeProvider(client) � applies/updates CSS vars, loads fonts, and manages theme mode (skipGoogleFonts/fontTargets)ThemeRoot(client) � thin wrapper aroundThemeProvider; forwards font options downstreamThemeScript(client) � early CSS var + font hydration script (put in<head>)
Theme payload viewers
ServerThemePayload(server) – pretty-prints a providedThemeStatePayloadClientThemePayload(client) – live payload from the store
Building blocks
ThemeControlPanel(client)ThemePreviewPanel(client)ColorPicker(client)SliderWithInput(client)FontPicker(client)HslAdjustmentControls(client)
Store and hooks
useEditorStore– Zustand store withthemeStateuseTheme– provider context withtheme/setTheme/toggleThemeuseThemeExport,useThemeImport,useThemePersistence– helpers for IO
Note: the store and hooks above are client-only; import them from
tweakcn-ui/client.Serialization/runtime
exportThemePayload,importThemePayload,parseThemePayloadapplyThemeVars,computeCssVarMap
Previews namespace
Previews.CardsPreview,Previews.PricingPreview,Previews.ShadowsPreview
Types
- Re-exported from
./types(e.g.ThemeStatePayload,ThemeStyles, etc.)
- Re-exported from
Google Fonts integration
- The bundled font selector pulls families from Google Fonts. Provide your key anywhere you render the editor UI:
<ThemeEditor googleFontsApiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY} /><ThemeControlPanel googleFontsApiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY} /><FontPicker googleApiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY} />
- Without a key, the editor falls back to the built-in shortlist so the controls keep working, just without search over the full catalog.
- You can disable the Google Web Fonts fetch (for example if you generate your list server-side) via
useGoogleWebFontsApi={false}onFontPicker. ThemeScript,ThemeProvider, andThemeRootautomatically load Google CSS for any non-system font selected in the theme. SupplyskipGoogleFonts={["Inter", "Roboto"]}(or similar) when your app already imports those families vianext/fontor self-hosting:
<ThemeRoot skipGoogleFonts={["Inter"]}>
<ThemeScript skipGoogleFonts={["Inter"]} />
</ThemeRoot>Override the default typography targets (headings -> serif, body -> sans, etc.) with
fontTargets, e.g.{ serif: ["h1", "h2"], sans: ["body", ".prose p"] }.The current font payload is stored in
localStoragealongside CSS vars and exposed onwindow.__tweakcnSelectedFontsfor debugging.To inspect the derived font import URLs and flags, render the debug payload component:
import { ClientFontsPayload } from 'tweakcn-ui/client';
<ClientFontsPayload />Build & publish
cd tweakcn-ui
npm install # installs dev deps (tsup, typescript)
npm run build
npm publish --access publicStyling + Tailwind
This kit uses Tailwind-style tokens (e.g. bg-background, text-foreground, etc.). Include Tailwind v4 or set CSS variables yourself. ThemeScript/ThemeProvider apply CSS vars on :root.
shadcn/ui + Radix
The internal UI primitives are implemented using shadcn/ui patterns powered by Radix UI and cmdk:
- Buttons, Cards, Inputs, Labels, Tabs, Popover, Tooltip, Select, Switch, Slider, Separator, ScrollArea, Resizable, Command palette.
Notes for consumers:
- No CLI needed in your app; the components are already included in this package.
- Ensure Tailwind is configured (tokens like
bg-background,text-foreground,border, etc.). - Optional: if you want shadcn-style enter/exit animations, add the
tailwindcss-animateplugin to your Tailwind config. Without it, components still work but without those transitions. - If your Tailwind build does not scan
node_modules, consider safelisting common utility classes used by these components (optional). Example:
// tailwind.config.js (example)
module.exports = {
content: [
'./src/**/*.{ts,tsx}',
// optionally include the library if your setup purges aggressively
// './node_modules/tweakcn-ui/dist/**/*.{js,cjs}',
],
plugins: [require('tailwindcss-animate')], // optional
safelist: [
{ pattern: /(w-\[\d+rem\]|h-\d+|p-\d+|px-\d+|py-\d+|rounded-md|rounded-lg)/ },
{ pattern: /(animate-in|fade-in-0|zoom-in-95|slide-in-from-(top|bottom|left|right)-2)/ },
{ pattern: /(data-\[state=(open|closed|active|inactive)\]:.*)/ },
],
};Because this is a library, the UI classes are bundled in JS. Most apps already use a superset of these utilities, so safelisting is often unnecessary, but it can help in strict purge setups.
Notes
- Trimmed features: Auth, AI, save/share, Next.js-only pieces. You can build those on top using the exported stores and callbacks.
- Preview includes Typography + Color Palette by default to avoid heavy demo deps. You can add your own previews.
Attribution
Portions of this code are based on tweakcn (https://github.com/jnsahaj/tweakcn) licensed under Apache-2.0.
Simplest Setup
- Next/React app using Tailwind v4:
"use client";
import { ThemeRoot, ThemeScript, ThemeEditor } from 'tweakcn-ui/client';
export default function Page() {
return (
<html>
<head>
<ThemeScript />
</head>
<body>
<ThemeRoot>
<ThemeEditor googleFontsApiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY} />
</ThemeRoot>
</body>
</html>
);
}- Tailwind v3 (expects raw HSL in CSS vars):
"use client";
import { ThemeRoot, ThemeScript, ThemeEditor } from 'tweakcn-ui/client';
export default function Page() {
return (
<>
<ThemeScript />
<ThemeRoot tailwindVersion="3">
<ThemeEditor googleFontsApiKey={process.env.NEXT_PUBLIC_GOOGLE_FONTS_API_KEY} />
</ThemeRoot>
</>
);
}Notes:
ThemeRootwrapsThemeProviderand applies theme vars to:root.- It also persists a precomputed var map and the current font payload to
localStorageundertweakcn-css-varsfor faster early hydration used byThemeScript. - All internal buttons default to
type="button"to avoid form submits in admin UIs. - Supplying
googleFontsApiKeyonThemeEditorunlocks the full Google Fonts catalog; omit it only if the fallback list covers your needs or you provide your own picker.
Persisted themes and fonts
If you store a ThemeStatePayload per tenant, pass it to both ThemeScript and ThemeRoot so fonts persist across sessions without a flash of defaults:
import { ThemeRoot, ThemeScript } from "tweakcn-ui/client";
const payload = await loadTenantTheme(tenant); // ThemeStatePayload | null
<html lang="en">
<head>
<ThemeScript
initialPayload={payload}
localStorageKey={`tweakcn-css-vars:${tenant}`}
skipGoogleFonts={SKIP_GOOGLE_FONTS}
/>
</head>
<body>
<ThemeRoot
initialThemePayload={payload}
localStorageKey={`tweakcn-css-vars:${tenant}`}
skipGoogleFonts={SKIP_GOOGLE_FONTS}
>
{children}
</ThemeRoot>
</body>
</html>ThemeScript writes the payload into the persisted editor store (editor-storage) before hydration and primes Google fonts. ThemeRoot mirrors the same data into the Zustand store without polluting history so the UI rehydrates with the exact same fonts and CSS variables.
