Design System Reference
Living reference for pure HTML/CSS prototyping (WCAG 2.2 AA on light backgrounds). Last updated 14 June 2026. Stored in BeameryHQ/design-artefacts, aligned to BeameryHQ/lib-design-system.
The design system ships as three files in this folder — styles.css (tokens &
components), sprite.svg (logos & icons) and scripts.js (behaviours). Use
starter.html — a shell page which already links all the right files — to get started.
Table of contents
Colours
Components must consume semantic tokens (e.g. text.*, borders.*, backgrounds.*, controls.*), never raw primitive shades.
Primary band colours
Click a row to copy the hex.
Key colours
Click a row to copy the hex. These shades are most often referenced in semantic tokens and components.
Primitives – Full colour scales: neutral, indigo, red, yellow, green (50 → 950)
Click to copy hex values. Solid swatches are in-use theme colours.
neutral
Greyscale
indigo
Primary accent
red
Semantic error
yellow
Semantic warning
green
Semantic success
Typography
Single family (Inter / system stack). Hierarchy by size and weight. Default: 14px / 400 / neutral-900.
H1 – Page (24px / 800 / 36px)
H2 – Section (20px / 700 / 32px)
H3 – Section or Modal (16px / 700 / 24px)
H4 – Small Header (14px / 700 / 24px)
H5 – Group Title (12px / 700 / 20px / uppercase)
Body – default product copy at 14px.
Body Emphasised – 600 weight for inline emphasis.
Small – 12px for captions and metadata.
Secondary – neutral-700 for descriptions and hints.
Link – brand primary, darkens on hover.
Error text – validation messages.
Success text – confirmations.
Table
Every column is sortable – click toggles ascending/descending (aria-sort); the unsorted
indicator shows bidirectional arrows in text.secondary, the active column's arrow turns
controls.primary. Headers: 12px / 400, text.secondaryOnGray on backgrounds.secondaryOnGray.
Spacing, Sizing & Shape
4px grid. Gap classes .gap-1….gap-8; most common gap is 16px (--space-4).
Control height: 32px (lib-design-system sizes.x8) for buttons, inputs and selects; chips are 28px. Every interactive target clears the 24×24 minimum (SC 2.5.8).
Radii: 4px (sm – connector chips, small controls), 6px (md – buttons, inputs, selects: the workhorse), 8px (lg – cards), 9999px (pill – value chips, status pills, toggle tracks).
Logo & Icons
All assets ship as SVG sprite symbols. Click an icon to copy its SVG source.
Logo
Full wordmark, 118:32. The bexa mark is always brand-orange; the wording defaults to neutral-900, brand-navy on marketing surfaces.
Bexa
The logo mark without the wording, brand-orange. Scales freely.
Icon library (151 icons)
Most icons are stroke-only – outline icons using stroke="currentColor" – set color on the parent to tint. Partner marks and special cases appear at the end.
Buttons
Base class .btn + variant. One filled primary action per surface.
The secondary family carries a visible resting border (borders.form, rendered as an inset box-shadow).
Add a leading sprite with .btn-icon; use .btn-icon-only for icon-only controls (include aria-label).
Action menus attach a floating .list-panel to a secondary button — labelled or overflow (three dots).
Variants
Disabled = 0.5 opacity + cursor: not-allowed.
With icons
Icon-only
Small (28px)
.btn-sm matches chip height — for inline filter rows and compact toolbars.
Action menus
Overflow commands in a .list-panel (role="menu"). Wire with DS.initActionMenu.
Use a labelled btn-secondary trigger or an icon-only overflow button (ds-icon-more).
Icons only on high-signal items; secondary actions stay text-only.
States (secondary family)
| State | Fill | Border (inset) |
|---|---|---|
| Rest | controls.secondary (neutral-200) | borders.form (neutral-700) |
| Hover / Focus | controls.secondaryHover (neutral-400) | borders.formActive (neutral-900) |
| Active | controls.secondaryActive (neutral-500) | borders.formActive |
| Pressed (toggled) | – | borders.primary |
Inputs
Grey-fill control with a visible resting border. Placeholders never replace a visible <label>.
.typeahead-results as alias of .list-panel.
DS.initSelectPicker
data-select-any or the empty <option> text). Prefix inside trigger when a value is chosen (Sort by: Match score). No external label. Inline auto-width variant — .select-picker-labeled without .select-full.
.select-picker-chip or .qualifier-select inside .keyword-group.
States
Text inputs
| State | Fill | Border | Notes |
|---|---|---|---|
| Rest | controls.secondary (neutral-200) | borders.form (neutral-700) | Placeholder forms.placeholder (neutral-700; 750 on grey surfaces) |
| Hover | controls.secondaryHover (neutral-400) | borders.formActive (neutral-900) | |
| Focus | white | borders.primary (indigo-500) | + focus ring |
| Error | neutral-200 | borders.error (red-500) | Label text.error; aria-invalid + aria-describedby |
| Disabled | neutral-200 | borders.form | 0.5 opacity |
Select picker
btn-secondary trigger + list-panel listbox — same chrome as the Actions menu.
| State | Trigger | Panel | Notes |
|---|---|---|---|
| Rest | controls.secondary + caret | hidden | Selected value on trigger |
| Open | aria-expanded="true" | white surface + shadow | Click-outside / Escape closes |
| Option hover | – | backgrounds.secondary | |
| Selected option | – | aria-selected="true" | Medium weight + primary text |
| Focus | focus ring | item focus ring |
Select picker (labeled)
Prefix label inside the trigger — .select-picker-labeled + data-select-label. Selected: Sort by: value. Empty: Any … via data-select-any or empty <option> text. Full width with .select-full; inline auto-width without.
| State | Trigger | Panel | Notes |
|---|---|---|---|
| Selected | Label: + value + caret | hidden | Prefix bold; value regular weight |
| Empty / default | Any … + caret | hidden | Prefix hidden; full “Any X” phrase bold |
| Open | aria-expanded="true" | white surface + shadow | Same list-panel behaviour |
Select picker (chip)
28px pill trigger matching .chip — for chip-pair keyword rows (location radius, language level). Add .select-picker-chip or use .qualifier-select inside .keyword-group (excludes .threshold-value-select, filter-level .status-select).
| State | Trigger | Panel | Notes |
|---|---|---|---|
| Rest | white fill + borders.form + 12px caret | hidden | Same geometry as value chip |
| Hover | backgrounds.secondary + borders.formActive | – | |
| Open | aria-expanded="true" | white surface + shadow | Click-outside / Escape closes |
| Focus | focus ring | item focus ring |
Chips and toggles
Status pills, boolean connectors, interactive chips, single checkboxes, and on/off switches – shared affordances for filters, selections, and settings.
Pills
Compact status labels – 12px / 500, full pill radius. *-100 fill with matching dark text token.
Typically not interactive; used to communicate state (e.g. Active, Pending).
Boolean connectors
Boolean-logic connectors for the filter system. 11px / 700, radius 4px. All pairings ≥ 4.5:1 (AA).
Full state spec in Filter System/connector-chip-spec.md.
Chips
28px pills. Resting border borders.form; hover darkens the border, not just the fill.
Toggles
Off track is neutral-700 (≥3:1 vs page and thumb); on is brand primary. Three non-colour cues: thumb position, track colour, icon (SC 1.4.1). Use for immediate on/off settings with no separate save step. Default track is 44×24 (18px knob + 3px padding). When a label is used, it must declare the active state and stay the same when off — never swap to an inactive phrasing (that confuses which side is on).
Default – 24px track
Compact exploration – 16px track
Smaller visuals for dense panels (e.g. filter rows). Hit target kept at ≥24×24 via padding on the label (.toggle--compact-hit), not the track – otherwise the thumb looks undersized inside a padded pill.
Checkbox
Binary choices that apply on Save / Apply / Submit, or as part of a batch action. Single option: 16×16 box, 4px radius, label at 14px / 400. Resting border borders.form; hover border borders.primary; checked fill controls.primaryActive with white check. Matches the lib-design-system Checkbox component.
Single option
When to use toggle vs checkbox
toggle = “turn this on now”; checkbox = “include this”.
Use a toggle when flipping the control should take effect immediately — a live on/off for a single setting. Examples: enable/disable an active filter without clearing it, show/hide a panel section in place.
Use a checkbox when the choice is part of a form, list, or batch action — especially if several options can be selected, or the change only applies after Save / Apply / Submit. Examples: row selection, “Select all”, optional columns, consent, sub-filters in a filter panel.
Multiple options
When several choices need configuring together, wrap chips around native checkbox or radio inputs – use .chip-check; selected styles apply when the nested input is checked. Every option stays visible as a pill.
States — chips
| State | Fill | Border | Text |
|---|---|---|---|
| Rest | white | borders.form (neutral-700) | text.default; close icon text.secondary |
| Hover | backgrounds.secondary (neutral-200) | borders.formActive (neutral-900) | close icon darkens to text.default |
| Active | backgrounds.secondaryOnGray (neutral-300) | borders.formActive | |
| Selected | backgrounds.accent (indigo-100); hover accentHover (indigo-200) | borders.primaryActive (indigo-900) | text.primaryActive |
| Remove preview (prototype pattern) | white | text.error (red-500) | text.error – hovering a remove-only chip previews the destructive outcome |
| Focus | Focus ring (--focus-ring) on the chip; remove buttons get their own ring | ||
States — checkbox
| State | Fill | Border | Check |
|---|---|---|---|
| Rest | white | borders.form (neutral-700) | hidden |
| Hover | white | borders.primary (indigo-500) | hidden |
| Checked | controls.primaryActive (indigo-900) | borders.primaryActive | white check icon |
| Disabled | 50% opacity; cursor: not-allowed | ||
| Focus | Focus ring (--focus-ring) on the box | ||
States — boolean connectors
| Connector | Fill | Border | Text | Hover |
|---|---|---|---|---|
| OR · IS (neutral) | backgrounds.secondary | borders.default | text.secondary (4.6:1) | Whole fill darkens one neutral step; border borders.form |
| AND (amber) | backgrounds.warningSubtle (yellow-50) | borders.warningSubtle (yellow-150) | text.warningSubtle (yellow-750, 5.5:1) | Border only → warningSubtleHover (yellow-750) |
| IS NOT (red) | backgrounds.errorSubtle (red-50) | borders.errorSubtle (red-200) | text.error (red-500, 5.9:1) | Border only → borders.error |
| Locked | Identical to rest; cursor: not-allowed; not focusable (tabindex="-1"); no hover response | |||
| Focus | outline: 2px solid outlines.focus, offset 2px | |||
Alerts
Alert text uses text.warning (yellow-800) / text.error / text.success on the matching 100-tier fill. Error alerts carry role="alert"; others role="status".
Please fix the highlighted fields before submitting.
Account created successfully.
Your trial expires in 3 days.
Tooltip
Brief, plain-text hint that explains an element. CSS-only: wrap a focusable trigger
in .tooltip with a .tooltip__bubble child. It reveals on
hover and keyboard focus after a short delay, and only the
hovered/focused one ever shows. Set data-position to top
(default), right, bottom or left — choose the
side with room (this lightweight version doesn't auto-flip like the React component).
Set data-delay="instant" on .tooltip to skip the default
hover-intent delay (side nav uses instant tooltips by convention).
Markup
<span class="tooltip"> <button aria-describedby="tip-save">Save</button> <span class="tooltip__bubble" id="tip-save" role="tooltip" data-position="top">Saves your changes</span> </span>
Two markup patterns: a trigger with aria-describedby pointing at a
role="tooltip" bubble when the tip adds information; or, for
icon-only controls whose tip just repeats the aria-label (the side nav
and the gear above), mark the bubble aria-hidden="true" to avoid a
double announcement.
Accessibility (WCAG 2.2 AA)
- Form-control boundaries are visible at rest.
borders.form(neutral-700) gives 5.2:1 on white and 4.6:1 on the neutral-200 fill – comfortably above the 3:1 non-text minimum (SC 1.4.11). Hover state darkens toborders.formActive(neutral-900). - Placeholders:
forms.placeholder(neutral-700, 4.6:1 on neutral-200) andplaceholderOnGray(neutral-750, 5.9:1). Placeholders never substitute for a visible<label for>. - Focus ring (
0 0 0 2px white, 0 0 0 4px indigo-500) on every interactive element; connector chips use a 2px offset outline. Never remove:focus-visiblestyles. - Hover is supplementary – every interactive element has a resting affordance (border, fill, or underline). Hover/colour change is never the only signal.
- Text contrast:
text.default16.7:1,text.secondary5.2:1 on white; connector chips 4.6–5.9:1; white oncontrols.primary7.0:1. - Targets: 32px controls, 28px chips – all clear the 24×24 minimum (SC 2.5.8).
- Error inputs carry
aria-invalid="true"+aria-describedby; required fieldsaria-required="true"plus the visual*. - Icon-only buttons carry
aria-label. Locked connectors are unfocusable (tabindex="-1") withcursor: not-allowed. - Disabled controls are 0.5 opacity (exempt from contrast requirements) but keep their border so the field shape remains discernible.
TODO
Tracked here so the reference and the live system stay honest about what exists versus what's planned. Grouped by type; not in priority order.
Components to build
- Toasts / notifications — formalise the existing copy-confirmation toast into a documented component.
- Modals / dialogs — focus trap,
aria-modal, escape to close. - Tabs —
role="tablist", arrow-key navigation. - Pagination and breadcrumbs.
- Empty states and loading / skeleton states.
- Progress indicators.
Foundations & tokens
- Motion tokens — standard durations and easings (currently hardcoded
0.15s). - Elevation / shadow tokens — documented scale (shadows exist but are undocumented).
- Breakpoint scale + responsive guidance (only ad-hoc media queries today).
- i18n considerations.
Governance & docs
- Decision guidance — which button variant when; an anti-patterns gallery; microcopy / voice.
- Accessibility testing checklist (axe + manual screen-reader) and per-component keyboard patterns.