Theme Customizer

A real-time theme customization panel with color presets, dark mode toggle, and style controls. Great for apps that offer personalization options.

Color PresetsDark ModeLive PreviewRadius Control

Installation

Terminal
ReactTailwind
TSX
npx @uiblox/cli add theme-customizer

Preview

Theme Customizer

Source Code

theme-customizer.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"use client"; import { useState } from "react"; import { cn } from "@/lib/utils"; const colorPresets = [ { name: "Purple", primary: "#8B5CF6", secondary: "#A78BFA" }, { name: "Blue", primary: "#3B82F6", secondary: "#60A5FA" }, { name: "Green", primary: "#10B981", secondary: "#34D399" }, { name: "Orange", primary: "#F97316", secondary: "#FB923C" }, { name: "Pink", primary: "#EC4899", secondary: "#F472B6" }, { name: "Cyan", primary: "#06B6D4", secondary: "#22D3EE" }, ]; const radiusOptions = ["none", "sm", "md", "lg", "full"]; export function ThemeCustomizer() { const [isOpen, setIsOpen] = useState(true); const [theme, setTheme] = useState({ mode: "light" as "light" | "dark", primaryColor: colorPresets[0], radius: "md", fontSize: 16, }); return ( <> {/* Toggle Button */} <button onClick={() => setIsOpen(!isOpen)} className="fixed right-4 bottom-4 p-3 bg-purple-600 text-white rounded-full shadow-lg hover:bg-purple-700 transition-colors z-50" > <PaletteIcon /> </button> {/* Panel */} <div className={cn( "fixed right-0 top-0 h-full w-80 bg-white dark:bg-slate-900 border-l border-slate-200 dark:border-slate-800 shadow-xl z-40 transition-transform duration-300", isOpen ? "translate-x-0" : "translate-x-full" )}> <div className="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800"> <h2 className="text-lg font-semibold text-slate-900 dark:text-white">Theme Customizer</h2> <button onClick={() => setIsOpen(false)} className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg"> <XIcon /> </button> </div> <div className="p-4 space-y-6 overflow-y-auto h-[calc(100%-4rem)]"> {/* Mode Toggle */} <div> <label className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 block">Mode</label> <div className="flex gap-2"> <button onClick={() => setTheme({ ...theme, mode: "light" })} className={cn( "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg border transition-colors", theme.mode === "light" ? "bg-purple-50 border-purple-500 text-purple-700" : "border-slate-200 dark:border-slate-700" )} > <SunIcon /> Light </button> <button onClick={() => setTheme({ ...theme, mode: "dark" })} className={cn( "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg border transition-colors", theme.mode === "dark" ? "bg-purple-50 border-purple-500 text-purple-700" : "border-slate-200 dark:border-slate-700" )} > <MoonIcon /> Dark </button> </div> </div> {/* Primary Color */} <div> <label className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 block">Primary Color</label> <div className="grid grid-cols-6 gap-2"> {colorPresets.map((color) => ( <button key={color.name} onClick={() => setTheme({ ...theme, primaryColor: color })} className={cn( "w-10 h-10 rounded-lg transition-transform hover:scale-110", theme.primaryColor.name === color.name && "ring-2 ring-offset-2 ring-slate-900 dark:ring-white" )} style={{ backgroundColor: color.primary }} title={color.name} /> ))} </div> </div> {/* Border Radius */} <div> <label className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 block">Border Radius</label> <div className="flex gap-1"> {radiusOptions.map((r) => ( <button key={r} onClick={() => setTheme({ ...theme, radius: r })} className={cn( "flex-1 px-3 py-2 text-sm rounded-lg border transition-colors", theme.radius === r ? "bg-purple-50 border-purple-500 text-purple-700" : "border-slate-200 dark:border-slate-700" )} > {r} </button> ))} </div> </div> {/* Font Size */} <div> <label className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 block"> Font Size: {theme.fontSize}px </label> <input type="range" min="12" max="20" value={theme.fontSize} onChange={(e) => setTheme({ ...theme, fontSize: parseInt(e.target.value) })} className="w-full accent-purple-600" /> </div> {/* Preview */} <div> <label className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-2 block">Preview</label> <div className="p-4 border border-slate-200 dark:border-slate-800 rounded-xl space-y-3"> <button className="w-full px-4 py-2 text-white font-medium transition-colors" style={{ backgroundColor: theme.primaryColor.primary, borderRadius: theme.radius === "none" ? 0 : theme.radius === "sm" ? 4 : theme.radius === "md" ? 8 : theme.radius === "lg" ? 12 : 9999, fontSize: theme.fontSize }} > Primary Button </button> <div className="p-3 border border-slate-200 dark:border-slate-700" style={{ borderRadius: theme.radius === "none" ? 0 : theme.radius === "sm" ? 4 : theme.radius === "md" ? 8 : theme.radius === "lg" ? 12 : 9999 }} > <p style={{ fontSize: theme.fontSize }} className="text-slate-700 dark:text-slate-300">Sample text</p> </div> </div> </div> {/* Reset Button */} <button className="w-full px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"> Reset to Defaults </button> </div> </div> </> ); } function PaletteIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /></svg>; } function XIcon() { return <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>; } function SunIcon() { return <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>; } function MoonIcon() { return <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>; }