Theming
A shadow root is opaque, which protects the component author and blocks everyone else. The platform provides three ways through; alacris uses all three. From Go you reach the first one.
Custom properties
Section titled “Custom properties”A component declares what it is themed by with vars():
import { vars, css } from 'alacris';
const chip = vars('chip', { bg: '#eee', fg: '#111', radius: '999px' });// chip.bg === 'var(--chip-bg, #eee)'
define('ala-chip', { styles: css`:host { background: ${chip.bg}; color: ${chip.fg}; border-radius: ${chip.radius} }`, setup: () => html`<slot></slot>`,});Custom properties inherit straight through the shadow boundary, so setting one on the element, or anywhere above it, reaches inside:
chip := alacris.Vars("--chip-bg", "--chip-fg", "--chip-radius")return alacris.E("ala-chip"). Apply(chip, map[string]string{ "--chip-bg": "#ffe9a8", "--chip-radius": "999px", }). Text("new")<ala-chip style="--chip-bg:#ffe9a8;--chip-radius:999px">new</ala-chip>Custom properties reach through the shadow boundary.
Making the contract checkable
Section titled “Making the contract checkable”Var will set any property you name, including one the component does not
declare. VarSet is the version that knows the difference:
var ChipVars = alacris.Vars("--chip-bg", "--chip-fg", "--chip-radius")
alacris.E("ala-chip").Apply(ChipVars, map[string]string{ "--chip-bg": "#ffe9a8",})A key outside the contract fails the render rather than writing a property nothing reads:
alacris.E("ala-chip").Apply(ChipVars, map[string]string{"--chip-colour": "#111"})// render fails: alacris: --chip-colour is not part of this theming contract// (have: --chip-bg, --chip-fg, --chip-radius)That turns a rename in the component into a failing request instead of styling that quietly stops working.
Generating it
Section titled “Generating it”Declare the properties in the component’s JSDoc and the contract is generated along with everything else:
/** * @cssprop [--chip-bg=#eee] - the background * @cssprop [--chip-fg=#111] - the text colour * @cssprop [--chip-radius=999px] - the corner radius */define('ala-chip', { props: { tone: 'neutral' }, setup });// generatedvar ChipVars = alacris.Vars( "--chip-bg", // the background (defaults to #eee) "--chip-fg", // the text colour (defaults to #111) "--chip-radius", // the corner radius (defaults to 999px))@components.Chip(components.ChipProps{}).Apply(components.ChipVars, map[string]string{"--chip-bg": "#ffe9a8"})NewVarSet builds the same thing from a prefix and camelCase keys, mirroring
vars() directly:
alacris.NewVarSet("chip", "bg", "fg", "borderRadius")// --chip-bg, --chip-fg, --chip-border-radiusWhat gets refused
Section titled “What gets refused”Values are checked, not escaped. Anything that could end the declaration, end the attribute, or open a comment is an error:
.Var("bg", "red;position:fixed") // ';' is not allowed.Var("bg", "red/*x*/") // comment markers are not allowed.Var("bg", "url(javascript:...)") // "javascript:" is not allowedColour and length functions are fine, because that is what a themable value usually is:
.Var("bg", "oklch(0.7 0.1 200)").Var("bg", "color-mix(in srgb, var(--brand) 40%, transparent)")The other two routes
Section titled “The other two routes”They are CSS, not Go:
::part reaches a specific internal the component chose to expose. Outer
rules beat the component’s own, so a consumer always wins:
ala-chip::part(dot) { width: 14px }adoptGlobal pushes a stylesheet into every alacris component on the page,
including ones that do not exist yet. The escape hatch for restyling a
component set whose source you do not control:
import { adoptGlobal, css } from 'alacris';const remove = adoptGlobal(css`:host { font-family: Inter, system-ui }`);One rule
Section titled “One rule”Page-level theming
Section titled “Page-level theming”Because custom properties inherit, the most maintainable place to set them is usually your ordinary stylesheet, not Go:
ala-chip { --chip-bg: #ececf2; --chip-fg: #333 }.dark ala-chip { --chip-bg: #2b2b33; --chip-fg: #eee }[data-tone="warn"] { --chip-bg: #ffe9a8 }Use Apply when the value is per-element and comes from data (a category
colour, a progress percentage) rather than from design.
The design system
Section titled “The design system”Alacris UI is the page-level version of the
same idea. Config.UI: true loads sixty-eight Material Design 3 components
and calls applyTheme with the module’s defaults (seed #e8ad18, Google
Sans Flex, scheme from the OS). Config.Theme then writes one stylesheet of
system tokens; every component consumes them, so a seed colour re-skins the
page without touching an element.
zero := 0.0alacris.Config{ UI: true, Theme: alacris.Theme{ Seed: "#0b57d0", Scheme: "dark", Radius: &zero, // square corners; a pointer so 0 is distinct from unset },}Theme.Colors names palettes directly (Primary, Error, …). Theme.Overrides
writes raw tokens last (color-primary, radius-md) for light, dark, or
both. Per-component ui.ButtonVars and ::part still work on top.
Radius, Motion, Density and LoadFonts are pointers so Go’s zero value
means “leave the Material default” rather than “square / instant / compact /
don’t load fonts”.