Documentation

Asgard

Getting started

Theming

Guides

Layout

Docking

Inputs & forms

Button

Switch

Checkbox

Radio

Select

Slider

TextBox

Upload

ImageCropper

Pickers

Color picker

Calendar

Time picker

DateTime input

Data display

Card

Accordion

Typography

Progress

DataTable

TreeView

SyntaxColor

MarkdownRenderer

Terminal

Layout & docking

Grid

Splitter

SDiv

Dock

Theme switcher

Media query

Feedback & overlays

Alert

Toast

Tooltip

Popups

Drawer

FloatingPanel

FloatingWindow

Navigation & chrome

Breadcrumb

Stepper

Toolbar

Ribbon

ContextMenu

FeatureTour

Documentation

Asgard

Getting started

Theming

Guides

Layout

Docking

Inputs & forms

Button

Switch

Checkbox

Radio

Select

Slider

TextBox

Upload

ImageCropper

Pickers

Color picker

Calendar

Time picker

DateTime input

Data display

Card

Accordion

Typography

Progress

DataTable

TreeView

SyntaxColor

MarkdownRenderer

Terminal

Layout & docking

Grid

Splitter

SDiv

Dock

Theme switcher

Media query

Feedback & overlays

Alert

Toast

Tooltip

Popups

Drawer

FloatingPanel

FloatingWindow

Navigation & chrome

Breadcrumb

Stepper

Toolbar

Ribbon

ContextMenu

FeatureTour

Theming

Every Asgard component reads its colors from the nearest ThemeProvider. A theme is a plain object of tokens (backgrounds, text, borders, accents, semantic colors, per-component overrides), so you can use a built-in palette or supply your own.

ThemeProvider

The simplest, controlled usage: pass a theme object and the provider renders exactly that.

1
2
3
4
5
import { ThemeProvider, themes } from "@ekko/asgard"; // or from "@ekko/asgard/theme"
 
<ThemeProvider theme={themes.nord}>
{/* every Asgard component below is themed */}
</ThemeProvider>

ThemeProvider, themes, ThemeCssVars, useTheme are exported from both the main entry @ekko/asgard and the @ekko/asgard/theme subpath, import them from either (the ekko init rune -t asgard scaffold uses the main entry).

ThemeProvider has no persistence of its own, it renders the theme you feed it. Persisting the choice (localStorage, a Mimir atom, an ekko:rune store) is the app's job; just feed the saved value back into theme.

Theme your own markup (the CSS-vars bridge)

ThemeProvider themes Asgard components, it does not touch your own header, footer, or hand-written SCSS. If you've hit "I switched theme and Asgard re-themed but my own page chrome didn't," this is why, and <ThemeCssVars/> is the fix.

<ThemeCssVars/> mirrors the active theme onto document.documentElement as CSS custom properties (default prefix --ekko-), so your own SCSS can read the same tokens and re-theme on every switch. It renders nothing and is SSR-safe (the write is a no-op on the server and runs once mounted on the client). Place it inside <ThemeProvider>:

1
2
3
4
5
6
7
8
import { ThemeProvider, ThemeCssVars } from "@ekko/asgard";
import { themes } from "@ekko/asgard/theme";
 
<ThemeProvider theme={themes.nord}>
<ThemeCssVars />
{/* Asgard components AND your own markup now share the theme */}
<header className="app-header">My App</header>
</ThemeProvider>

Your app SCSS consumes the mirrored tokens with plain var(...):

1
2
3
4
5
6
7
8
9
10
11
12
.app-header {
background: var(--ekko-background-secondary);
color: var(--ekko-text-primary);
border-bottom: 1px solid var(--ekko-border-divider);
}
 
.app-header a {
color: var(--ekko-accent-primary);
}
.app-header a:hover {
color: var(--ekko-accent-primary-hover);
}

When you change the theme prop, <ThemeCssVars/> rewrites the custom properties and every rule above re-themes automatically, no extra wiring.

Custom prefix

Pass prefix to change the CSS-var namespace (e.g. to scope vars to your app):

1
2
<ThemeCssVars prefix="--app-" />
/* then in SCSS: background: var(--app-background-primary); */

Imperative escape hatch

For non-React callers, or to apply tokens once outside the component tree, use the imperative helpers (both from @ekko/asgard):

  • applyThemeToRoot(theme, prefix?), sets the custom properties on document.documentElement now (no-op

during SSR; default prefix --ekko-).

  • themeToCssVars(theme, prefix?), returns the flat { "--ekko-background-primary": "#…", … } record

