Skip to content
CodeAtlas UI
On this page Options

Getting started

Theming

Define a theme with defineTheme, derive a brand preset from one hex color, switch schemes and surface styles, and override tokens through the cascade.

A theme is a plain object. The library compiles it to CSS once at build time (the same object always gives the same CSS) and injects it before any stylesheet, so there is no per-render style work and no flash.

// src/lib/theme.ts
import type { ThemeOptions } from "@codeatlasdev/ui/vite";

export const theme: ThemeOptions = {
  presets: [{ id: "acme", color: "#2563eb" }],
  defaultPreset: "acme",
  defaultScheme: "system",
  defaultStyle: "solid",
  fonts: { body: "Geist, system-ui, sans-serif" },
  radius: 0.8,
  density: "comfortable",
  overrides: { light: { "--ca-card-padding": "16px" } },
};

Pass the same object to caThemeHandle and caThemePlugin (SvelteKit), caUI (Astro) or themeHead (custom setups), see Installation. ThemeOptions is Partial<ThemeDefinition>, so every field is optional.

Options

Option Default Description
presets [] Brand presets generated from a hex color. Built-in presets are always available.
defaultPreset "amber" Preset used until the user picks one. Must be a built-in or one of presets.
defaultScheme "dark" "light", "dark" or "system". bunx @codeatlasdev/ui init scaffolds "system".
defaultStyle "solid" Surface material: "solid", "flat", "frosted", "glass", "outlined", "neumorphic", "clay", "tonal" or "chrome". Solid is the sober default; glass is the opt-in liquid look.
fonts {} body, mono and display. Each is a font stack string or a self-hosted font descriptor (@font-face, preload, metric-matched fallback), see Performance. They set --ca-font-sans, --ca-font-mono and --ca-font-display.
radius 0.8 Multiplier applied to the --ca-radius-xs to --ca-radius-xl scale (0.8 makes --ca-radius-md 8px).
density "comfortable" "compact" (0.75), "comfortable" (1), "airy" (1.25) or a number. Scales the --ca-space-* tokens, so padding, gaps and page spacing follow.
paper "plain" Page background: "plain", "mesh" (brand glow, drawn by the glass and frosted styles), "grid", "dots" or "lines". Flat patterns are drawn in --ca-outline-subtle, so they follow the preset and scheme.
iconStroke 1.75 Lucide stroke width.
controlRadius "md" Corner role of buttons, fields and chips: "xs", "sm", "md", "lg", "xl" or "pill".
border { width: 1, strong: 2 } Border weights in px (--ca-border-width, --ca-border-width-strong).
elevation "soft" How shadows are drawn: "soft", "none", "hard" (offset, no blur) or "glow".
typography {} scale, weights (heading defaults to 600), headingTracking, headingCase, labelTracking, labelCase, lineHeight and measure.
motion {} scale (multiplier on every duration) and easing: "standard", "emphasized", "spring", "linear" or "steps".
noise 0 Film-grain over the page background, from 0 to 0.12.
overrides {} Raw --ca-* token overrides, per scheme (light, dark). Prefer preset role colors (below) for palettes.

defineTheme(options) resolves the options (defaults, validation) and themeCSS(resolved) returns the CSS string. You rarely call them yourself; the integrations do. Invalid input fails the build: a preset id that clashes with a built-in, a color that is not #rrggbb, or a defaultPreset that does not exist.

Presets

Eight OKLCH presets ship with the package: amber, ocean, crimson, cyan, violet, emerald, rose and slate. One attribute switches the whole app:

<html data-preset="ocean" class="dark">

In code, use the store:

import { colorScheme } from "@codeatlasdev/ui";

colorScheme.setPreset("violet");
colorScheme.presets; // every valid id: built-ins plus your brand presets

Brand preset from a hex color

Give the library your logo color and it derives hue, chroma and lightness in OKLCH, then emits the complete light and dark token set (brand, surfaces, text, outlines, glass tints). Charts use the hex as their first series color.

export const theme: ThemeOptions = {
  presets: [{ id: "acme", color: "#2563eb", label: "Acme" }],
  defaultPreset: "acme",
};

Porting a palette by role

To bring an existing palette (a terminal or editor theme) into a preset, set role colors per scheme. Unset roles keep the generated value; a per-scheme brand regenerates that scheme’s brand ramp, and background also drives <meta name="theme-color">. Roles are scoped to the preset, so switching presets and theme islands keep working.

presets: [{
  id: "tokyo",
  color: "#7aa2f7",
  colors: {
    light: { brand: "#34548a" },
    dark: { background: "#1a1b26", surface: "#232430", text: "#c0caf5", border: "#565f89", error: "#f7768e" },
  },
}],

Roles: brand, background, sunken, surface, overlay, text, textSecondary, textHint, textDisabled, borderSubtle, border, borderStrong, success, warning, info, error. Check contrast after overriding text or surfaces: the generated palette guarantees AA, hand-picked colors are yours to verify.

Sketchbook style and hand-drawn marks

designStyle("sketchbook", { ... }) is a journal look: grid paper, serif display, square outlined cards with hard shadows. It pairs with the opt-in marks stylesheet:

import "@codeatlasdev/ui/styles/marks.css";
<span class="ca-mark-underline">already knows.</span>
<p class="ca-mark-note">A margin note with a sketched arrow.</p>

Marks are SVG masks painted with --ca-mark-color (brand by default); swap the shapes with --ca-mark-scribble and --ca-mark-arrow. In forced-colors mode they paint in CanvasText.

Schemes

defaultScheme: "system" follows the operating system, including live changes while the page is open. The init script also keeps <meta name="theme-color"> in sync with the preset’s surface color.

