Docs Theme Development

Theme & Layout Development

Create custom visual themes and page layouts for Contento sites.

How Themes Work

Contento themes are CSS variable collections stored as JSON. When a theme is activated, its variables are injected as CSS custom properties on the :root element, overriding the default design tokens. This means themes control colors, typography, spacing, and more — without touching HTML templates.

The theme system uses a layered approach:

  1. Base design tokens — Defined in app.css, provide default values for all CSS variables
  2. Theme overrides — The active theme's cssVariables JSON overrides specific tokens
  3. Dark mode — The .dark class on <html> swaps to dark variants automatically

Theme Data Model

Each theme is stored in the themes database table with these fields:

FieldTypeDescription
idUUIDAuto-generated primary key
namevarchar(400)Display name (e.g. "Wabi Sabi")
slugvarchar(200)URL-safe identifier, unique
descriptiontextTheme description
versionvarchar(100)Semantic version
authorvarchar(200)Theme author name
css_variablesjsonbJSON object of CSS variable overrides
base_layout_idUUID (FK)Optional link to a default layout
thumbnail_urlvarchar(500)Theme preview image URL
is_activebooleanOnly one theme can be active at a time
settingsjsonbAdditional theme configuration
Single active theme: Only one theme can be active at a time. When you activate a theme, the previously active theme is automatically deactivated.

CSS Variables Reference

The design system uses semantic CSS custom properties. Your theme's cssVariables JSON should use these keys:

Core Colors

VariableDefault (Light)Description
--color-ink #1A1A1APrimary text color
--color-paper #FAFAF8Page background
--color-snow #FFFFFFCard/surface background
--color-stone #6B6B6BSecondary/muted text
--color-mist #E5E5E3Borders, dividers
--color-cloud #F5F5F3Subtle backgrounds, hover states

Accent Colors

VariableDefaultDescription
--color-accent #3D5A80Primary accent (links, buttons, focus)
--color-accent-hover #2C4A6EAccent hover state
--color-accent-light #E8EEF4Light accent for backgrounds

Typography

VariableDefaultDescription
--font-bodyNoto Sans JP, sans-serifBody text font family
--font-headingNoto Serif JP, serifHeading font family
--font-monoIBM Plex Mono, monospaceCode font family

Spacing & Layout

VariableDefaultDescription
--radius6pxDefault border radius
--shadow0 1px 3px rgba(0,0,0,0.06)Default box shadow
--max-width72remMaximum page content width

Creating a Theme

Via the Admin UI

  1. Navigate to Admin → Themes
  2. Click "Create Theme"
  3. Fill in the name, slug, description, and author
  4. Enter your CSS variables as a JSON object
  5. Save the theme
  6. Click "Activate" to make it live

Via the API

curl -X POST https://your-site.com/api/v1/themes \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Midnight",
    "slug": "midnight",
    "description": "A dark, elegant theme with amber accents",
    "version": "1.0.0",
    "author": "Your Name",
    "cssVariables": "{\"--color-ink\":\"#E5E5E3\",\"--color-paper\":\"#0F0F0F\",\"--color-snow\":\"#1A1A1A\",\"--color-stone\":\"#8A8A8A\",\"--color-mist\":\"#2A2A2A\",\"--color-cloud\":\"#1F1F1F\",\"--color-accent\":\"#F59E0B\",\"--color-accent-hover\":\"#D97706\",\"--color-accent-light\":\"#292211\",\"--font-body\":\"Inter, sans-serif\",\"--font-heading\":\"Playfair Display, serif\"}"
  }'

# Activate the theme
curl -X POST https://your-site.com/api/v1/themes/{id}/activate \
  -H "Authorization: Bearer <token>"

Via Database Seeding

For themes that ship with the CMS, add them to DatabaseSeeder.cs:

var theme = new Theme
{
    Name = "Midnight",
    Slug = "midnight",
    Description = "Dark elegance with amber accents",
    Version = "1.0.0",
    Author = "Contento",
    IsActive = false,
    CssVariables = JsonSerializer.Serialize(new Dictionary<string, string>
    {
        ["--color-ink"] = "#E5E5E3",
        ["--color-paper"] = "#0F0F0F",
        ["--color-accent"] = "#F59E0B",
        ["--font-body"] = "Inter, sans-serif",
        ["--font-heading"] = "Playfair Display, serif",
        // ... more variables
    })
};

Built-in Themes

Contento ships with 8 built-in themes covering a range of styles:

ThemeStyleBody FontAccentBackground
Wabi Sabi Japanese minimalism Noto Sans JP #3D5A80 #FAFAF8
Mono Monochrome developer Noto Sans JP #1A1A1A #FFFFFF
Sakura Soft cherry blossom Noto Sans JP #C084A0 #FFF5F7
Ink Modern editorial Playfair Display #E63946 #FFFFFF
Grove Classic literary Merriweather #2D6A4F #FBF8F1
Neon Playful creative DM Sans #7C3AED #FAF5FF
Folio Minimal reading Crimson Pro #0E7490 #FEFDFB
Dusk Dark elegance Lora #F59E0B #1A1A2E

Dark Mode

Contento supports automatic dark mode through the CSS .dark class. When dark mode is toggled (via the sun/moon button in the header), the <html> element gets a .dark class and the app.css swaps to dark variants:

/* Light mode (default) */
:root {
    --color-ink: #1A1A1A;
    --color-paper: #FAFAF8;
    --color-snow: #FFFFFF;
}

