Skip to content

Documentation

Design System & Admin Theme Settings

Design System workspace: multi-theme management, density, visual styles, personal overrides, custom CSS, and live component preview.

7/30/2026
18 min read Edit on GitHub

SveltyCMS’s admin theme system lets you create, switch, and customize multiple named themes, each with its own density, visual style, features, and custom CSS. Changes preview instantly before saving.

Sole route: /config/design-system (Config → Design System). There is no separate Appearance page.

Deep links (query ?tab=):

Tab URL Audience
My Overrides /config/design-system?tab=overrides All users (linked from /user Settings)
Live Preview /config/design-system?tab=preview Component catalog + token readout
Themes / Presets / Layout / Style / Features / Advanced ?tab=themes etc. Workspace admins

🎨 Multi-Theme Management

SveltyCMS supports multiple admin themes stored in the database. Each theme stores its own density, card variant, feature toggles, and custom CSS independently.

Themes Tab

Navigate to Config → Design System → Themes.

Action Description
Activate Switch to a different theme. Applies all settings instantly.
Clone Copy an existing theme as a starting point for a new variant.
Delete Remove an unused theme. Cannot delete the active or default theme.

The active theme shows a green Active badge. The system default theme (created during setup) shows a Default badge and cannot be deleted.

Creating a Theme

  1. Navigate to Config → Design System → Themes.
  2. Enter a name (e.g., “Midnight Blue”, “High Contrast Reader”).
  3. Click Create.

New themes start with the current density, variant, and features as defaults. Switch to other tabs to customize before saving.

Tip

Clone a theme to experiment with variations without losing your working settings. If you don’t like the clone, delete it.


📐 Layout & Density

The Layout & Density tab controls spacing, sizing, and overall compactness of the admin interface.

Density Sidebar Width Header Height Best For
Compact 200px 48px Developers, data-heavy table views, translators
Cozy 240px 64px Standard content editing (default)
Spacious 300px 72px Distraction-free writing, managers, accessibility needs

Changing density updates the sidebar width, card padding, button sizes, and font sizing across all 37+ native UI components in real time.

Note

Density only affects the admin panel. Your public-facing website uses its own theme system and is not impacted.


🖌️ Visual Style

The Visual Style tab controls card borders and shadows.

Variant Border Shadow Effect
Flat None None Minimal, modern, distraction-free
Bordered 1px Subtle (1-3px) Clear card separation (default)
Elevated 1px Prominent Cards “pop” with depth perception

These affect every card, modal, and content container in the admin panel. The preview cards in the settings page update in real time so you can see the effect before saving.


🔧 Theme Features

The Features tab provides five toggles plus layout region controls:

Feature Effect
Sticky Action Bar Save/Delete buttons stick to the bottom on scroll in content forms
Collapsible Sidebar Users can collapse the left navigation sidebar for more screen space
Branded Login Shows tenant logo and custom colors on the login page
High Contrast Mode Enforces WCAG AAA contrast ratios across the entire admin panel
Reduced Motion Disables all animations and transitions globally
Collections Position Moves the Collections tree to the left sidebar, right sidebar, or both

Layout Region Repositioning

Each theme can reposition the Collections tree independently of visual style:

Position Effect
Left Collections in the left navigation sidebar (default)
Right Collections in the right sidebar, always visible when sidebar is open
Both Collections appear in both sidebars simultaneously

This enables radically different workspace layouts per theme. For example, a “Developer Workbench” theme could move Collections to the right sidebar, freeing the left sidebar for pinned favorites and system tools — while keeping the entry inspector visible during editing.

// Theme A: Traditional layout (Collections on left)
features: {
  layoutRegions: {
    collections: "left";
  }
}

// Theme B: Developer workspace (Collections on right, compact density)
features: {
  layoutRegions: {
    collections: "right";
  }
}
density: "compact";
Important

