TinyKit Pro Docs

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:

  1. Static Fallback (src/app/styles/theme.css) - Default theme if database is empty
  2. Database Storage (siteSettings.currentThemeConfig) - Active theme configuration
  3. 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 flash

TinyKit 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 experience

Technical 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

  1. Navigate to Admin Panel: Go to /admin/theme
  2. Browse Themes: Preview 40+ pre-built themes from shadcn/ui
  3. Select Theme: Click on any theme to preview
  4. Apply Theme: Click "Apply Theme" button
  5. 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 colors
  • card / card-foreground - Card and panel backgrounds
  • primary / primary-foreground - Primary action colors
  • secondary / secondary-foreground - Secondary elements
  • muted / muted-foreground - Subdued text and backgrounds
  • accent / accent-foreground - Accent elements
  • destructive / destructive-foreground - Error states
  • border - Border colors
  • input - Form input backgrounds
  • ring - 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-200

Email Template Theming

Email templates automatically use the active database theme:

How It Works

  1. Admin applies theme in /admin/theme
  2. Theme colors converted from OKLCH to hex format
  3. Stored in siteSettings.emailThemeColors
  4. 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 :root CSS variables
  • Dark mode uses .dark CSS 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 start

Caching 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 seconds

Performance 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:

  1. First request: Next.js fetches theme from database and renders layout
  2. Next 60 seconds: Cached HTML (with theme CSS) served to all users
  3. After 60 seconds: Cache expires, Next.js re-fetches theme and regenerates layout
  4. 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 minutes

Recommendation: 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 fallback

Fallback Chain

  1. Primary: Database theme (server-injected)
  2. Fallback: Static theme.css file
  3. Emergency: Browser default styles

Customization

Creating Custom Themes

  1. Start with existing theme: Choose a base theme
  2. Modify colors: Use OKLCH color format
  3. Save to database: Use admin panel or API
  4. 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

  1. Always use semantic colors - Never hardcode Tailwind colors
  2. Test both modes - Verify light and dark themes
  3. Use opacity modifiers - bg-primary/90 for hover states
  4. Check contrast - Ensure text is readable in both modes

Theme Selection

  1. Match brand identity - Choose theme that fits your brand
  2. Consider accessibility - High contrast for readability
  3. Test with content - Preview with real content
  4. Mobile testing - Verify on different devices

Performance

  1. Minimal theme changes - Only update when necessary
  2. Cache consideration - Browser caches include theme CSS
  3. Monitor bundle size - Theme CSS is small (~2-4KB)

Troubleshooting

Theme Not Appearing

Symptoms: Old theme still showing after applying new one

Solutions:

  1. Hard refresh (Cmd/Ctrl+Shift+R)
  2. Clear browser cache
  3. Check database: siteSettings.currentThemeConfig.cssVars exists
  4. Verify server logs for errors

Theme Flicker on Load

Symptoms: Colors flash from default to database theme

Causes:

  1. ThemeStyleInjector not in <head>
  2. CSS load order incorrect
  3. Client-side theme loading interfering

Solutions:

  1. Ensure <ThemeStyleInjector> inside <head> tag
  2. Check import order in layout.tsx
  3. Remove client-side CSS variable updates

Hydration Warnings

Symptoms: React hydration mismatch warnings

Solutions:

  1. Add suppressHydrationWarning to <style> tag
  2. Ensure no client components modify CSS variables on mount

See Also

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro