Skip to content

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.

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")
renders verified by go test
<ala-chip style="--chip-bg:#ffe9a8;--chip-radius:999px">new</ala-chip>
which the browser turns into the real runtime, upgrading that markup
new

Custom properties reach through the shadow boundary.

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.

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 });
// generated
var 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-radius

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 allowed

Colour 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)")

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 }`);

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.

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.0
alacris.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”.