lotusui the ikaito design language for Gio — desktop, mobile, web
GitHub

Theming

Paired role tokens, color scales, schemes — resolved once at NewTheme.

Palette is the single source of truth for color: named semantic tokens instead of raw hex scattered through views. Tokens come in pairs — a fill and its readable ink (BrandSolid+BrandContrast, Danger+DangerContrast, each status ink with its pastel background) — so a component that paints a surface always knows what stays readable on it. Dark mode is just another palette: see Dark mode.

Tokens

The standard semantic vocabulary: background ladder (Bg, BgSubtle, BgMuted, BgEmphasized, BgPanel, BgInverted), borders (BorderSubtleBorderEmphasized), a foreground ladder (FgFgDisabled, plus FgInverted), the brand slots (BrandSolid, BrandSubtle, BrandFg, BrandEmphasized, BrandContrast), four status pairs, the dialog Overlay and the keyboard FocusRing. Space is the 8pt spacing scale; Radius the corner scale.

Go
th := lotusui.NewTheme()

// Semantic tokens, never raw hex in views:
bg := th.Palette.Bg   // the canvas
ink := th.Palette.Fg  // primary text

// Scales:
gap := th.Space.MD  // 16dp
r := th.Radius.MD   // 10dp

If you come from the web

The tokens are a direct transposition of the semantic-token vocabulary web component libraries use — dotted paths become CamelCase, because in Go a field can't be both a color and a namespace (fg can't simultaneously be Fg and hold Fg.Muted; flattening keeps the most-used token the shortest).

Web tokenlotusui
bg, bg.subtle, bg.muted, bg.emphasized, bg.panel, bg.invertedBg, BgSubtle, BgMuted, BgEmphasized, BgPanel, BgInverted
fg, fg.muted, fg.subtle, fg.invertedFg, FgMuted, FgSubtle, FgInverted (+ FgDisabled for placeholders)
border, border.muted, border.subtle, border.emphasizedBorder, BorderMuted, BorderSubtle, BorderEmphasized
colorPalette.solid / .subtle / .fg / .emphasized / .contrastBrandSolid, BrandSubtle, BrandFg, BrandEmphasized, BrandContrast
fg.success / bg.success (and warning, info, error)Success/SuccessBg, Warning/WarningBg, Info/InfoBg, Danger/DangerBg/DangerContrast
focus ring / dialog overlayFocusRing / Overlay

Schemes — variants carry their role

A Scheme is a semantic color scheme: how one color role renders across a component's variants — including its interaction steps (base, hover, active). The palette exposes three — Accent() (the brand), Neutral() (the greys), and DangerScheme() (destructive actions only) — and each VARIANT resolves its own: Default→Accent, Secondary/Outline/Ghost→Neutral, Destructive→Danger. Every scheme field is a palette token or scale step — nothing hard-coded — so a custom palette propagates to every variant and every interaction state.

Go
accent := th.Palette.Accent()
// accent.Solid / SolidHover / SolidActive, OnSolid,
// accent.Subtle / SubtleHover / SubtleActive, OnSubtle, Outline

Per-instance color — the precedence ladder

Three layers, most-derived wins. Default: the variant's role scheme from the theme. Per instance: Color (any ColorScale) re-colors the variant — one anchor in, the whole .500/.600/.700 interaction ladder out, so a custom color can never break its own hover and pressed states. Full control: Scheme overrides every slot by hand.

Go
lotusui.Button(th, &b, "Save", lotusui.ButtonOpts{})                      // theme role
lotusui.Button(th, &b, "Go", lotusui.ButtonOpts{Color: lotusui.Teal})     // scale — ladder derived
soft := lotusui.Teal.SoftScheme()
lotusui.Button(th, &b, "Go", lotusui.ButtonOpts{Scheme: &soft})           // manual slots

Color scales

A ColorScale is one hue graded 50…900 — 50 the faintest tint, 500 the anchor, 900 the darkest ink. Ten stock scales ship (Gray through Pink), and ScaleFrom grades a complete scale from any single color — one brand color in, ten usable steps out. A scale becomes a button scheme via .Scheme() (saturated, .500/.600/.700 interaction steps) or .SoftScheme() (pastel).

Your brand's scale needs no setup at all: th.BrandScale is graded automatically from the palette's BrandFg when the theme is built — define one custom brand color and the whole 50…900 ladder is already there.

Go
// Automatic — derived from your palette at NewTheme:
tint := th.BrandScale.C50
ink := th.BrandScale.C700
scheme := th.BrandScale.Scheme()

// Or grade any color on the spot:
mint := lotusui.ScaleFrom(color.NRGBA{R: 0x31, G: 0x97, B: 0x95, A: 0xFF})

Multiple themes

A theme is plain data — a Palette is ~200 bytes — so an app can compile in as many looks as it wants and let its users switch: build the themes once at startup, swap a pointer on selection. No parsing, no assets, no measurable weight. The palette picker in this site's top bar is exactly that pattern live: eleven presets compiled into both the pages and the wasm demos; picking one restyles everything on the fly.

Go
var themes = map[string]*lotusui.Theme{
	"lavender": lotusui.NewTheme(),
	"teal":     lotusui.NewTheme(lotusui.WithPalette(tealPalette)),
	"rose":     lotusui.NewTheme(lotusui.WithPalette(rosePalette)),
}

th := themes[userChoice] // switching themes is a pointer swap

Theming

Customization flows through NewTheme options and resolves once, at construction — a fully custom look has zero per-frame cost. Components read every color from th.Palette, every corner from th.Radius, every gap from th.Space; swap any of them and the whole library follows consistently. WithFaces replaces the embedded DM Sans with your brand font.

The fastest start: generate a palette from your brand color — go run github.com/ikaito-com/lotusui/cmd/lotusui theme -anchor '#319795' -o theme_gen.go grades the anchor into a scale, maps it onto the brand tokens, and emits plain Go you own.

For the full declarative workflow, keep a theme.json (anchor, token overrides, radius/space scales, text size) and regenerate with -config theme.json: designers edit JSON, the build turns it into typed literals, and the runtime never parses anything. Because this runs at build time it also validates: every text-on-surface pair is contrast-checked (WCAG 4.5:1), and -strict makes an unreadable palette a build failure instead of a shipped bug.

Go
brand := lotusui.DefaultPalette
brand.BrandSolid = color.NRGBA{R: 0xD9, G: 0xF3, B: 0xE4, A: 0xFF}
brand.BrandFg = color.NRGBA{R: 0x11, G: 0x7A, B: 0x3D, A: 0xFF}

th := lotusui.NewTheme(
	lotusui.WithPalette(brand),
	lotusui.WithRadius(lotusui.RadiusScale{SM: 4, MD: 8, LG: 12}),
	lotusui.WithTextSize(15),
)

Every demo on this site is the real Go component compiled to WebAssembly — one gallery app, one bundle, addressed by URL hash.