Looks
Astrolabe has three looks: the default look, which needs nothing, and two built-in style templates, playful and
notebook. A look is a named set of design token overrides for the light and the dark theme, plus ::part()
declarations for components. It changes how an exercise looks, never how it works or grades.
The player and the editor render in their own shadow roots, so your page CSS cannot break them. You style them
through three documented surfaces: design tokens (--astl-* custom properties), themes and parts; a look
bundles tokens and parts under a name that authors pick.
The three looks
Section titled “The three looks”The same exercise in each look, running in this page (switch the site theme to see their dark values):
<astl-doc lang="en" style-template="notebook"> <h2>Animals</h2> <p>An elephant is <astl-gap id="g1" answer="grey"><astl-alt>gray</astl-alt></astl-gap>.</p> <astl-choice id="q1"> <astl-prompt>How many legs does a spider have?</astl-prompt> <astl-option id="a" correct>Eight</astl-option> <astl-option id="b">Six</astl-option> <astl-option id="c">Ten</astl-option> </astl-choice></astl-doc>Default look
Section titled “Default look”No style-template anywhere: the base tokens.
Animals
An elephant is
Playful
Section titled “Playful”style-template="playful": rounder shapes, bolder lines and brighter colours.
Animals
An elephant is
Notebook
Section titled “Notebook”style-template="notebook": cream paper on a kraft desk, ink-blue text, a book face and small corners.
Animals
An elephant is
Their colours, parts and contrast ratios: Built-in style templates.
What a look never changes:
- Grading and behaviour. A look styles; the same source grades the same in every look.
- High contrast. In
theme="hc"and underforced-colors: activeno look applies, so readers who need high contrast always get it. - Reduced motion. Under
prefers-reduced-motion: reducedurations stay zero, whatever a look sets. - Verdicts. A look's
::part()rules cannot touch borders, outlines, colours, display or text size (see the rules), and its colour tokens keep the contrast pairs, so a verdict keeps its icon, word and border style.
Pick a look
Section titled “Pick a look”Authors write style-template on <astl-doc> for the whole exercise, or on one component:
<astl-doc lang="en" style-template="notebook"> <p>A young cat is called a <astl-gap id="cat" answer="kitten"></astl-gap>.</p> <astl-choice id="sky" style-template="playful" variant="cards"> <astl-prompt>What colour is a clear sky at noon?</astl-prompt> <astl-option id="blue" correct>Blue</astl-option> <astl-option id="green">Green</astl-option> </astl-choice></astl-doc>- The closest one wins: the component's look, then the document's, then the player's default, which the host
sets with
<astl-player style-template="notebook">(or thestyleTemplateproperty). - Scopes. The document's look styles the whole player: the exercise canvas and the chrome (page navigation, timer, buttons, result). A component's look styles that element and everything inside it; tokens it does not set come from the document's look.
- In the editor. The inspector of
<astl-editor>shows Look after every component's own fields ("Same as the exercise" removes it), and Look of the whole exercise for<astl-doc>when the cursor is outside every component: "Default look", "Playful", "Notebook", or your own looks, labelled from their catalogs. The preview plays the exercise in its looks, the way a student sees it. With no looks registered, the editor offers no look controls. - Unknown names are ignored. A name the player does not know (such as the old
calm), or a look not made for that place (a component-only look on<astl-doc>, a look limited to other tags), changes nothing: the exercise plays in the look around it. The engine reports the warningSTYLE_TEMPLATE_UNKNOWNwith{ name, tag }and keeps the attribute in the view, so a player that knows more looks can still use it.
Variants such as cards are another thing: they come with a component and may change how it behaves
(Variants and versions); a look restyles any variant.
Themes
Section titled “Themes”| Theme | What it is |
|---|---|
light |
dark text on light surfaces |
dark |
light text on dark surfaces |
hc |
high contrast: black on white, every text pair at least 7:1, selection marked with a bar and bold text |
auto |
the default of <astl-player> and <astl-editor>: light or dark, following prefers-color-scheme |
<astl-player theme="hc">and<astl-editor theme="hc">pick the theme; each writes it to its owndata-theme, and<astl-editor>passes it to its kit and preview.- The kit elements used on their own read
data-themedirectly (<astl-inspector data-theme="dark">; without it,light).<astl-preview>passes its theme on to the player inside it. - Looks have light and dark values; with
autothey followprefers-color-schemelike the themes do (a value set for light only never reaches a dark system). - Under
forced-colors: active(Windows high contrast) every colour token becomes a system colour, whatever the theme. Underprefers-reduced-motion: reduceboth durations become0ms.
Override tokens with CSS
Section titled “Override tokens with CSS”Every colour, font, size, space, radius, width and duration is a token. The full list with values, swatches and contrast ratios: Design tokens.
Each element carries the tokens on its own host, so override them on the element itself. A value set on a parent
(such as body) does not reach inside.
astl-player,astl-editor { --astl-accent: #6b2fb3; --astl-focus: #5a2599; --astl-font-body: "Atkinson Hyperlegible", system-ui, sans-serif; --astl-radius-m: 12px; --astl-target-min: 48px;}Component elements (<astl-gap>, <astl-choice>) sit inside the player and inherit its tokens, so one rule on
astl-player styles them too. Tokens set on <astl-editor> reach its kit, its preview player and its TipTap surface.
The kit elements used on their own (astl-insert-menu, astl-inspector, astl-issues, astl-preview) take the
rule each.
A rule without a theme selector applies to every theme. To change one theme only, add its attribute (the player and
the editor write their theme to data-theme):
astl-player[data-theme="dark"] { --astl-accent: #c7a6ff;}@media (prefers-color-scheme: dark) { astl-player[data-theme="auto"] { --astl-accent: #c7a6ff; }}Keep the contrast pairs. Text on its backgrounds needs at least 4.5:1 (7:1 in high contrast); borders, focus rings
and verdict colours at least 3:1. The token reference lists every pair that has to
hold; --astl-on-accent on --astl-accent, for example, is the text of the primary button. Never shrink
--astl-target-min below 44px or remove the focus ring (--astl-focus, --astl-focus-width).
Keep picks apart from verdicts. A picked option or card shows --astl-selected where a checked one shows the
verdict colour, so --astl-selected stays at least 25 (CIEDE2000) from --astl-success and --astl-danger, and
--astl-selected-soft at least 7 from their soft fills: a teal pick next to a green "correct" would read as checked.
The check reports it as pick-distance.
The page around the player. To style your own page with the same tokens and themes:
-
With the script-tag bundles, link
astrolabe.css(indist/cdn/). It sets the tokens on:rootand on.astl-theme, with the samedata-themevalues:<html data-theme="auto"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/astrolabe@0.1/dist/cdn/astrolabe.css" /> -
With a bundler, write the stylesheet for any selector with
tokensCssfromastrolabe/playerorastrolabe/editor, as the editor examples do. An adopted stylesheet is not an inline style, so it works under a strict Content Security Policy:import { tokensCss } from 'astrolabe/player';const page = new CSSStyleSheet();page.replaceSync(tokensCss(':root'));document.adoptedStyleSheets = [...document.adoptedStyleSheets, page];
tokensCss(selector) writes the light values on the selector, the data-theme="dark", "hc" and "auto" rules,
system colours for forced-colors: active and zero durations for prefers-reduced-motion: reduce. Then use the
properties in your CSS, for example background: var(--astl-canvas).
Looks win over host tokens. A look's values apply inside the player, over the --astl-* values you set on
<astl-player> itself: the look the author picked is the one the student sees. To adjust a built-in look, register
your own look under another name.
Group colours and switches
Section titled “Group colours and switches”Two kinds of tokens do a job beyond one colour or size:
- The group palette:
--astl-palette-1to--astl-palette-8, each with a soft fill--astl-palette-<n>-soft. Components that tell groups apart give them the slots in order and start again after the eighth: the categories of<astl-categorise>(an author can pin one to a slot with<astl-category palette="3">), the joined lines of matching, the pairs turned up in memory, the words found in a word search. The slots are never red or green (the verdict colours), neighbours stay apart for red-green colour blindness, each colour reads as text on the page and on its soft fill, and a group always has a label or a pattern too, so colour never carries the meaning alone. Override them only together and keep those rules; their values per theme are on the token reference. Under forced colours they becomeCanvasTextandCanvas. - The separator switch:
--astl-separator-displayisnone, and a look sets it toinlineto show a separator between options laid out as words, such as the buttons of<astl-select variant="buttons">("mat / hat"). The built-innotebooklook does, as printed exercises do. The separator is decoration: screen readers skip it.
Parts are named elements inside a shadow root that you can style with ::part().
| Element | Parts |
|---|---|
<astl-player> |
player (the root), header (page counter, progress, timer), canvas (the exercise), result (score and counts), footer (navigation and actions), problem-details (the errors of an invalid source) |
<astl-editor> |
surface, insert-menu, inspector, issues, preview |
<astl-insert-menu> |
root, title, button, icon |
<astl-inspector>, <astl-issues> |
root, title |
<astl-preview> |
root, title, button, player (the <astl-player> inside) |
astl-player::part(result) { border-inline-start: 4px solid var(--astl-accent);}astl-editor::part(preview) { margin-block-start: 2rem;}Each component also declares part names in its manifest (cssParts), for example input and verdict on
<astl-gap>; the component reference lists them. The player mounts components inside its
own shadow root, so these parts are reachable only where you place a component element in a scope of your own. Inside
<astl-player>, style components through the tokens, or through the parts of a look.
Write your own look
Section titled “Write your own look”import type { StyleTemplate } from 'astrolabe/player';
export const storybook: StyleTemplate = { name: 'storybook', version: 1, scopes: ['document', 'component'], messages: 'acme.style.storybook', catalogs: [ { namespace: 'acme.style.storybook', locale: 'en', messages: { label: 'Storybook', description: 'Round cards and a book face.' }, }, { namespace: 'acme.style.storybook', locale: 'pl', messages: { label: 'Bajka', description: 'Okrągłe karty i książkowa czcionka.' }, }, ], tokens: { 'font-display': 'Georgia, serif', 'radius-m': '14px', 'radius-l': '20px' }, light: { accent: '#7a2fb3', focus: '#7a2fb3', selected: '#1f5bc6', 'selected-soft': '#e8effc' }, dark: { accent: '#d0a8ff', focus: '#d0a8ff', selected: '#8fb8ff', 'selected-soft': '#16284a' }, parts: { 'astl-choice': { option: { 'box-shadow': '0 2px 0 var(--astl-line)' } } },};| Field | Meaning |
|---|---|
name |
What authors write in style-template: lowercase kebab-case, 1 to 40 characters, starting with a letter. Keep it apart from variant names such as cards. |
version |
Bump it when the look changes visibly; exercises keep naming the look. |
scopes |
document, component or both. |
components |
Tags it supports at the component scope; leave it out for every registered component. |
messages, catalogs |
The i18n namespace and its catalogs, with a label (the inspector's option) and a one-line description, in English and Polish. |
tokens |
Token overrides for light and dark, by token name without --astl- (Design tokens). |
light, dark |
Overrides on top of tokens for one theme. |
parts |
::part() declarations by component tag, then part name (the manifest's cssParts, listed on each component page). |
Rules the player enforces:
- Part properties are limited to shape, spacing and type: corner radii,
box-shadow, padding, gaps,font-family,font-style,letter-spacingandline-height(PART_PROPERTIESin@astrolabe/tokens). A::part()rule from outside beats the component's own styles, so borders, outlines, colours, display and text size stay with the component: they carry the verdict border styles, focus rings, selection and contrast. Other properties are dropped. - Values must be safe: a value with
{,},;,<,>, a backslash, a line break, a comment opener, an unclosed quote or unbalanced( )or[ ]is dropped, so a look can never break the stylesheet around it. - Write part values with tokens (
var(--astl-line)), so they follow the theme.
Colours: a look may set a few colour tokens, as above, or the whole scheme, as the built-ins do. The colours it leaves out come from the scheme around it, and a component look's text also sits on the backgrounds of the scope around it, where the component paints none; the check covers both.
Register it in all three places, with the built-in looks, since an option replaces its default ([storybook]
alone would leave only storybook and the default look):
import { builtInStyleTemplates, definePlayer } from 'astrolabe/player';import { createEngine } from 'astrolabe/server';import { defineEditor } from 'astrolabe/editor';import { storybook } from './storybook.js';
const styleTemplates = [...builtInStyleTemplates, storybook];
definePlayer({ styleTemplates }); // players apply itconst engine = createEngine({ secret: process.env.ASTL_SECRET, styleTemplates }); // validation knows itdefineEditor({ styleTemplates }); // the inspector offers it and the preview shows itdefinePlayeranddefineEditorcan be called again with more looks: a second call adds, and players already on the page restyle at once. A name that another look already took throws (two copies of a bundle would otherwise mix silently).createEnginethrows for a name registered twice or a name that is not lowercase kebab-case.- The script-tag bundles register the built-in looks on load and export
definePlayer(andastrolabe-editor.jsdefineEditor), so a page adds its own look with one more call, such asdefinePlayer({ styleTemplates: [storybook] }). For the in-browser engine ofastrolabe.jsthat look is unknown, which is only a warning: the exercise compiles and the player applies the look. - With the
@astrolabe/*packages, pass the same list todefinePlayer,createEngineandcreateAuthoring.
Check a look in CI
Section titled “Check a look in CI”checkStyleTemplate from @astrolabe/tokens (add it as a dev dependency) returns the problems of a look, an empty
list when it is fine: the name, version, scopes, component tags, messages, token names, unsafe values, colours that
are not #rgb or #rrggbb (their contrast cannot be checked), part properties and names, accessible minimums
(--astl-target-min at least 44px, --astl-focus-width 2px, --astl-outline-width 1px and text sizes 14px, written
in px or rem), and contrast. Every text pair must reach 4.5:1 and every border, focus ring and verdict colour 3:1, in
light and in dark (the pairs are on the Design tokens page); component looks are also
checked against the backgrounds around them.
import { builtInManifests } from 'astrolabe/engine';import { builtInStyleTemplates, checkStyleTemplate } from '@astrolabe/tokens';import { storybook } from './storybook.js';
// Tags and part names must exist in the manifests.expect(checkStyleTemplate(storybook, { manifests: builtInManifests })).toEqual([]);// The same look on components inside an exercise that uses a built-in look.for (const around of builtInStyleTemplates) { expect(checkStyleTemplate(storybook, { within: [around] })).toEqual([]);}contrastRatio(a, b) (WCAG 2) and templateColors(theme, templates) (the colours of a theme with looks applied in
order) are exported for your own checks.
How it works
Section titled “How it works”Every <astl-player> adopts one generated stylesheet, shared by all players on the page and rebuilt in place when
registrations change. The document's look is mirrored on the player's root as data-style-template; the host's
default matches the player's own style-template attribute and applies only when the view names no document look the
player knows. Cascade layers order the scopes (player default, then document, then component), every rule excludes
[data-theme="hc"], and the whole sheet sits inside @media (forced-colors: none). Constructed stylesheets are not
inline styles, so looks work under the strict Content Security Policy of the security guide.