Extending: Creating Custom Themes
Relevant source files
The following files were used as context for generating this wiki page:The Character Sheet Maker features a data-driven theming system that allows for extensive visual customization of the character sheets. This system is designed to be easily extensible, enabling developers to add new themes by defining configuration objects without altering the core rendering logic. Themes control fonts, colors, and the specific styling of each block type (e.g., headers, form boxes, dividers) using TailwindCSS utility classes.
This document provides a technical overview of the theme architecture, how themes are applied, and a step-by-step guide to creating and integrating a new custom theme into the application. The system is distinct from the application's global light/dark mode, which is managed separately and styles the surrounding UI, not the sheet content itself.
Theme Architecture
The entire theming system is defined within src/lib/themes.ts. It revolves around a central configuration object, THEMES, which acts as a registry for all available themes.
Sources: src/lib/themes.ts
Core Data Structures
The system is built upon two main TypeScript types: ThemeKey and ThemeConfig.
-
ThemeKey: A string literal union type that serves as a unique identifier for each theme.export type ThemeKey = "simple" | "medieval" | "cyberpunk";Sources: src/lib/themes.ts:1-1
-
ThemeConfig: An interface that defines the structure for a theme object. It contains all the styling information required to render a sheet.
Sources: src/lib/themes.ts:4-23
The relationship between these structures is visualized below.
This diagram shows that the THEMES object is a record indexed by a ThemeKey, where each key maps to a ThemeConfig object.
ThemeConfig Interface
The ThemeConfig interface is composed of four main properties that use TailwindCSS class strings to define the visual appearance.
| Property | Type | Description |
|---|---|---|
name | string | The display name of the theme, shown in the UI. |
fonts | object | Defines the font-family for heading and body text elements. |
colors | object | Defines global text colors for text, accent, and faint elements. |
blocks | object | Contains nested objects with TailwindCSS class strings for styling each specific block type (e.g., header, formBox). |
Sources: src/lib/themes.ts:4-23
The THEMES Registry
The THEMES constant is the central export of the module. It's a Record where each key is a ThemeKey and each value is a complete ThemeConfig object. The application currently defines three themes: simple, medieval, and cyberpunk.
export const THEMES: Record<ThemeKey, ThemeConfig> = {
// --- THEME 1: MODERN / SIMPLE ---
simple: {
name: "Modern Clean",
fonts: { /* ... */ },
colors: { /* ... */ },
blocks: { /* ... */ },
},
// --- MEDIEVAL / FANTASY ---
medieval: { /* ... */ },
// --- CYBERPUNK / SCI-FI ---
cyberpunk: { /* ... */ },
};
Sources: src/lib/themes.ts:25-121
Theme Application Flow
Applying a theme involves state management, UI interaction, and dynamic rendering. The process begins when a user selects a theme in the ToolsSidebar, which triggers a state update in the useSheetStore, causing the BlockRenderer components to re-render with the new styles.
State Management with Zustand
The currently active theme is managed globally via a Zustand store.
useSheetStore: This store contains aglobalStyleobject which holds the activethemekey.setThemeaction: This function is exposed by the store to update theglobalStyle.themevalue. When called, it updates the state, and all components subscribed to this state will re-render.
// src/store/useSheetStore.ts
interface SheetState {
// ...
setTheme: (theme: ThemeKey) => void;
globalStyle: {
theme: SheetStyle["theme"]; // Holds the current ThemeKey
// ...
};
}
export const useSheetStore = create<SheetState>()(
persist(
(set) => ({
// ...
globalStyle: {
theme: "simple", // Default theme
// ...
},
setTheme: (theme) =>
set((state) => ({
globalStyle: { ...state.globalStyle, theme },
})),
// ...
}),
)
);
Sources: src/store/useSheetStore.ts:43-46, src/store/useSheetStore.ts:60-63, src/store/useSheetStore.ts:74-77
UI Integration
The ToolsSidebar component is responsible for presenting the theme options to the user.
- It imports the
THEMESobject and theuseSheetStore. - It iterates over the keys of the
THEMESobject to render a button for each available theme. - The
onClickhandler for each button calls thesetThemeaction from the store with the correspondingthemeKey.
// src/components/layout/ToolsSidebar.tsx
export const ToolsSidebar = ({ isOpen, toggle }: SidebarProps) => {
const { setTheme, globalStyle } = useSheetStore();
return (
// ...
{(Object.keys(THEMES) as ThemeKey[]).map((themeKey) => (
<Button
key={themeKey}
variant={globalStyle.theme === themeKey ? "default" : "outline"}
onClick={() => setTheme(themeKey)}
// ...
>
{isOpen ? THEMES[themeKey].name : /* ... */}
</Button>
))}
// ...
);
};
Sources: src/components/layout/ToolsSidebar.tsx:15, src/components/layout/ToolsSidebar.tsx:98-116
Rendering with Theme Styles
The BlockRenderer component consumes the active theme from the store and applies the styles to the individual blocks on the canvas.
- It gets the
globalStyle.themekey fromuseSheetStore. - It uses this key to look up the full
ThemeConfigobject from theTHEMESregistry. - It then accesses the properties of this
ThemeConfigobject (e.g.,currentTheme.blocks.header.h1,currentTheme.colors.text) and applies them asclassNameorstyleprops to the rendered HTML elements.
The following sequence diagram illustrates the complete flow:
How to Create a New Theme
Adding a new theme is a straightforward process that primarily involves editing src/lib/themes.ts and src/index.css.
This flowchart shows the steps to create and add a new theme.
Step 1: Import Fonts
If your theme uses custom Google Fonts, import them at the top of src/index.css. Follow the existing format.
/* src/index.css */
@import url('https://fonts.googleapis.com/css2?family=Your+New+Font&display=swap'); /* Your New Theme */
@import url('https://fonts.googleapis.com/css2?family=Cinzel...&display=swap'); /* Medieval */
/* ... */
Sources: src/index.css:2-4
Step 2: Define a New ThemeKey
Open src/lib/themes.ts and add a unique key for your new theme to the ThemeKey type definition.
// src/lib/themes.ts
export type ThemeKey = "simple" | "medieval" | "cyberpunk" | "your-new-theme";
Sources: src/lib/themes.ts:1-1
Step 3: Create the ThemeConfig Object
In the same file, add a new entry to the THEMES constant. The key must match the ThemeKey you just added. Populate the name, fonts, colors, and blocks properties with the desired font-family strings and TailwindCSS utility classes.
// src/lib/themes.ts
export const THEMES: Record<ThemeKey, ThemeConfig> = {
// ... existing themes
// --- YOUR NEW THEME ---
"your-new-theme": {
name: "Your Theme Name",
fonts: {
heading: '"Your New Font", sans-serif',
body: '"Another Font", serif',
},
colors: {
text: "text-slate-800",
accent: "text-teal-500",
faint: "text-slate-400",
},
blocks: {
header: {
h1: "font-bold text-5xl border-b-4 border-teal-500/50",
h2: "font-semibold text-3xl text-slate-700",
},
formBox: {
label: "text-xs font-medium text-slate-500",
input: "bg-white border border-slate-300 rounded-md",
},
container: "border border-dashed border-slate-200 bg-slate-50",
divider: "bg-slate-300 h-[1px]",
shape: "bg-slate-100 border border-slate-300",
},
},
};
Sources: src/lib/themes.ts:25-121
Once these changes are saved, the application will automatically pick up the new theme and display it as an option in the ToolsSidebar. No further code changes are required.
Application UI Theme (Light/Dark Mode)
It is important to distinguish the sheet's content themes from the application's overall UI theme (light/dark mode). The UI theme is controlled by the useTheme hook, which toggles a dark class on the root <html> element and persists the choice in localStorage. This system is independent and does not affect the ThemeConfig applied to the character sheet canvas.
Sources: src/hooks/useTheme.ts, src/components/layout/TopBar.tsx:14, src/components/layout/TopBar.tsx:68-74