Design System & Admin Theme Settings
Design System workspace: multi-theme management, density, visual styles, personal overrides, custom CSS, and live component preview.
On this page
SveltyCMS’s admin theme system has two layers that are easy to confuse:
- Themes — named, workspace-wide designs (density, visual style, features, custom CSS) that an admin creates and activates. They apply to everyone and are stored in the database.
- My Overrides — a per-user layer that sits on top of the active theme. It changes how the admin looks for you only; it never edits the theme and never affects other users.
This page (Config → Design System, /config/design-system) is the sole route — there is no separate Appearance page. Changes preview instantly before saving.
Deep links (query ?tab=):
| Tab | URL | Audience | Scope |
|---|---|---|---|
| My Overrides | /config/design-system?tab=overrides |
All users (linked from /user Settings) |
Just you — layered on the active theme |
| Live Preview | /config/design-system?tab=preview |
All users | Read-only component catalog + token readout |
| Themes / Presets / Layout / Style / Features / Advanced | ?tab=themes etc. |
Workspace admins | Whole workspace (applies to every user) |
🎨 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.
The built-in theme is a single row named Default. On boot (and on every /src/themes/*.json change) the file sync reconciles it: a legacy SveltyCMSTheme row is renamed to Default, any duplicate built-in row is pruned, and a stale customCss palette is cleared so brand colors always come from the src/app.css @theme tokens unless you explicitly set one.
Creating a Theme
- Navigate to Config → Design System → Themes.
- Enter a name (e.g., “Midnight Blue”, “High Contrast Reader”).
- Click Create.
New themes start with the current density, variant, and features as defaults. Switch to other tabs to customize before saving.
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.
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";
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 in the sticky bar at the bottom of the screen when the feature is on (and fall back to their original in-place position when it is off) — so the actions never appear twice on screen. The bar uses role="toolbar", aria-label="Page actions", and aria-live="polite" for screen reader compatibility.
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.
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 the default seeds, or remove only the palette CSS block |
Seeds expand via a hue-preserving OKLCH ramp into full 50–950 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.
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 a shorthand palette block in the Palette & Import tab.
Default install
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: none — the builtin default carries no
propertiesmap, so brand colors come straight from thesrc/app.css@themetokens (green primary). A stale palette here would silently override those tokens at:root.
Users land on a polished default — no need to discover Design System first. Customize from Config → Design System when ready.
Theme JSON import
Theme JSON exports carry 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": "#5fd317",
"tertiary": "#1d4ed8",
"surface": "#f8fafc"
}
Shorthand keys expand to full 50–950 shade scales via a hue-preserving OKLCH ramp — light surface values anchor at --color-surface-50, other palettes anchor at -500 — and every step is emitted as a CSS oklch(...) value.
On import or boot sync, SveltyCMS maps those tokens to runtime admin CSS scoped to :root (plus .admin-theme-container / [data-admin-theme]) — so every utility (bg-primary-500, text-tertiary-500), every --admin-* semantic role, and even portalled overlays update immediately, across the whole admin panel. Dark-mode overrides are emitted under html.dark so they keep the specificity edge over the light :root block.
| Palette key | SveltyCMS palette | Notes |
|---|---|---|
primary |
primary |
Direct match |
secondary |
secondary |
Direct match |
tertiary |
tertiary |
Direct match |
accent |
tertiary |
Legacy alias for accent naming |
success |
success |
Direct match |
warning |
warning |
Direct match |
error |
error |
Direct match |
surface |
surface |
Direct match |
--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).
CSS exports ([data-theme='...'] { ... }) pasted as JSON with a css or code field are supported when a name is provided.
Contrast warnings: On 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) {#my-overrides}
/config/design-system?tab=overrides — available to every signed-in user, no admin rights required.
This tab is not a theme editor. It is a thin per-user layer on top of the active workspace theme: it only changes how the admin looks for you, never the theme definition, and never other users. Everything applies immediately — no page reload.
| Control | Values | What it changes (for your session only) |
|---|---|---|
| Density | theme default · compact · cozy · spacious | Panel spacing, sidebar width, text size |
| Card Style | theme default · flat · bordered · elevated | Card / panel variant |
| Reduced Motion | on · off | Disables animations and transitions |
| High Contrast | on · off | Boosts text / background contrast |
| My Layout (6 selects) | theme default · visible · hidden | Visibility of leftSidebar, rightSidebar, pageheader, pagefooter, header, footer |
Use current layout snapshots the panels exactly as they are right now into the layout selects. You can also toggle sidebars directly in the shell — those auto-save after 2 seconds (non-admins store only the difference from the tenant theme).
Overrides vs. theme — the difference
| Theme (Themes / Layout / Style / Features / Advanced tabs) | My Overrides (this tab) | |
|---|---|---|
| Scope | Workspace-wide (every user) | Just you |
| Who can edit | Workspace admins only | Every signed-in user |
| What it writes | The theme definition stored in the database | Your user.preferences.theme row |
| Route | ?tab=themes, ?tab=layout, … |
?tab=overrides |
| Affects other users | Yes | No |
An override is always relative to the theme: leaving a select on Use theme default means “inherit whatever the active theme says”. Clear Overrides removes the whole personal layer and falls back to the theme.
Locking. If a workspace admin locks a setting in the theme (theme.lockedSettings), the matching control here is disabled. When layoutState is locked you see “Layout is locked by your workspace admin — tenant defaults apply.” Personal accessibility overrides (contrast, motion) still work unless the admin locked those specifically.
How it is saved. Save My Preferences calls PUT /api/user/update-user-attributes with { user_id: "self", newUserData: { preferences: { theme: … } } } — the server-side source of truth is user.preferences.theme. On success the UI caches the value in localStorage (sveltycms:theme-prefs), applies it through the userThemePrefs store (optimistic), re-renders the layout, and invalidates app:user-prefs to re-sync server data. Clear Overrides sends an empty payload for the same path.
User → Settings offers a compact density/card/accessibility strip that edits the same per-user preferences and links to this deep link for the full layout controls. It is the same data, not a second system.
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.
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": "Default",
"description": "SveltyCMS default theme — cozy density, bordered cards.",
"density": "cozy",
"variant": "bordered",
"features": {
"stickyActionBar": true,
"collapsibleSidebar": true,
"brandedLogin": true,
"layoutRegions": { "collections": "left", "mediaGalleries": "left" }
},
"properties": {
"primary": "#5fd317",
"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 iconshidden→ Invisible; a toggle button inheader-edit.sveltelets users reveal it- The sidebar also has its own internal
isSidebarFullstate for expand/collapse cycles
Right Sidebar (right-sidebar.svelte) — The entry inspector panel (rightSidebar) appears conditionally:
- Shown in
edit/createmodes when the user has write permissions - Contains Save, Status toggle, Clone, Delete, Schedule, and entry metadata
- Automatically hides in
viewmode 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.pageheaderandui.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
4. Motion & Transition System
All shared motion lives in src/utils/admin-transitions.ts and is reduced-motion safe (every transition drops to 0ms when prefers-reduced-motion: reduce or the theme’s reducedMotion feature is on — see the Accessibility Guide).
| Transition | When to use |
|---|---|
adminPage |
Page entry (240ms fade + 8px rise) — applied once by (app)/+layout.svelte, keyed on page.url.pathname |
adminFade |
Modal / section reveals (200ms fade) |
adminStagger |
Card grids / list rows — pass index for a cascade |
adminSlide |
Drawers / sidebars — signed distance for direction |
Motion timing tokens live in src/utilities.css (--admin-motion-fast: 150ms, --admin-motion-base: 200ms, --admin-motion-slow: 300ms, --admin-ease-out, --admin-ease-in-out) and density radii come from --admin-radius-*. Use the shared utilities — never per-page in:fly/in:scale one-offs — so every route (dashboard, collection builder, system settings, setup wizard) moves identically.
Neutral aliases: gray, slate, neutral, zinc, stone are aliased to the surface ramp in @theme (--color-gray-500: var(--color-surface-500)), so neutral utilities follow the active theme. Alpha on palette tokens is applied with color-mix(in srgb, var(--color-primary-500) 30%, transparent) — never rgb(var(--color-*) / 0.3), which is invalid once tokens resolve to oklch()/hex.
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/. Read methods require system:read, write methods require system:settings (fail-closed via ENDPOINT_PERMISSIONS); GET /api/theme/public is standalone and unauthenticated. Full payloads, response shapes and the public-branding contract live in the Theme & Design System API reference.
| 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/set-default |
POST | Set system default ({ themeId }) |
/api/theme/import-preset |
POST | Import SveltyCMS/shorthand JSON |
/api/theme/get-current-theme |
GET | Active theme record (404 if none) |
/api/theme/update-theme |
POST | Low-level customCss-only update |
/api/theme/public |
GET | Public tenant branding |
🏢 Agency Whitelabeling & Client Handover
SveltyCMS is built from the ground up to support complete agency whitelabeling. Agencies and enterprise dev teams can rebrand the entire CMS administration experience in minutes, presenting a cohesive, professional brand identity to clients without touching a single line of CMS core code.
5-Minute Client Whitelabel Blueprint
Follow this step-by-step workflow to prepare a turnkey client installation:
(Logo, Favicon, Site Name)"] --> B["2. Palette Studio
(Brand Hex Colors)"] B --> C["3. Branded Login
(Custom Login Experience)"] C --> D["4. Agency Credits
(Support Links in Footer)"] D --> E["5. Git Export
(/src/themes/client.json)"]
Step 1: Set Brand Identity & Assets
Navigate to Config → System Settings:
| Setting Key | Value / Format | Effect |
|---|---|---|
SITE_NAME |
Client Organization / Project Name | Replaces “SveltyCMS” in browser title, header breadcrumbs, and sidebar. Capped at 40 characters so the sidebar lockup can never overflow (longer names are ellipsised). |
SITE_SLOGAN |
Optional tagline (max 60 chars) | Tagline rendered under the logo on the login screen. Leave blank to keep the translated default (with Sveltekit Power). |
LOGO_URL |
/media/client-logo.svg or CDN |
Client brand logo rendered on login screen and admin sidebar |
FAVICON_URL |
/favicon.ico or PNG |
Custom browser tab icon |
ACCENT_COLOR |
Brand hex (e.g., #5fd317) |
Meta theme-color for mobile browser chrome |
Step 2: Configure Client Brand Palette
Navigate to Config → Design System → Palette & Import:
- In the Palette Studio, input the client’s corporate primary color (e.g.,
#1e40affor corporate navy,#047857for emerald). - Choose a matching secondary and surface seed.
- SveltyCMS instantly generates full 50–950 tint and shade scales via a hue-preserving OKLCH ramp, automatically binding semantic variables (
--admin-bg-*,--admin-border-*). - Click Live apply to verify the aesthetic in real time across the current admin workspace.
Step 3: Enable Branded Login
In Config → Design System → Features:
- Toggle Branded Login to
ON. - When active, the
/loginauthentication screen automatically:- Replaces default CMS logos with the client’s high-resolution
LOGO_URL. - Prominently displays the custom
SITE_NAME. - Uses the active theme’s card variant (
flat,bordered, orelevated) and border styling. - Applies sanitized custom CSS rules scoped exclusively to the login container.
- Replaces default CMS logos with the client’s high-resolution
Step 4: Add Agency Support Credits & Contact Information
To provide a branded client handoff with direct support escalation, inject your agency credentials into the global-footer slot:
<!-- Example: Injecting agency support into layout via plugin or theme component -->
<div class="flex items-center justify-between text-xs text-surface-500 px-4 py-2">
<span>© 2026 Acme Corp. All rights reserved.</span>
<span>
Managed by <a href="https://youragency.com" class="text-primary-500 font-medium hover:underline" target="_blank">YourAgency</a>
• <a href="mailto:support@youragency.com" class="hover:underline">Need Help?</a>
</span>
</div>
Step 5: Version-Control the Client Theme
Once customized, click Export Theme JSON on the Palette & Import tab. Save the output to:
/src/themes/client-brand.json
Because SveltyCMS features a File + DB Hybrid Theme Architecture, committing this file to your project repository guarantees that when your deployment pipeline runs (git push origin main), the client’s custom branding is automatically loaded into the database during initial setup or migration without manual GUI re-configuration.
Protecting Brand Integrity: Role Locking & Permissions
Clients and non-technical content editors should enjoy the customized interface without the risk of accidental brand breakage:
- Role Gating: Only users with the
adminrole or explicittheme:writepermissions have access to/config/design-system. - Editor Experience: Content editors and authors see the client’s branded interface out of the box, with full focus on content creation.
- Accessibility Overrides: Individual users can still adjust their personal preferences (such as High Contrast Mode or Compact Density) via
/config/design-system?tab=overrideswithout impacting the global company branding.
💼 Commercial Strategy & Marketplace Ecosystem
SveltyCMS balances rapid open-source developer adoption with a sustainable commercial monetization path.
The “Redis vs. Memory Cache” Architecture Rule
To maintain peak performance and avoid bloated core code, SveltyCMS strictly enforces the Zero-Friction Core vs. Scalable Extensions design pattern:
| Category | Free Open-Core (Zero Dependencies) | Commercial / Marketplace Extension |
|---|---|---|
| Caching | High-performance in-memory LRU cache with sub-1µs hits | Redis L2 Distributed Cluster with pub/sub invalidation |
| Database | Embedded SQLite with sub-2ms local persistence | Managed PostgreSQL / MariaDB / MongoDB with connection pooling |
| Media | Local filesystem storage with WebP/AVIF processing | S3 / Cloudflare R2 Cloud Storage CDN with global edge cache |
| Theme System | Built-in 6-region shell, Palette Studio, 37+ UI components | Premium Agency Theme Packs (Medical, SaaS, Editorial) |
| Translations | Standard multi-locale content schema & analytics engine | AI Automated Bulk Translation (DeepL / OpenAI cockpit plugin) |
| Security | Native WebAuthn passkeys, 2FA TOTP, AES-256 ALE | Enterprise SAML / OIDC Single Sign-On (SSO) connector |
Adoption First: The Open-Core foundation contains everything required to build blazing-fast production websites. No arbitrary artificial limits are placed on user accounts, collections, API traffic, or languages.
Monetization Channels
- Agency Whitelabel Pro Packs:
- Turnkey agency tools including multi-client workspace switchers, client billing integration, and one-click brand provisioning.
- Marketplace Extensions (
marketplace.sveltycms.com):- Official and third-party verified widgets, dashboards, and enterprise connectors.
- Transparent 70/30 developer revenue share, encouraging ecosystem developers to build and sell high-value Svelte 5 components.
- Built-in license verification via
src/utils/license-manager.tssupporting offline-safe validation and 14-day client evaluation periods.
- Managed SveltyCMS Cloud:
- Turnkey global hosting with zero DevOps overhead, automated daily backups, managed Redis clusters, and edge CDN distribution.
🛠️ 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
📚 Related Documents
- System Settings Guide
- Access Management
- GUI Styling & UX Guide
- Native UI —
AdminPageShell,AdminCard, and the controls insrc/components/ui - Accessibility Guide
- Hybrid Package Model