High Contrast Mode and Reduced Motion are accessibility features. Enable them to meet WCAG 3.0 Functional Performance standards for users with visual or vestibular sensitivities.

Sticky Action Bar

When Sticky Action Bar is enabled, any page can make its action buttons stick to the bottom of the viewport on scroll. Wrap your buttons in the <StickyActions> component:

<script>
  import StickyActions from "@components/ui/sticky-actions.svelte";
</script>

<StickyActions>
  <button onclick={save} aria-keyshortcuts="Mod+S">Save Changes</button>
  <button onclick={reset}>Reset</button>
</StickyActions>

The buttons render both in their original position AND in the sticky bar at the bottom of the screen — no duplication in your code. The bar uses role="toolbar", aria-label="Page actions", and aria-live="polite" for screen reader compatibility.

Tip

The sticky bar height adapts to density — 44px in compact, 56px in cozy, 64px in spacious. Toggle the feature per-theme in Design System → Features.


⌨️ Advanced Customization

Custom CSS

The Advanced tab provides a code editor where you can inject custom CSS into the admin panel.

/* Example: Change the active tab indicator */
[data-admin-theme] .active-tab-indicator {
  border-color: oklch(65% 0.2 260deg);
  border-width: 3px;
}

Custom CSS is sanitized server-side before storage. Dangerous constructs (url(), @import, javascript:, <script>, HTML tags) are automatically stripped.

Caution

Custom CSS can override theme settings. If your density or variant changes don’t appear to take effect, check your custom CSS for conflicting rules.

Palette studio (Design System → Palette & Import)

A lightweight brand editor (Skeleton-inspired) lives on the Palette & Import tab:

Control Purpose
Primary / Tertiary / Surface Core seeds (color picker + hex)
More colors Secondary, success, warning, error
Live apply Pushes expanded shades into theme CSS immediately
Expanded shades Shows 50 / 500 / 950 previews
Apply / Reset / Clear Commit, restore Corporate defaults, or remove only the palette CSS block

Seeds expand via color-mix into full scales and rebind --admin-bg-* (same path as theme JSON import). Manual Advanced → Custom CSS outside the sveltycms-palette-start/end markers is preserved. Click Save Theme to persist.

For a full visual generator (typography, edges, etc.), use Skeleton Theme Create and import the JSON below.

Export / Import

  • Export Theme JSON: Downloads the current theme (density, variant, features, custom CSS) as a portable JSON file.
  • Import: Paste SveltyCMS theme JSON or Skeleton.dev theme exports in the Palette & Import tab.

Default install (Corporate workspace)