without touching the DOM, handy if you want to inject the vars yourself.

1
2
3
4
5
import { applyThemeToRoot, themeToCssVars } from "@ekko/asgard";
import { themes } from "@ekko/asgard/theme";
 
applyThemeToRoot(themes.dracula); // write to :root immediately
const vars = themeToCssVars(themes.dracula); // { "--ekko-accent-primary": "#bd93f9", … }

How tokens map to CSS-var names

Each dotted token path becomes a dash-joined name under the prefix; the name field is excluded. For example:

Token pathCSS variable
background.primaryvar(--ekko-background-primary)
text.secondaryvar(--ekko-text-secondary)
accent.primaryHovervar(--ekko-accent-primary-hover)
border.dividervar(--ekko-border-divider)
semantic.errorvar(--ekko-semantic-error)
components.sidebar.backgroundvar(--ekko-components-sidebar-background)
opacity.hovervar(--ekko-opacity-hover)

Built-in palettes

themes (from @ekko/asgard/theme) is a map of ready-made palettes:

1
2
3
import { themes } from "@ekko/asgard/theme";
 
<ThemeProvider theme={themes.tokyoNight}></ThemeProvider>

There are 26 built-in themes. Each is either dark or light (the page background and every token follow it). The keys, grouped:

Dark (18): dark, dracula, monokai, nord, onedark, solarizedDark, gruvboxDark, tokyoNight, ayuDark, materialDark, catppuccinMocha, githubDark, palenight, rosePine, everforestDark, arctic, ubuntu, linuxMint.

Light (8): light, githubLight, solarizedLight, gruvboxLight, ayuLight, materialLight, catppuccinLatte, everforestLight.

The source of truth is Object.keys(themes). Note the exact casing: onedark (not oneDark), ayuDark/ayuLight (not ayu), materialDark/materialLight, everforestDark/everforestLight, catppuccinMocha/catppuccinLatte. Using a key that isn't in the map yields undefined, which ThemeProvider will not accept.

For a plain light/dark toggle, map your boolean to one dark key and one light key, the theme names themselves are not "dark"/"light" so pick a pair:

1
2
const [isDark, setIsDark] = useState(true);
<ThemeProvider theme={themes[isDark ? "githubDark" : "githubLight"]}></ThemeProvider>

Switching theme is just changing which object you pass:

1
2
const [name, setName] = useState("nord");
<ThemeProvider theme={themes[name]}></ThemeProvider>

Reading the theme

Inside a component, useTheme() returns the active theme and setters:

1
2
3
4
5
6
import { useTheme } from "@ekko/asgard";
 
function Panel() {
const { theme } = useTheme();
return <div style={{ background: theme.background.primary, color: theme.text.primary }}></div>;
}

useTheme() returns { theme, setTheme, themeName, setThemeName }. It must be called inside a ThemeProvider.

Token reference

A theme is a typed object (Theme, from @ekko/asgard/theme). The token tree below is the source of truth (src/theme/types.ts); the same paths are what <ThemeCssVars/> flattens into CSS variables.

background

FieldMeaning
background.primaryMain background
background.secondarySecondary background (panels, cards)
background.tertiaryTertiary background (hover states)
background.elevatedElevated elements (dropdowns, menus)

text

FieldMeaning
text.primaryMain text
text.secondarySecondary text (labels, descriptions)
text.disabledDisabled text
text.inverseText on colored backgrounds

border

FieldMeaning
border.defaultDefault borders
border.focusFocused element borders
border.dividerDividers and separators

accent

FieldMeaning
accent.primaryPrimary accent (buttons, links)
accent.primaryHoverPrimary accent hover
accent.primaryActivePrimary accent active/pressed
accent.secondarySecondary accent

interactive

FieldMeaning
interactive.hoverHover background
interactive.activeActive/pressed background
interactive.selectedSelected state
interactive.focusFocus ring/outline

semantic

FieldMeaning
semantic.errorError color
semantic.warningWarning color
semantic.successSuccess color
semantic.infoInformational color

components (per-component overrides)

