CUI

Guide

Theming

CUI ships one carefully tuned look in two modes and eight accents. Most apps never need more than that, but every value is open to change.

Light, dark and accents

cui_set_theme(ui, CUI_THEME_DARK);          /* or CUI_THEME_LIGHT */
cui_set_accent(ui, CUI_ACCENT_BLUE);        /* NEUTRAL, VIOLET, BLUE, GREEN, AMBER, ORANGE, CORAL, ROSE */

Both changes animate: every color blends over about 300 ms in OKLab, so the whole interface fades from one theme to the other instead of flashing. You can also pick them up front:

cui_app_create(&(cui_app_desc){ .theme = CUI_THEME_DARK, .accent = CUI_ACCENT_GREEN });
cui_app_create(&(cui_app_desc){ .follow_system_theme = true });   /* Windows and macOS */
Neutral, lightGreen, dark

The neutral accent is near black in light mode and near white in dark mode. The colored accents are tuned per mode so text and controls keep their contrast.

Semantic colors

Components never use raw colors. They use tokens, and so should your own boxes and text, so they follow the theme:

RoleTokens
Page and surfacesCUI_COLOR_BACKGROUND, SURFACE, SURFACE_RAISED, SURFACE_MUTED
TextCUI_COLOR_FOREGROUND, TEXT_SECONDARY, TEXT_MUTED
LinesCUI_COLOR_BORDER, BORDER_SUBTLE, BORDER_STRONG
AccentCUI_COLOR_ACCENT, ACCENT_STRONG, ACCENT_SUBTLE, ACCENT_FOREGROUND
StatusCUI_COLOR_SUCCESS, WARNING, DANGER, INFO
InteractionCUI_COLOR_HOVER, CUI_COLOR_SCRIM
ChartsCUI_COLOR_SERIES(0..5)
cui_box(ui, .bg = CUI_COLOR_SURFACE_MUTED, .border = 1, .radius = 12, .pad = 16) {
    cui_text(ui, "Muted panel", .color = CUI_COLOR_TEXT_SECONDARY);
}

Tokens are ordinary cui_color values with a marker, so they work anywhere a color does. To get the concrete color, call cui_color_of(ui, CUI_COLOR_ACCENT) or cui_token_color(ui, CUI_TOKEN_ACCENT).

Spend the accent with intent: the selected item, the one series that matters, progress, focus. Status colors are for status only, always paired with a label or an icon.

Customizing

cui_theme_get returns the live theme. Change what you like; it applies on the next frame.

cui_theme *t = cui_theme_get(ui);
t->radius_control = 18;          /* pill shaped buttons and inputs (default 10) */
t->radius_panel = 20;            /* menus, popovers (14) */
t->radius_surface = 26;          /* cards, dialogs (18) */
t->control_md = 32;              /* denser controls (36) */
t->text_body = 14;               /* body text size (13) */
t->font_scale = 1.1f;            /* scale all text */
t->colors[CUI_TOKEN_ACCENT] = cui_oklch(0.62f, 0.19f, 145, 1);   /* your brand color */

cui_set_theme and cui_set_accent rebuild the colors but keep your radii, motion and font scale. To build a theme from scratch, fill all of colors.

Color helpers work in OKLab, like CSS color-mix(in oklab):

cui_color brand = cui_oklch(0.58f, 0.16f, 265, 1);     /* lightness 0..1, chroma, hue in degrees, alpha */
cui_color hover = cui_mix(brand, CUI_RGB(0xffffff), 0.12f);
cui_color faint = cui_alpha(brand, 0.15f);
cui_color hex   = CUI_RGB(0x7747ff);

Typography

RoleFontUsed for
CUI_FONT_BODYInter RegularBody text, controls
CUI_FONT_MEDIUMInter MediumLabels, buttons, headings under 28 pt
CUI_FONT_DISPLAYGeist MediumHeadings from 28 pt, big numbers
CUI_FONT_MONOGeist MonoCode
CUI_FONT_ICONSLucideIcons

The embedded fonts cover Latin, Latin-1 and Latin Extended-A plus common symbols. Replace a role or add fallbacks for other scripts:

cui_font brand = cui_font_load_file(ui, "fonts/MyBrand-Regular.ttf");
cui_font_set_role(ui, CUI_FONT_BODY, brand);

cui_font cjk = cui_font_load_file(ui, "fonts/NotoSansCJK-Regular.otf");
cui_font_add_fallback(ui, CUI_FONT_BODY, cjk);      /* missing glyphs come from here */
cui_font_add_fallback(ui, CUI_FONT_MEDIUM, cjk);

Text is rasterized with subpixel positioning and kerning at the exact pixel size of the display, so it stays crisp at 100%, 150% and 200%.

Motion

Animations are springs. Values that report state (a switch thumb, a progress bar, a panel height) land without overshoot. Small feedback (presses, highlights) gets a little life. Exits are faster than entrances.

cui_set_reduced_motion(ui, true);    /* snap to final states; keep feedback */
cui_theme_get(ui)->motion = 2.0f;    /* slow motion, handy when designing */

Respect your users' system setting by passing it in at startup.

Shadows

Only floating layers (menus, popovers, dialogs, toasts) cast shadows. Cards rest on a border. The three elevations come from the theme and are tuned separately for light and dark:

cui_box(ui, .elevation = CUI_ELEVATION_RAISED, .bg = CUI_COLOR_SURFACE_RAISED, .radius = 16) { ... }
t->shadow_floating[0] = (cui_shadow){ .y = 24, .blur = 60, .alpha = 0.14f };