Fresh installs seed the active theme from /src/themes/default.json:

  • Layout: cozy density, bordered cards, sticky action bar, collapsible sidebar
  • Branding: brandedLogin: true — login shows tenant site name + accent colors immediately
  • Palette: teal primary (#0f766e), blue tertiary (#1d4ed8), warm professional tones

Users land on a polished default — no need to discover Design System first. Customize from Config → Design System when ready.

Skeleton.dev color import

Skeleton’s theme generator exports JSON with a properties map (--color-primary-500, --color-surface-950, etc.). SveltyCMS also accepts a shorthand palette format in /src/themes/*.json for quick branding:

"properties": {
  "primary": "#0f766e",
  "tertiary": "#1d4ed8",
  "surface": "#f8fafc"
}

Shorthand keys expand to full 50–950 shade scales via CSS color-mix() — light surface values anchor at --color-surface-50; other palettes anchor at -500.

On import or boot sync, SveltyCMS maps those tokens to runtime admin CSS scoped under .admin-theme-container and [data-admin-theme] — so utilities like bg-primary-500 and text-tertiary-500 update immediately across the admin panel.

Skeleton palette SveltyCMS palette Notes
primary primary Direct match
secondary secondary Direct match
tertiary tertiary Direct match
accent tertiary Mapped when Skeleton uses accent naming
success success Direct match
warning warning Direct match
error error Direct match
surface surface Direct match

Skeleton --radius-base and --radius-container also bridge to --admin-radius-* tokens. You do not need to edit src/app.css — compile-time defaults stay intact; imported colors apply at runtime via the theme’s customCss field (visible and editable in Advanced → Custom CSS).

Skeleton CSS exports ([data-theme='...'] { ... }) pasted as JSON with a css or code field are supported when a name is provided.

Contrast warnings: On Skeleton or shorthand palette import, SveltyCMS runs an advisory WCAG AA audit (body text, button labels, accent on surface). Failures appear as toast warnings — imports are not blocked.

My Overrides (per-user preferences)

All users can override density, card style, reduced motion, high contrast, and layout regions (sidebars, page header/footer) on Design System → My Overrides (/config/design-system?tab=overrides). User → Settings offers a compact density/card/a11y strip and links to the same deep link for full layout prefs. Use My Layout to set each region to visible, hidden, or theme default; click Use current layout to snapshot the panels as they are now. Sidebar toggles also auto-save to your profile after 2 seconds (non-admins store only differences from the tenant theme). Preferences apply immediately across the admin shell without a page reload (optimistic client overlay + app:user-prefs invalidation).

Theme Files (/src/themes/*.json)

Drop a .json file into the /src/themes/ directory and Vite auto-imports it to the database on next save. This is the File + DB hybrid — files are version-controlled distribution artifacts, DB is the runtime source of truth.

Tip

Theme files are Git-trackable, shareable between instances, and marketplace-ready. Copy a .json between servers and it works identically.

Example — Default Theme (/src/themes/default.json):

{
  "name": "Corporate",
  "description": "Enterprise default theme preset — cozy density, bordered cards.",
  "density": "cozy",
  "variant": "bordered",
  "features": {
    "stickyActionBar": true,
    "collapsibleSidebar": true,
    "brandedLogin": true,
    "layoutRegions": { "collections": "left", "mediaGalleries": "left" }
  },
  "properties": {
    "primary": "#0f766e",
    "secondary": "#334155",
    "tertiary": "#1d4ed8",
    "success": "#16a34a",
    "warning": "#d97706",
    "error": "#dc2626",
    "surface": "#f8fafc"
  }
}

Example — Developer Workbench (custom theme, save to src/themes/developer-workbench.json):

{
  "name": "Developer Workbench",
  "description": "Compact density, flat cards, collections on the right.",
  "density": "compact",
  "variant": "flat",
  "features": {
    "stickyActionBar": false,
    "collapsibleSidebar": true,
    "layoutRegions": { "collections": "right", "mediaGalleries": "left" }
  },
  "layoutState": {
    "leftSidebar": "full",
    "rightSidebar": "full",
    "pageheader": "hidden",
    "pagefooter": "hidden",
    "header": "hidden",
    "footer": "hidden"
  }
}

Example — Distraction-Free Writer (custom theme, save to src/themes/distraction-free-writer.json):

{
  "name": "Distraction-Free Writer",
  "description": "Spacious density, elevated cards, all chrome hidden.",
  "density": "spacious",
  "variant": "elevated",
  "features": {
    "stickyActionBar": true,
    "reducedMotion": true,
    "layoutRegions": { "collections": "left", "mediaGalleries": "left" }
  },
  "layoutState": {
    "leftSidebar": "hidden",
    "rightSidebar": "hidden",
    "pageheader": "hidden",
    "pagefooter": "hidden",
    "header": "hidden",
    "footer": "hidden"
  }
}

Reset to Defaults

The Danger Zone in the Advanced tab resets all settings to factory defaults. This cannot be undone.


⚡ Live Preview

All changes in Design System are applied instantly via Svelte 5’s reactive $state. The entire admin panel behind the settings page reflects your changes in real time — before you click Save.

A sticky Save Bar appears at the bottom whenever there are unsaved changes:

  • Discard — Revert all preview changes back to the last saved state.
  • Save Theme — Persist all settings to the database.

🏗️ Flexible Admin Layout Shell

Beyond visual styling, SveltyCMS’s admin layout is built on a highly flexible shell where every UI region can be independently toggled, resized, and adapted per screen size. Each admin theme can express not just colors and spacing, but full layout structure.

Independently Togglable Regions

The +layout.svelte (src/routes/(app)/+layout.svelte) wraps every admin page with six independently toggleable regions, all content-injectable via Svelte 5 <Slot>:

Region ui.state key Slot Name Purpose
Top Toolbar header global-toolbar Theme-injectable global toolbar
Left Sidebar leftSidebar (built-in) Navigation: collections, media, user, settings
Right Sidebar rightSidebar (built-in) Entry inspector: save, status, schedule, metadata
Page Header pageheader (built-in) Collection breadcrumb + action buttons
Page Footer pagefooter (built-in) Entry metadata (created/updated timestamps)
Bottom Bar footer global-footer Theme-injectable status bar

Additionally, a sticky action bar region (Slot name="sticky-action-bar") appears at the bottom of the main content area when theme.features.stickyActionBar is enabled.

<!-- +layout.svelte — how regions work -->
{#if ui.state.header !== 'hidden'}
  <header style="height: var(--admin-header-height, 32px);">
    <Slot name="global-toolbar" />  <!-- ← Theme injects custom toolbar here -->
  </header>
{/if}

<!-- Sticky action bar — theme-controlled -->
{#if theme.features.stickyActionBar}
  <div class="sticky bottom-0 backdrop-blur-md"
       style="min-height: var(--admin-sticky-bar-height, 56px);">
    <Slot name="sticky-action-bar" />
  </div>
{/if}

{#if ui.state.footer !== 'hidden'}
  <footer>
    <Slot name="global-footer" />  <!-- ← Theme injects status bar here -->
  </footer>
{/if}

Each region is independently toggleable at runtime via the ui singleton store (src/stores/ui-store.svelte):

// Switch between layouts in real time
ui.toggle("leftSidebar", "hidden"); // Full-width content mode
ui.toggle("rightSidebar", "full"); // Show entry inspector
ui.toggle("pageheader", "hidden"); // Hide breadcrumbs

The ui-store is verified by unit tests (tests/unit/ui-store.test.ts) confirming toggle operations, singleton behavior, and screen-size-aware updateLayout() calls.

How Components Use Layout State

Left Sidebar (left-sidebar.svelte) — The main navigation adapts its layout based on ui.state.leftSidebar:

  • full → Shows logo, collection tree, media folders, user avatar grid, and all footer icons
  • hidden → Invisible; a toggle button in header-edit.svelte lets users reveal it
  • The sidebar also has its own internal isSidebarFull state for expand/collapse cycles

Right Sidebar (right-sidebar.svelte) — The entry inspector panel (rightSidebar) appears conditionally:

  • Shown in edit/create modes when the user has write permissions
  • Contains Save, Status toggle, Clone, Delete, Schedule, and entry metadata
  • Automatically hides in view mode or when user lacks write access

Page Header (header-edit.svelte) — The collection header bar adapts to multiple contexts:

  • Shows collection name + icon with current mode (edit/create)
  • Offers a “Show More” dropdown on mobile for compact actions (Status, Delete, Schedule, Clone)
  • Provides translation status, save/cancel buttons, and sidebar toggle
  • Visibility controlled by both ui.state.pageheader and ui.state.leftSidebar

Page Footer (page-footer.svelte) — Shows entry-level metadata:

  • Created/Updated timestamps with username attribution
  • Only visible when ui.state.pagefooter !== 'hidden'

Responsive Breakpoints

Layout visibility adapts to screen size via src/utils/screen-size.ts, which defines six breakpoints:

Breakpoint Min Width Layout Behavior
xs 0px Mobile — floating bottom nav, hidden sidebars
sm 640px Small tablet — compact sidebar, stacked content
md 768px Tablet — full sidebar possible
lg 1024px Desktop — full layout, all regions available
xl 1280px Large desktop — spacious density comfortable
2xl 1536px Ultrawide — maximum content area

The screen store (src/stores/screen-size-store.svelte) exposes isDesktop and isMobile booleans. On mobile, a <FloatingNav /> replaces the sidebar, and the main content area gets extra bottom padding for the mobile navigation bar.

How Themes Leverage the Shell

Because every region is independently toggleable via ui.state, combined with the theme system’s density presets and --admin-sidebar-width CSS variable, theme authors have complete control over both visual appearance and structural layout:

// A "Full-Width Editor" theme: hide all chrome for maximum content canvas
ui.toggle("leftSidebar", "hidden");
ui.toggle("rightSidebar", "hidden");
ui.toggle("pageheader", "hidden");
ui.toggle("pagefooter", "hidden");

// A "Developer Workbench" theme: compact density + visible inspector + system footer
ui.toggle("rightSidebar", "full");
ui.toggle("pagefooter", "full");
// Plus: density = "compact", variant = "flat"
flowchart LR
    subgraph Shell["Admin Layout Shell (6 regions + sticky bar)"]
        direction TB
        TH["Top Toolbar (header)<br/>Slot: global-toolbar"]
        LS["Left Sidebar<br/>Collections + Nav"]
        MC["Main Content<br/>+ Page Header/Footer"]
        RS["Right Sidebar<br/>Entry Inspector"]
        SB["Sticky Action Bar<br/>Slot: sticky-action-bar<br/>(theme-controlled)"]
        BF["Bottom Bar (footer)<br/>Slot: global-footer"]
    end

    UI["ui-store<br/>6 toggleable regions"]
    SC["Screen Size Store<br/>6 breakpoints"]
    TM["Theme Context<br/>Density + CSS vars + layoutRegions"]

    UI -->|"visibility"| Shell
    SC -->|"responsive"| Shell
    TM -->|"sizes + styles + layout"| Shell

This architecture provides a highly optimized toolbar, sidebar, and layout system — all without PHP/Twig overhead, using native Svelte 5 reactivity.


🧱 Architecture

The admin theme system is built on three pillars that work together:

1. Structural CSS Variables (--admin-*)

Defined in src/utilities.css, these variables control radii, shadows, sidebar width, header height, and density at the browser level. They cascade from the data-density attribute on the admin layout root element.

/* Applied automatically when density changes */
[data-density="compact"] {
  --admin-sidebar-width: 200px;
  --admin-header-height: 48px;
}

2. Semantic surface roles (--admin-bg-*, text, borders)

Also defined in src/utilities.css (light defaults + html.dark elevation). Use these instead of hardcoding paired dark: surface classes on admin shells:

Token Role
--admin-bg-page Admin page canvas (AdminPageShell)
--admin-bg-card Elevated card surface (AdminCard, .card) — one step above page in dark mode
--admin-bg-sidebar Sidebar background
--admin-border-default / --admin-border-subtle Hairline borders
--admin-text-body / --admin-text-muted Primary / secondary text

Card shadows stay variant-driven (flat / bordered / elevated) via AdminTheme.cardShadow and --admin-shadow-* — do not hardcode hover:shadow-lg on every card.

3. Svelte 5 AdminTheme Context

The AdminTheme class in src/components/ui/theme-context.svelte.ts is the reactive runtime state consumed by all 37+ native UI components (Button, Card, Input, Table, Modal, Drawer, etc.).

// Every component reads theme context for adaptive rendering
const theme = getThemeContext();
const shadow = theme?.cardShadow ?? "var(--admin-shadow-elevation)";
sequenceDiagram
    participant SP as Settings Page
    participant TC as Theme Context
    participant CP as Components

    SP->>TC: Mutate density/variant
    TC-->>CP: Reactive $state update
    CP->>CP: Re-render with new vars
    Note over SP,CP: Instant — before save
    SP->>API: POST /api/theme/admin-theme
    API->>DB: Persist to Theme record

Theme Loading on Startup

App Start → hooks.server.ts → ThemeManager.getTheme()
         → reads config.adminTheme from DB
         → +layout.svelte sets AdminTheme context
         → all components render with stored settings

🔌 API Endpoints

All theme operations go through the REST API at /api/theme/. Admin authentication is required for write operations.

Endpoint Method Description
/api/theme/list GET List all themes with summaries
/api/theme/admin-theme GET Get active theme config
/api/theme/admin-theme POST Save theme config
/api/theme/admin-theme DELETE Reset to defaults
/api/theme/create POST Create new theme ({ name })
/api/theme/activate POST Activate theme ({ themeId })
/api/theme/clone POST Clone theme ({ sourceId, name })
/api/theme/delete POST Delete theme ({ themeId })
/api/theme/import-preset POST Import Skeleton.dev/SveltyCMS JSON

Related Documents


🛠️ Developer Guide: Building Custom Admin Themes

SveltyCMS’s admin theme system is designed for both GUI configuration and programmatic theme development.

Quick Start: Theme JSON File

Drop a .json file into /src/themes/ and Vite auto-imports it:

{
  "name": "My Custom Theme",
  "description": "A custom admin theme.",
  "density": "cozy",
  "variant": "bordered",
  "features": {
    "stickyActionBar": true,
    "collapsibleSidebar": false,
    "brandedLogin": false,
    "highContrastMode": false,
    "reducedMotion": false,
    "layoutRegions": {
      "collections": "left",
      "mediaGalleries": "left"
    }
  },
  "layoutState": {
    "leftSidebar": "full",
    "rightSidebar": "hidden",
    "pageheader": "hidden",
    "pagefooter": "hidden",
    "header": "hidden",
    "footer": "hidden"
  },
  "customCss": "/* Optional custom CSS */"
}

Programmatic Theme Creation

// Create via API
await fetch("/api/theme/create", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "My Theme",
    settings: { density: "compact", variant: "flat" },
  }),
});