FieldMeaning
components.sidebar.backgroundSidebar background
components.sidebar.itemHoverSidebar item hover
components.sidebar.itemActiveSidebar item active
components.sidebar.borderSidebar border
components.tab.backgroundTab strip background
components.tab.activeBackgroundActive tab background
components.tab.activeBorderActive tab border
components.tab.activeTextActive tab text
components.tab.inactiveTextInactive tab text
components.tab.hoverBackgroundTab hover background
components.tab.closeButtonHoverBackgroundTab close-button hover background
components.toolbar.backgroundToolbar/ribbon background
components.toolbar.buttonHoverToolbar button hover
components.toolbar.buttonActiveToolbar button active
components.toolbar.groupBorderToolbar group border
components.toolbar.groupLabelToolbar group label
components.menu.backgroundContext menu background
components.menu.itemHoverMenu item hover
components.menu.separatorMenu separator
components.menu.shadowMenu shadow
components.tooltip.backgroundTooltip background
components.tooltip.textTooltip text
components.tooltip.borderTooltip border
components.tooltip.shadowTooltip shadow
components.dropdown.backgroundDropdown background
components.dropdown.itemHoverDropdown item hover
components.dropdown.itemSelectedDropdown selected item
components.dropdown.borderDropdown border
components.scrollbar.thumbScrollbar thumb
components.scrollbar.thumbHoverScrollbar thumb hover
components.scrollbar.trackScrollbar track

opacity

FieldMeaning
opacity.disabledDisabled-element opacity (number)
opacity.hoverHover overlay opacity (number)
opacity.backdropBackdrop/scrim opacity (number)

Custom themes

A theme is just a typed object. Start from a built-in one and override what you need:

1
2
3
4
5
6
7
8
import { themes, type Theme } from "@ekko/asgard/theme";
 
const brand: Theme = {
...themes.nord,
accent: { ...themes.nord.accent, primary: "#5e81ac", primaryActive: "#4c6f9a" },
};
 
<ThemeProvider theme={brand}></ThemeProvider>

Nesting

Providers nest. Wrap a subtree in a second ThemeProvider to theme just that region, handy for a preview pane or a differently-themed panel inside an otherwise neutral app.

Inputs hand you the value, not an event

Asgard inputs are controlled and, by design, call your onChange with the value directly, never a DOM event. Write onChange={(v) => setX(v)}, not onChange={(e) => setX(e.target.value)}. This is uniform across the whole suite. The exact payload per input:

ComponentonChange signaturePayload
ButtononClick?: (e: MouseEvent) => voidButtons use onClick and DO receive the DOM mouse event (they are not value inputs)
TextBox(value: string) => voidThe input's string value
Select(value: string | number) => voidThe selected option's value
Slider(value: number | number[]) => voidThe number (or array for range sliders)
Checkbox(checked: boolean) => voidChecked state
CheckboxGroup(value: string[]) => voidArray of checked values
Switch(checked: boolean) => voidToggle state
Radio(checked: boolean) => voidChecked state
RadioGroup(value: string) => voidSelected value
ColorPicker(color: Color) => voidThe selected Color object
Calendar(value: Date | Date[] | null, selections?) => voidSelected date(s)
TimePicker(value: TimeValue) => voidThe selected TimeValue
DateTimeInput(value: DateTimeValue) => voidThe selected DateTimeValue

All Asgard inputs hand your onChange the VALUE directly, write onChange={(v) => setX(v)}, not e.target.value. (Button is the exception: it isn't a value input, so its onClick receives the DOM mouse event as usual.)

Buttons and forms

<Button> renders a native <button type="button"> by default, so it will not submit a surrounding form. To make a button submit (or reset) a form, set htmlType:

1
2
3
4
5
6
<form onSubmit={handleSubmit}>
<TextBox value={name} onChange={setName} />
<Button htmlType="submit">Save</Button> {/* submits the form */}
<Button htmlType="reset">Clear</Button> {/* resets the form */}
<Button onClick={doSomething}>Cancel</Button> {/* default: type="button", no submit */}
</form>

htmlType accepts 'button' (default), 'submit', or 'reset' and maps directly to the underlying <button type=...>. Because the default is 'button', you don't need a guard against accidental double-submits, an Asgard <Button> only submits when you ask it to.

Server-side rendering

Asgard components render on the server with ekko:rune and hydrate on the client. A few notes:

  • Select renders only its trigger button during SSR (the dropdown/listbox opens on interaction), so it is

SSR-safe and hydrates without layout shift.

  • The CSS-vars bridge is SSR-safe: <ThemeCssVars/> and applyThemeToRoot are no-ops on the server (no

document) and apply once the client mounts. SSR styling still comes from the components themselves and your initial SCSS.

  • Components that need a live DOM at mount, canvas-based pickers (ColorPicker's eyedropper checks

window), and drag-and-drop surfaces (Upload), guard their browser-only paths and light up after hydration. If you wrap such a feature yourself, gate it behind a mounted/isBrowser check rather than reading window/document during render.