Theming System
TinyKit Pro features a comprehensive theming system with database-driven customization, server-side CSS injection for zero flicker, and automatic light/dark ...
TinyKit Pro features a comprehensive theming system with database-driven customization, server-side CSS injection for zero flicker, and automatic light/dark mode support.
Features
- 🎨 Pre-built Themes - 40+ professionally designed themes from shadcn/ui registry
- ⚡ Zero Flicker Loading - Server-side CSS injection eliminates theme flicker on page load
- 🌓 Automatic Dark Mode - All themes include light and dark mode variants
- 💾 Database-Driven - Themes stored in database for instant updates without deployment
- 📧 Email Template Theming - Email templates automatically use database theme colors
- 🎯 Semantic Colors - Consistent color system across all components
- 🔄 Instant Updates - Theme changes go live immediately without code deployment
How It Works
Architecture Overview
The theming system uses a three-layer approach:
- Static Fallback (
src/app/styles/theme.css) - Default theme if database is empty - Database Storage (
siteSettings.currentThemeConfig) - Active theme configuration - Server-Side Injection - Theme CSS injected into HTML
<head>before rendering
Zero-Flicker Implementation
Traditional Theme Loading (with flicker):
1. Browser receives HTML with static theme
2. Page renders with default colors
3. JavaScript fetches database theme
4. CSS variables updated
👎 User sees color flashTinyKit Pro (zero flicker):
1. Server fetches theme from database
2. Server injects CSS into <head>
3. Browser receives HTML with theme CSS
4. Page renders with correct colors
✅ Perfect user experienceTechnical Components
1. Database Query (convex/siteSettings/public/queries.ts)
export const getThemeCSSVars = query({
args: {},
handler: async (ctx) => {
const settings = await ctx.db.query("siteSettings").first();
if (!settings?.currentThemeConfig?.cssVars) {
return null; // Falls back to static theme.css
}
return {
theme: settings.currentThemeConfig.cssVars.theme ?? {},
light: settings.currentThemeConfig.cssVars.light,
dark: settings.currentThemeConfig.cssVars.dark,
};
},
});2. Server Component (src/components/ThemeStyleInjector.tsx)
export function ThemeStyleInjector({ cssVars }: ThemeStyleInjectorProps) {
if (!cssVars) {
return null; // No database theme - use static fallback
}
const { theme, light, dark } = cssVars;
// Generate CSS content
const css = `
:root {
${generateCSSDeclarations(light)}
}
.dark {
${generateCSSDeclarations(dark)}
}
`;
return (
<style
id="theme-css-vars"
dangerouslySetInnerHTML={{ __html: css }}
suppressHydrationWarning
/>
);
}3. Root Layout Integration (src/app/layout.tsx)
export default async function RootLayout({ children }) {
// Fetch theme CSS server-side
const themeCSSResult = await preloadQuery(
api.siteSettings.public.queries.getThemeCSSVars,
);
const themeCSSVars = themeCSSResult._valueJSON;
return (
<html lang="en" suppressHydrationWarning>
<head>
{/* Inject theme CSS before page renders */}
<ThemeStyleInjector cssVars={themeCSSVars} />
</head>
<body>{children}</body>
</html>
);
}Admin Theme Management
Applying Themes
- Navigate to Admin Panel: Go to
/admin/theme - Browse Themes: Preview 40+ pre-built themes from shadcn/ui
- Select Theme: Click on any theme to preview
- Apply Theme: Click "Apply Theme" button
- Instant Live: Theme goes live immediately across the entire site
Theme Configuration
Each theme includes:
{
name: "modern-minimal",
title: "Modern Minimal",
description: "A clean, minimal design with focus on content",
cssVars: {
theme: {
// Typography and spacing
"font-sans": "var(--font-sans)",
"radius": "0.5rem",
},
light: {
// Light mode colors (OKLCH format)
"background": "oklch(0.99 0.01 84.57)",
"foreground": "oklch(0.14 0.01 84.57)",
"primary": "oklch(0.42 0.15 264.05)",
// ... all semantic colors
},
dark: {
// Dark mode colors (OKLCH format)
"background": "oklch(0.09 0.01 84.57)",
"foreground": "oklch(0.95 0.01 84.57)",
"primary": "oklch(0.68 0.15 264.05)",
// ... all semantic colors
}
}
}Semantic Color System
TinyKit uses a semantic color system that automatically adapts to light/dark themes:
Core Semantic Colors
background/foreground- Base page colorscard/card-foreground- Card and panel backgroundsprimary/primary-foreground- Primary action colorssecondary/secondary-foreground- Secondary elementsmuted/muted-foreground- Subdued text and backgroundsaccent/accent-foreground- Accent elementsdestructive/destructive-foreground- Error statesborder- Border colorsinput- Form input backgroundsring- Focus ring colors
Usage in Components
// Always use semantic colors
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">
Primary Action
</Button>
<Card className="bg-card border text-card-foreground">
<p className="text-muted-foreground">Subdued content</p>
</Card>
// Never hardcode colors
// ❌ bg-blue-500, text-gray-600, border-slate-200Email Template Theming
Email templates automatically use the active database theme:
How It Works
- Admin applies theme in
/admin/theme - Theme colors converted from OKLCH to hex format
- Stored in
siteSettings.emailThemeColors - Email templates automatically use these colors
Email Theme Selection
Choose which mode to use for emails in /admin/branding:
- Light Mode - Use light theme colors in emails (default)
- Dark Mode - Use dark theme colors in emails
Email Color Access
// In email templates (emails/*.tsx)
import { getEmailTheme } from "@/lib/email-theme";
const theme = await getEmailTheme();
<div style={{ backgroundColor: theme.background, color: theme.foreground }}>
<h1 style={{ color: theme.primary }}>Welcome!</h1>
<p style={{ color: theme.mutedForeground }}>Thanks for signing up.</p>
</div>Light/Dark Mode Toggle
Users can toggle between light and dark modes using the theme switcher in the header:
// Implemented in ThemeProvider
const [theme, setTheme] = useState<"light" | "dark" | "system">("system");
// Toggle function
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};The toggle works seamlessly with database themes because:
- Light mode uses
:rootCSS variables - Dark mode uses
.darkCSS variables - Both are injected server-side from the database
Performance
Server-Side Injection Benefits
- Zero Flicker: CSS available before page paint
- Minimal Performance Impact: ~5-10ms server query
- Small HTML Increase: ~2-4KB for inline CSS
- No Client JavaScript: Theme loads without JS
- Browser Caching: HTML (including CSS) is cached
Measurements
Server-side query time: 5-10ms
HTML size increase: 2-4KB
Client-side JavaScript: 0 bytes
Render blocking: None
First Contentful Paint: Correct colors from startCaching Optimization
To minimize database calls, the root layout uses Next.js Route Segment Caching:
// In src/app/layout.tsx
export const revalidate = 60; // Cache for 60 secondsPerformance Impact:
- Without caching: DB call on every page request (~1000 calls/min for 1000 requests/min)
- With caching: DB call once per 60 seconds (~1 call/min)
- Reduction: 99.9% fewer database calls
Tradeoffs:
- ✅ Massive reduction in DB queries - Reduces Convex usage and cost
- ✅ Zero flicker maintained - Server-side injection still works perfectly
- ✅ Faster page loads - Cached HTML served instantly
- ⚠️ Theme update delay - Admin theme changes take up to 60 seconds to propagate
How it works:
- First request: Next.js fetches theme from database and renders layout
- Next 60 seconds: Cached HTML (with theme CSS) served to all users
- After 60 seconds: Cache expires, Next.js re-fetches theme and regenerates layout
- Process repeats
Customizing cache duration:
// Faster theme propagation (more DB calls)
export const revalidate = 10; // 10 seconds
// Longer caching (fewer DB calls)
export const revalidate = 300; // 5 minutesRecommendation: 60 seconds is a good balance between performance and theme update responsiveness.
Fallback Strategy
The system gracefully handles missing database themes:
// If database theme is null
if (!cssVars) {
return null; // ThemeStyleInjector returns null
}
// Browser uses static theme.css instead
// No errors, perfect fallbackFallback Chain
- Primary: Database theme (server-injected)
- Fallback: Static
theme.cssfile - Emergency: Browser default styles
Customization
Creating Custom Themes
- Start with existing theme: Choose a base theme
- Modify colors: Use OKLCH color format
- Save to database: Use admin panel or API
- Test both modes: Verify light and dark variants
Color Format (OKLCH)
OKLCH provides better color perception:
/* OKLCH Format: oklch(lightness chroma hue) */
--primary: oklch(0.42 0.15 264.05);
/* ↑ ↑ ↑
42% 15% 264° hue
light chroma (saturation)
*/Benefits over RGB/HSL:
- Perceptually uniform
- Better color manipulation
- More vibrant colors
- Future-proof (CSS Color Module Level 4)
Best Practices
Component Development
- Always use semantic colors - Never hardcode Tailwind colors
- Test both modes - Verify light and dark themes
- Use opacity modifiers -
bg-primary/90for hover states - Check contrast - Ensure text is readable in both modes
Theme Selection
- Match brand identity - Choose theme that fits your brand
- Consider accessibility - High contrast for readability
- Test with content - Preview with real content
- Mobile testing - Verify on different devices
Performance
- Minimal theme changes - Only update when necessary
- Cache consideration - Browser caches include theme CSS
- Monitor bundle size - Theme CSS is small (~2-4KB)
Troubleshooting
Theme Not Appearing
Symptoms: Old theme still showing after applying new one
Solutions:
- Hard refresh (Cmd/Ctrl+Shift+R)
- Clear browser cache
- Check database:
siteSettings.currentThemeConfig.cssVarsexists - Verify server logs for errors
Theme Flicker on Load
Symptoms: Colors flash from default to database theme
Causes:
- ThemeStyleInjector not in
<head> - CSS load order incorrect
- Client-side theme loading interfering
Solutions:
- Ensure
<ThemeStyleInjector>inside<head>tag - Check import order in
layout.tsx - Remove client-side CSS variable updates
Hydration Warnings
Symptoms: React hydration mismatch warnings
Solutions:
- Add
suppressHydrationWarningto<style>tag - Ensure no client components modify CSS variables on mount
See Also
- Database Theme Implementation Guide - Technical deep dive
- Site Branding - Logo, colors, and brand settings
- Email Templates - Email template theming
- Admin Dashboard - Admin panel access
Site Banner System
TinyKit Pro includes a comprehensive site banner system for displaying promotional announcements, maintenance notices, and important updates across the platf...
llms.txt Endpoint
TinyKit Pro includes an llms.txt endpoint that provides structured site information optimized for Language Model consumption, following the llms.txt specific...