colorScheme.preference; // "light" | "dark" | "system"
colorScheme.resolved;   // "light" | "dark" (system already resolved)
colorScheme.isDark;     // boolean
colorScheme.setPreference("system");
colorScheme.toggle();   // flips resolved light and dark

Surface styles

data-style on <html> swaps the material of every surface at once.

Style Look
solid Default. No blur, opaque surfaces, plain page, hairline edges, no glow. Keeps soft neutral shadows.
flat No blur, no mesh, no glow, no drop shadows: separation comes from borders.
frosted Less blur, calmer mesh background.
glass Liquid Glass: strong blur and saturation, translucent tints, brand mesh, glow and rim light. Opt-in.
outlined Border-defined opaque surfaces with no elevation. Used by the swiss, brutalism, terminal and sketchbook recipes.
neumorphic One base color extruded by paired light and dark shadows. Low contrast: use it for showcases, not dense data UIs.
clay Soft, inflated opaque surfaces.
tonal Surfaces tinted with the brand hue and soft elevation (Material 3 style).
chrome Glossy gradient with a metallic rim and brand glow. Blur stays on.
colorScheme.setStyle("glass");

The liquid recipe

Earlier versions looked like Liquid Glass: glass surfaces over a three-hue mesh, glow on selected states, round corners (10px controls), 2px icons, bold headings and hue-tinted shadows. That look is now the opt-in liquid recipe:

import { designStyle } from "@codeatlasdev/ui/astro";
import type { ThemeOptions } from "@codeatlasdev/ui/vite";

export const theme: ThemeOptions = designStyle("liquid", {
  presets: [{ id: "acme", color: "#2563eb" }],
  defaultPreset: "acme",
  defaultScheme: "dark",
});

designStyle("default") is the sober default. designStyle is plain data, so theme.ts can import it in Node from @codeatlasdev/ui/astro (it is also exported from the root and from @codeatlasdev/ui/kit, which pull in Svelte files that vite.config cannot load). To migrate an app that relied on the old defaults, use designStyle("liquid", { ... }), or set defaultStyle: "glass" yourself.

Other design styles

DESIGN_STYLES maps each id to { id, label, description, taxonomy, theme }. designStyle(id, overrides) returns that recipe’s theme with your overrides merged on top, and mergeTheme(a, b) merges two partial themes (b wins). The ids are default, liquid, swiss, brutalism, neumorphism, glassmorphism, material, fluent, apple, bento, claymorphism, y2k, editorial, terminal and sketchbook. Each recipe only sets the design knobs from the options table (material, radius, border, elevation, typography, motion, icon stroke, paper).

Users on prefers-reduced-transparency: reduce get opaque surfaces regardless of the style.

The colorScheme store

Call colorScheme.init() once in the root layout (inside onMount). It re-applies the state to <html>, follows OS scheme changes and keeps the browser chrome color in sync. It is idempotent and returns a cleanup function.

Member Description
preference, resolved, preset, style, isDark, presets Reactive getters (Svelte 5 $state).
setPreference(pref), toggle() Change the scheme.
setPreset(id), setStyle(style) Change preset or surface style.
setTokens(vars) Runtime token overrides, see below.
init() Start listeners. Returns cleanup.

All choices persist in localStorage (ca-scheme, ca-preset, ca-style). Storage errors in private mode are ignored, and the values are validated against the theme before use.

Runtime tokens

setTokens writes your --ca-* variables to a single <style id="ca-runtime-tokens">, persists them and replays them in the init script before first paint. Use it for user-defined themes such as an imported editor theme. Keys must match --ca-*; values containing {, }, < or ; are dropped.

colorScheme.setTokens({ "--ca-brand-main": "#ff5500", "--ca-radius-md": "14px" });
colorScheme.setTokens({}); // clear

Theme islands

Put data-theme-scope="light" or "dark" on any element to re-apply that scheme’s tokens inside it, regardless of the page scheme. Useful for a dark hero on a light page or a light preview inside a dark editor.

<section data-theme-scope="dark">
  <!-- dark tokens here, on any page -->
</section>

Add data-preset to switch the palette as well, on the same element or on a wrapper. The nearest preset wins, even when the page uses your brand preset:

<div data-preset="violet">
  <section data-theme-scope="dark"><!-- violet, dark --></section>
</div>

Islands also feed the browser chrome color when they touch the top edge, see Mobile.

Cascade and overriding tokens

global.css declares its layers up front:

@layer ca.reset, ca.tokens, ca.themes, ca.base, ca.materials, ca.utilities, ca.components;

Every component stylesheet is wrapped in @layer ca.components, the last kit layer. Your app CSS is unlayered, and unlayered rules beat every layer regardless of specificity or import order. Overriding therefore never needs !important or a doubled class, for tokens and for component rules alike:

/* app.css: no !important, no .ca-btn.ca-btn */
:root {
  --ca-card-padding: 16px;
  --ca-radius-lg: 18px;
}
.ca-btn:disabled { opacity: 0.3; }
.ca-tf__wrap { border-radius: 4px; }
.ca-dialog__body { padding: 0; }

If your own CSS lives in a layer (for example Tailwind’s @layer components), declare it after the kit’s order statement so it ranks higher: @layer ca.reset, ca.tokens, ca.themes, ca.base, ca.materials, ca.utilities, ca.components, app;.

For per-scheme overrides prefer overrides in the theme object, which is emitted for both :root and theme islands.

Do not break the glass

Two CSS properties silently kill backdrop-filter on glass surfaces:

  • will-change: transform on the same element or an ancestor.
  • position: absolute children inside an isolation: isolate parent.

Keep backdrop-filter on the element that composites directly against the page.