// Activate a theme
await fetch("/api/theme/activate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ themeId: "..." }),
});

Reading Theme Context in Components

All 37+ native UI components already consume the AdminTheme context. Any custom component can too:

import { getThemeContext } from "@components/ui/theme-context.svelte";

const theme = getThemeContext();
// theme?.density        → "compact" | "cozy" | "spacious"
// theme?.variant         → "flat" | "bordered" | "elevated"
// theme?.cardShadow      → synthesized shadow based on variant
// theme?.cardBorder      → border width based on variant
// theme?.features?.stickyActionBar → boolean

Injecting Content into Layout Shell Regions

Three theme-injectable slots are available for plugin or theme content:

Slot Name Region Purpose
global-toolbar Top header Global navigation toolbar
global-footer Bottom bar System status, version info
sticky-action-bar Main content bottom Page action buttons (via <StickyActions> component)

Plugins register into these slots via the PluginSlot system (src/plugins/types.ts).

6-Region Layout Control

Every admin page lives inside a 6-region shell controlled by ui-store:

import { ui } from "@src/stores/ui-store.svelte";

// Toggle any region independently
ui.toggle("leftSidebar", "hidden");
ui.toggle("rightSidebar", "full");
ui.toggle("pageheader", "hidden");

// Layout state is persisted to DB per-theme automatically
// (debounced 2s save to config.adminTheme.layoutState)

Screen Size Adaptation

import { screen } from "@src/stores/screen-size-store.svelte";

// screen.isDesktop → true when width >= 1024px
// screen.isMobile  → true when width < 768px
themeappearanceadmin-themedensitycustom-cssconfiguration
Was this page helpful?