/* Dark mode override */
.dark {
    --color-ink: #E5E5E3;
    --color-paper: #121212;
    --color-snow: #1E1E1E;
    --color-stone: #9A9A9A;
    --color-mist: #2E2E2E;
    --color-cloud: #1A1A1A;
}

There is also a @media (prefers-color-scheme: dark) fallback that activates when the user's OS is in dark mode and no explicit class has been set:

@media (prefers-color-scheme: dark) {
    html:not(.light) {
        --color-ink: #E5E5E3;
        --color-paper: #121212;
        /* ... same as .dark */
    }
}
Tip: If your theme is designed for dark backgrounds (like the "Dusk" theme), users can still toggle dark mode. Make sure your theme's accent colors have enough contrast against both light and dark backgrounds.

Layouts

Layouts define the structural arrangement of content regions on a page. Each layout is stored in the layouts table:

FieldTypeDescription
idUUIDAuto-generated primary key
site_idUUID (FK)The site this layout belongs to
namevarchar(400)Display name
slugvarchar(200)URL-safe identifier, unique per site
descriptiontextLayout description
structurejsonbJSON defining regions and grid layout
head_contenttextCustom HTML for <head>
custom_csstextCustom CSS for this layout
custom_jstextCustom JavaScript for this layout
is_defaultbooleanWhether this is the default layout

The Structure JSON

The structure field defines how content regions are arranged. Here are the built-in layout structures:

Standard (Default)

{
    "regions": ["header", "main", "sidebar", "footer"],
    "grid": "8fr 4fr",
    "maxWidth": "1200px"
}

Full Width

{
    "regions": ["header", "body", "footer"],
    "grid": "1fr",
    "maxWidth": "860px"
}

Magazine

{
    "regions": ["hero", "grid", "sidebar", "footer"],
    "grid": "8fr 4fr",
    "heroColumns": "12",
    "maxWidth": "1400px"
}

Minimal

{
    "regions": ["body"],
    "grid": "1fr",
    "maxWidth": "680px",
    "centered": true
}

Creating Layouts

Via the Admin UI

  1. Navigate to Admin → Layouts
  2. Click "Create Layout"
  3. Enter the name, slug, and description
  4. Define the structure JSON
  5. Optionally add custom CSS, JS, or head content
  6. Save the layout

Via the API

curl -X POST https://your-site.com/api/v1/layouts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Landing Page",
    "slug": "landing-page",
    "description": "Wide layout with hero and feature grid sections",
    "isDefault": false,
    "structure": "{\"regions\":[\"hero\",\"features\",\"cta\",\"footer\"],\"grid\":\"1fr\",\"maxWidth\":\"1400px\"}",
    "customCss": ".hero { min-height: 60vh; }",
    "customJs": null,
    "headContent": null
  }'

# Set as default layout
curl -X POST https://your-site.com/api/v1/layouts/{id}/set-default \
  -H "Authorization: Bearer <token>"

Themes & Layouts Together

Themes and layouts are independent but complementary:

  • Themes control how things look (colors, fonts, shadows)
  • Layouts control where things go (regions, grid, max width)

A theme can optionally specify a base_layout_id to suggest a default layout pairing. However, users can always override the layout for individual posts.

Headless Mode

When using Contento in headless mode, the Content Delivery API returns both theme and layout data. Your frontend can use this to apply the correct styling:

const { data } = await fetch('/api/v1/content/site').then(r => r.json());

// Apply theme CSS variables
const vars = JSON.parse(data.theme.cssVariables);
for (const [key, value] of Object.entries(vars)) {
    document.documentElement.style.setProperty(key, value);
}

// Use layout structure for page rendering
const layout = JSON.parse(data.defaultLayout.structure);
console.log(layout.regions);  // ["header", "main", "sidebar", "footer"]
console.log(layout.maxWidth); // "1200px"

API Reference

Theme Endpoints

MethodEndpointAuthDescription
GET/api/v1/content/themesNoList all themes (public)
GET/api/v1/themesYesList all themes (management)
GET/api/v1/themes/{id}YesGet theme by ID
POST/api/v1/themesYesCreate a new theme
PUT/api/v1/themes/{id}YesUpdate a theme
DELETE/api/v1/themes/{id}YesDelete a theme
POST/api/v1/themes/{id}/activateYesActivate a theme

Layout Endpoints

MethodEndpointAuthDescription
GET/api/v1/content/layoutsNoList all layouts (public)
GET/api/v1/layoutsYesList all layouts (management)
GET/api/v1/layouts/{id}YesGet layout with components
POST/api/v1/layoutsYesCreate a new layout
PUT/api/v1/layouts/{id}YesUpdate a layout
DELETE/api/v1/layouts/{id}YesDelete a layout (not the default)
POST/api/v1/layouts/{id}/set-defaultYesSet as default layout

Tips & Best Practices

  • Test on both light and dark mode — Ensure your accent colors have sufficient contrast (4.5:1 minimum for WCAG AA) against both your theme's background and the dark mode background
  • Use web-safe font stacks — Always include fallback fonts: "Playfair Display, Georgia, serif"
  • Keep CSS variables minimal — Only override the variables you want to change. Unspecified variables inherit from the base design system
  • Use the admin preview — The Themes admin page shows a live color swatch grid so you can preview your theme before activating
  • Version your themes — Use semantic versioning so administrators know when a theme has been updated
  • Test with real content — Create a few posts with images, code blocks, and headings to verify your theme looks good across all content types
  • Consider loading performance — If using Google Fonts, keep the total number of font weights low (2-3 per family) to minimize page load time