@phcdevworks/spectre-ui is the styling layer of the Spectre system. It
transforms Spectre tokens into reusable CSS, utilities, and class recipes for
consistent application interfaces.
Maintained by PHCDevworks. It sits between
@phcdevworks/spectre-tokens and the framework-specific adapter and component
packages, so no downstream repo needs to hand-roll CSS or hardcode design values
to consume Spectre's visual language.
| Field | Value |
|---|---|
| Project team | project-design |
| Repository role | Spectre L2 CSS and recipe contract |
| Package/artifact | @phcdevworks/spectre-ui |
| Current version/status | 5.8.0 |
- Read AGENTS.md, then the agent-specific guide for the task.
- Check TODO.md and ROADMAP.md for current scope.
- Make the smallest repo-local change that satisfies the task.
- Run
npm run checkwhen validation is required or practical. - Update docs and CHANGELOG.md only when behavior, public contracts, or release-relevant metadata changed.
| Guide | Path |
|---|---|
| Agent rules | AGENTS.md |
| Claude Code | CLAUDE.md |
| Codex | CODEX.md |
| Copilot | COPILOT.md |
| Jules | JULES.md |
| Grok | GROK.md |
| Roadmap | ROADMAP.md |
| Todo | TODO.md |
| Changelog | CHANGELOG.md |
| Security | SECURITY.md |
@phcdevworks/spectre-ui is Layer 2 of the Spectre design suite. It turns
@phcdevworks/spectre-tokens
into reusable CSS bundles and type-safe class recipes for downstream adapters
and apps.
For: adapter authors and app developers who need a stable, token-driven styling contract without re-implementing class logic themselves.
Not for: authoring design tokens (that belongs in
@phcdevworks/spectre-tokens) or building framework-specific components (that
belongs in adapter packages such as @phcdevworks/spectre-ui-astro).
Contributing | Code of Conduct | Changelog | Roadmap | Security Policy
@phcdevworks/spectre-tokens is the source of truth for visual values and
semantic meaning. ui-contract.manifest.json is the machine-readable contract
authority for this package's public styling surface.
| Layer | Path | Rule |
|---|---|---|
| Token authority | Published @phcdevworks/spectre-tokens package |
Design values and semantic meaning start there |
| UI contract authority | ui-contract.manifest.json |
Governs public recipes and CSS entry points |
| Source CSS | src/styles/ |
Token-backed CSS classes and bundle entry points |
| Source recipes | src/recipes/ |
Framework-agnostic class string APIs |
| Generated dist | dist/ |
Never edit directly — regenerated by npm run build |
After any contract-facing source change: run npm run check to validate the
full UI contract.
| Layer | Package or consumer | Responsibility | Relationship to this package |
|---|---|---|---|
| 1 | @phcdevworks/spectre-tokens |
Defines design values and semantic token meaning | Upstream source of truth |
| 2 | @phcdevworks/spectre-ui |
Translates tokens into CSS bundles and class recipes | This package |
| 3 | Adapters and apps, such as @phcdevworks/spectre-ui-astro |
Deliver Spectre through framework-native ergonomics | Downstream consumers |
@phcdevworks/spectre-components is a separate component package that can wrap
this styling contract in Lit web components. This package owns Layer 2 only: it
does not deliver components and it does not define tokens.
- Ships precompiled CSS:
index.css,base.css,components.css, andutilities.css - Exports type-safe class recipes for shared UI patterns
- Keeps CSS classes and recipe APIs aligned
- Gives adapters and apps a stable styling contract instead of re-implementing classes
- Enforces a zero-hex approach so visual values stay tied to
@phcdevworks/spectre-tokens
- Token-backed CSS class contracts in
src/styles/ - Precompiled CSS bundles for root, base, components, and utilities
- Framework-agnostic class recipe functions in
src/recipes/ - Contract validation that keeps CSS, recipes, exports, and docs aligned
This package is the correct place to define reusable styling structure on top of Spectre tokens.
- Design token values or semantic visual meaning. Those belong in
@phcdevworks/spectre-tokens. - Framework components, templates, hooks, or runtime behavior. Those belong in adapter packages.
- App-level layout, routing, data fetching, or product-specific composition.
- Local redefinition of token meaning. Downstream consumers should consume the token contract rather than recreate it.
Use @phcdevworks/spectre-ui when you need:
- precompiled, token-backed CSS ready to drop into any framework
- stable, type-safe class recipes for shared UI patterns (buttons, badges, cards, inputs, etc.) that you want to remain consistent across frameworks
- a styling contract that is enforced through tests and CI rather than conventions alone
Do not use @phcdevworks/spectre-ui when you need to:
- Define new design values — add them to
@phcdevworks/spectre-tokensinstead. - Deliver framework components — use an adapter package such as
@phcdevworks/spectre-ui-astrothat wraps this package in framework-native components.
npm install @phcdevworks/spectre-uiNo framework needed. Import the CSS and use the sp-* classes directly:
<!doctype html>
<html>
<head>
<link
rel="stylesheet"
href="node_modules/@phcdevworks/spectre-ui/dist/index.css"
/>
</head>
<body>
<button class="sp-btn sp-btn--primary sp-btn--md">Save</button>
<button class="sp-btn sp-btn--ghost sp-btn--md">Cancel</button>
<span class="sp-badge sp-badge--success sp-badge--sm">Published</span>
<div class="sp-card sp-card--elevated">
<p>Card content</p>
</div>
<div class="sp-input-wrapper">
<label class="sp-label">Email</label>
<input class="sp-input sp-input--md" type="email" />
</div>
</body>
</html>Import the full stylesheet:
import '@phcdevworks/spectre-ui/index.css'Or import the bundles separately:
import '@phcdevworks/spectre-ui/base.css'
import '@phcdevworks/spectre-ui/components.css'
import '@phcdevworks/spectre-ui/utilities.css'Class recipes are the stable styling API for adapters and apps. They return predictable class strings and keep behavior consistent across frameworks.
import {
getBadgeClasses,
getButtonClasses,
getPricingCardClasses
} from '@phcdevworks/spectre-ui'
const cta = getButtonClasses({ variant: 'primary', size: 'lg' })
const badge = getBadgeClasses({ variant: 'success', size: 'sm' })
const pricingCard = getPricingCardClasses({ featured: true })| What | Where it lives |
|---|---|
| Semantic color values, spacing scale, type scale | @phcdevworks/spectre-tokens |
| Token-to-CSS variable mapping | here — src/styles/ |
| Precompiled CSS bundles | here — built to dist/*.css |
| Class recipe functions (input → class string) | here — src/recipes/ |
| Astro, React, Vue, Lit, Svelte components | Adapter packages (e.g. spectre-ui-astro) |
| WordPress shortcodes or PHP templates | A WordPress adapter package |
| App-level layout, routing, or data fetching | Consuming apps |
| New design decisions (new colors, new spacing) | @phcdevworks/spectre-tokens |
Golden rule: this package consumes tokens and exposes class contracts. It does not define tokens and it does not deliver framework components.
All recipe functions accept a plain options object and return a class string.
Boolean options are caller-owned: omission adds no corresponding modifier class,
true adds it, and false omits it. Component packages define their own
property defaults and pass resolved boolean values explicitly. Non-boolean
recipe axes may retain documented visual fallbacks.
| Recipe | Function | Variants | Sizes | Common boolean flags |
|---|---|---|---|---|
| Button | getButtonClasses |
primary secondary ghost danger success cta action accent accentOutline inverse warning link light dark |
sm md lg |
disabled loading fullWidth pill iconOnly compact |
| Badge | getBadgeClasses |
primary secondary success warning danger neutral info ghost outline accent cta inverse brand |
sm md lg |
interactive dot disabled loading fullWidth, accentRail: top|right|bottom|left, accentRailColor: neutral|brand|info|success|warning|danger|cta|accent |
| Card | getCardClasses |
elevated flat outline ghost |
padded: sm md lg |
interactive padded (also accepts a size) fullHeight disabled loading, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Card bleed | getCardBleedClasses |
— | padded: sm md lg |
edges: single edge, an array, or 'all' |
| Input | getInputClasses |
— | sm md lg |
disabled loading fullWidth pill |
| Input state | getInputClasses |
state: default error success disabled loading |
— | — |
| IconBox | getIconBoxClasses |
primary secondary success warning danger info neutral ghost accent cta outline |
xs sm md lg |
interactive disabled loading pill fullWidth, iconSize: xs|sm|md|lg|xl|2xl|3xl |
| PricingCard | getPricingCardClasses |
— | — | featured interactive disabled loading fullHeight, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Rating | getRatingClasses |
— | sm md lg |
interactive disabled loading pill fullWidth |
| Testimonial | getTestimonialClasses |
elevated flat outline ghost |
— | interactive disabled loading fullHeight, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Quote | getQuoteClasses |
surface: plain subtle inverse, accent: edge (default) mark none, accentColor: neutral|brand|info|success|warning|danger|cta|accent (default)|action, align: start center |
sm md lg xl |
attribution: avatar |
| Stat | getStatClasses |
align: start center end, surface: page surface inverse, unitTone: accent (default) action |
sm md lg xl |
— |
| Alert | getAlertClasses |
info success warning danger neutral brand |
sm md lg |
dismissed dismissible |
| Avatar | getAvatarClasses |
— | xs sm md lg xl |
shape: circle square |
| Tag | getTagClasses |
default primary secondary success warning danger info neutral accent cta outline ghost |
sm md lg |
dismissible selected disabled loading interactive fullWidth |
| Spinner | getSpinnerClasses |
primary secondary success warning danger info neutral accent cta inverse |
sm md lg |
disabled loading |
| Skeleton | getSkeletonClasses |
shape: text rect circle |
— | animated |
| Logo cloud | getLogoCloudClasses |
size: sm md lg, fill: subtle card none |
— | muted; item: getLogoCloudItemClasses |
| Nav | getNavClasses |
— | — | bordered sticky fullWidth align: start|center|end, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Toast | getToastClasses |
info success warning danger neutral |
— | dismissed fullWidth, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Tooltip | getTooltipClasses |
placement: top bottom left right |
— | visible, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Dropdown | getDropdownClasses |
menu placement: bottom-start bottom-end top-start top-end |
— | fullWidth mega viewport, item: active selected disabled, menu accent: top|right|bottom|left, menu accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Modal | getModalClasses |
— | — | open fullWidth, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Container | getContainerClasses |
maxWidth: prose wide, padding: sm md lg xl 2xl 3xl 4xl |
— | — |
| Stack | getStackClasses |
direction: vertical horizontal, basis: sidebar, align: center stretch, gap: sm md lg xl 2xl 3xl 4xl |
— | — |
| Section | getSectionClasses |
spacing: sm md lg xl 2xl 3xl 4xl, gap: sm md lg xl 2xl 3xl 4xl, hero: sm md lg (replaces spacing) |
— | attached (drops top padding for a band that belongs to the section above) |
| Section head | getSectionHeadClasses |
layout: stacked split, align (stacked): start center |
— | — |
| Prose | getProseClasses |
— | md lg xl |
accentMarkers |
| Grid | getGridClasses |
columns: 1 2 3 4 6 12 auto |
gap/columnGap/rowGap: sm md lg xl 2xl 3xl 4xl |
span/offset/rowSpan/rowOffset/order: 1–12/0–11/first|last|none|1–12, per-breakpoint { base, md, lg } |
| Sidebar | getSidebarClasses |
— | — | bordered |
| Footer | getFooterClasses |
appearance: dark (default) light system, surface: page card subtle inverse hero action |
— | bordered fullWidth, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta|accent |
| Checkbox | getCheckboxClasses |
— | — | checked disabled |
| Radio | getRadioClasses |
— | — | checked disabled |
| Select | getSelectClasses |
size: sm md lg, state: default invalid success |
— | fullWidth pill disabled focused loading |
| Textarea | getTextareaClasses |
size: sm md lg, state: default invalid success |
— | fullWidth pill disabled focused loading |
| Fieldset | getFieldsetClasses |
— | — | disabled |
| Label | getLabelClasses |
— | — | disabled required |
| Text | getTextClasses |
color: default muted subtle meta brand accent onInverse onInverseMuted onInverseBrand onInverseAccent onSurface onSurfaceMuted onSurfaceSubtle onSurfaceMeta onSurfaceBrand onSurfaceAccent, family: sans serif mono |
xs–9xl |
— |
| Text state | getTextClasses |
transform: none uppercase lowercase capitalize, weight: 400–900 (emits sp-font-{weight}), leading: none tight snug normal relaxed loose, fontSize: 12–128 (emits sp-font-size-{px}), tracking: tightest–widest, decoration: thin base thick, underlineOffset: sm md lg |
— | tabularNums balance |
| Link | getLinkClasses |
tone: default onInverse, decoration: thin base thick, underlineOffset: sm md lg |
— | — |
| Surface | getSurfaceClasses |
surface: page card subtle overlay input inverse hero |
— | hover selected active |
| Divider | getDividerClasses |
— | — | — |
| Elevation | getElevationClasses |
level: flat raised overlay modal (surface, shadow, and z-index together) |
— | — |
| Chart color | getChartColorClasses |
fill stroke bg text border, each taking a chart role: series-1–8, sequential-1–7, diverging-1–7, axis grid label bg |
— | — |
| Motion | getMotionClasses |
animation: fadeIn fadeOut slideUp slideDown scaleIn bounce pulse shake, transition: all colors opacity transform none, duration: --sp-duration-* step, easing: --sp-easing-* step |
— | — |
| Spacing | getSpacingClasses |
p px py pt pr pb pl m mx my mt mr mb ml (margins also auto) gap gapX gapY basis, each a --sp-space-* step; sm md lg xl 2xl take the same axes from that breakpoint up |
— | — |
| Layout | getLayoutClasses |
display direction wrap flex grow shrink justify items content self position inset top right bottom left width minWidth maxWidth height minHeight maxHeight overflow overflowX overflowY whitespace textAlign objectFit objectPosition aspect border listStyle listPosition; sm–2xl per breakpoint |
— | — |
| Color | getColorClasses |
text bg border, each a raw semantic step (brand-500, error-700, …) or black white |
— | — |
| Palette | getPaletteClasses |
text bg border, each a palette hue step (blue-500, phcdevworks-paper-100, …) |
— | — |
| Border | getBorderClasses |
thick: all t r b l, style: --sp-border-style-*, width: --sp-border-width-*, radius: --sp-radius-* |
— | — |
| Shadow | getShadowClasses |
shadow: --sp-shadow-* or soft strong, inset: --sp-shadow-inset-* |
— | — |
| Opacity | getOpacityClasses |
role: --sp-opacity-* |
— | — |
| Z-index | getZIndexClasses |
layer: --sp-z-index-* |
— | — |
| Icon size | getIconSizeClasses |
size: --sp-icon-* |
— | — |
| Aspect ratio | getAspectRatioClasses |
ratio: --sp-aspect-ratio-* or auto |
— | — |
| Heading | getHeadingClasses |
level: h1–h6 (the full typography.heading preset) |
— | — |
| Display | getDisplayClasses |
level: 1–6 (the full typography.display preset) |
— | — |
| Lead | getLeadClasses |
— (the typography.lead preset) |
— | — |
| Tabs | getTabsClasses |
line pill |
— | vertical fullWidth, item: active disabled |
| Accordion | getAccordionClasses |
— | — | flush, item/header/icon/panel: expanded; native <details open> also expands |
| Breadcrumb | getBreadcrumbClasses |
— | — | customSeparator, item: current |
| List group | getListGroupClasses |
— | — | flush horizontal, item: interactive active selected disabled, accent/accentColor |
| Offcanvas | getOffcanvasClasses |
placement: start end top bottom |
— | open (panel and backdrop) |
| Carousel | getCarouselClasses |
control direction: prev next |
— | fade, slide/indicator: active |
| Table | getTableClasses |
row variant: neutral info success warning danger |
sm md |
striped hoverable bordered, row: selected |
| Pagination | getPaginationClasses |
— | sm md lg |
item: active disabled |
| Stepper | getStepperClasses |
orientation: horizontal vertical, step state: pending active done, step tone: brand accent |
— | — |
| Popover | getPopoverClasses |
placement: top bottom left right |
— | open |
| Progress | getProgressClasses |
bar variant: brand neutral info success warning danger |
sm md lg |
bar: indeterminate |
| Switch | getSwitchClasses |
— | sm md lg |
checked disabled focused (native :checked/:disabled also apply) |
| Range | getRangeClasses |
— | — | disabled focused |
| File input | getFileInputClasses |
state: default invalid success |
sm md lg |
fullWidth disabled focused |
| Input group | getInputGroupClasses |
— | — | disabled |
| Datepicker | getDatepickerClasses |
— | — | day (getDayClasses): selected today outsideMonth disabled |
| External auth button | getExternalAuthButtonClasses |
— | — | fullWidth disabled loading |
| Choice card | getChoiceCardClasses |
— | — | selected disabled (a wrapped native input also drives both) |
Each recipe family also exports sub-element helpers for its structural parts (labels, wrappers, sub-containers, text elements). See the full list below.
getCardClasses also accepts an optional accent edge ('top', 'right',
'bottom', or 'left') to render a thicker decorative rail, sized from
component.card.accent.thickness. accentColor selects the semantic color
scale (neutral, brand, info, success, warning, danger, cta,
accent) and
defaults to 'brand' when accent is set without it. Omitting accent renders
no rail — border width, color, and radius on every edge stay unchanged.
getCardClasses({ variant: 'outline', accent: 'left', accentColor: 'danger' })The same edge-rail pattern extends to nine more recipe families, each sourced
from its own component.<name>.accent token group (spectre-tokens 4.9.0):
getTestimonialClasses, getPricingCardClasses, getNavClasses,
getFooterClasses, getModalClasses, getToastClasses, getTooltipClasses,
getDropdownMenuClasses, all with the same accent edge / accentColor
options as getCardClasses above. getBadgeClasses uses
accentRail/accentRailColor instead, since variant: 'accent' already names
its single-tone brand-accent fill.
getToastClasses({ variant: 'success', accent: 'left', accentColor: 'brand' })
getBadgeClasses({
variant: 'outline',
accentRail: 'top',
accentRailColor: 'cta'
})getCardBleedClasses lets a child of a padded card (media, a flush internal
surface) run through the card's padding on one or more edges without the
consumer negating the active padding token or reconstructing card corner radius
locally. Pass the same padded value given to the parent getCardClasses call
so the bleed amount tracks the active padding step, and edges (a single edge,
an array, or 'all') for which sides run flush. Corner radius is only added
where two bled edges meet (e.g. edges: ['top', 'left'] rounds the top-left
corner to match the card), so a single-edge bleed stays square. Omitting
padded is correct for an unpadded card — the edge classes still apply corner
radius with zero bleed margin.
getCardClasses({ padded: 'lg' })
// child media flush to the top edge only
getCardBleedClasses({ padded: 'lg', edges: 'top' })
// child surface flush to every edge
getCardBleedClasses({ padded: 'lg', edges: 'all' })Grid also accepts two independent track-sizing options for layouts equal columns
cannot express: fixedTracks: { count: 1-4 } sizes every column from
--sp-space-240 (15rem) and replaces columns at every breakpoint;
leadingTracks: { weight: 1.5 | 1.6 | 2 | 2.5 | 3 } sizes one leading column at
weightfr against the rest of columns as equal 1fr tracks. A plain weight
applies at the lg breakpoint only (matching the original mega-menu/footer
evidence); pass { base, md, lg } for per-breakpoint control. Both emit
deterministic classes (sp-grid-fixed-tracks-*,
sp-{bp-}grid-leading-{weight}-of-{columns}) — no arbitrary widths, no inline
styles.
Grid also accepts rowSpan/rowOffset (same shape as span/offset: a single
value or a per-breakpoint { base, md, lg } object) for dashboard-style layouts
that need explicit row placement, via grid-row/grid-row-start — no explicit
row-track template required, since these apply against CSS Grid's auto-generated
implicit rows. Independent columnGap/rowGap options override the combined
gap on a single axis when a layout needs tighter rows than columns (or vice
versa).
columns: 'auto' (sp-grid-cols-auto) distributes any number of children
evenly across a single row with no explicit column count — matching Bootstrap's
bare .col / row-cols-auto — via
grid-template-columns: repeat(auto-fit, minmax(0, 1fr)).
order/order: { base, md, lg } (sp-order-*, responsive sp-{bp}-order-*)
reorders a grid item independent of source order, accepting first, last,
none, or 1–12.
Dropdown also accepts a mega flag (getDropdownClasses({ mega: true }) paired
with getDropdownMenuClasses({ mega: true })) for mega-menu panels: the trigger
wrapper (sp-dropdown--mega) cedes its positioning context to the nearest
positioned ancestor — typically sp-nav, which is a positioning context by
default — so the menu (sp-dropdown__menu--mega) spans that ancestor's full
width instead of tracking trigger width. Combine with a getGridClasses panel
inside the menu for a multi-column layout. Menu height is capped and scrollable
(max-height: 90vh; overflow-y: auto) so tall panels never overflow the
viewport.
A third viewport flag (getDropdownClasses({ viewport: true }) paired with
getDropdownMenuClasses({ viewport: true })) breaks the menu out to the full
browser viewport width instead of the trigger's or mega's positioned-ancestor
width — for a wide menu that would otherwise overflow past a narrow trigger or a
width-constrained nav. It uses the standard full-bleed breakout technique
(left: 50%; width: 100vw; margin-left: -50vw), which assumes the menu's
positioned ancestor is horizontally centered in the viewport (true for a
centered sp-container-based layout); it will not center correctly inside an
off-center ancestor (e.g. a fixed sidebar layout). viewport takes precedence
over mega if both are set — three widening tiers: trigger width (default),
mega (nav width), viewport (full browser width).
The component families added against the spectre-tokens 4.10.0 component.*
contracts follow Bootstrap's range of component types; their look stays
entirely token-driven. Each family reads its colors from its own mode-aware
token group, so dark mode needs no local overrides.
getTabsClasses({ variant: 'pill' }) // 'sp-tabs sp-tabs--pill'
getTabsItemClasses({ active: true }) // 'sp-tabs__item sp-tabs__item--active is-active'
getTableClasses({ striped: true, hoverable: true })
getTableRowClasses({ variant: 'warning' })
getOffcanvasClasses({ placement: 'end', open: true })
getStepperStepClasses({ state: 'done' })A few families need something from the caller that a class string cannot carry:
- Progress — set the bar's inline
widthto the current value.indeterminateanimates instead. - Range — Firefox paints the filled track natively. For WebKit/Blink, mirror
the value as a percentage in
--sp-component-range-valueon the input (e.g.style="--sp-component-range-value: 40%"). - Carousel — the viewport is a CSS scroll-snap track and works without
script.
fadestacks the slides and shows the one markedactive, which the caller toggles. - Popover — like the tooltip, place it inside a
position: relativetrigger wrapper.
Several families also follow native state, so the matching boolean flag is
optional: <details open> expands an accordion item, :checked/:disabled
drive the switch, a wrapped :checked input selects a choice card, and
aria-current/aria-selected mark the current tab, page, breadcrumb, or day.
getListGroupClasses also takes the accent/accentColor edge-rail options
described above, sourced from component.listGroup.accent.
The spectre-tokens 4.13.0 accent roles reach every layer that a child theme
used to restyle by hand. getTextClasses takes accent (and the
onSurfaceAccent, onInverseBrand, and onInverseAccent variants) for
eyebrows and units, plus a leading step from the named --sp-leading-*
scale. getButtonClasses({ variant: 'accentOutline' }) is the outline partner
of accent. To restyle both accent buttons, a child theme re-points the
published --sp-button-accent-* and --sp-button-accentoutline-* roles; do
not override .sp-btn internals. getStepperStepClasses({ tone }) colours the
active and done states from component.stepper.tone.{brand,accent}, and every
edge rail accepts accentColor: 'accent'.
getQuoteClasses({ size: 'xl', surface: 'inverse', accent: 'mark' })
getQuoteTextClasses() // the quoted text
getQuoteAttributionClasses({ avatar: true }) // avatar, then name and role
getQuoteAttributionNameClasses()
getQuoteAttributionRoleClasses() // role and organization
getStatClasses({ size: 'lg', align: 'center', surface: 'surface' })
getStatFigureClasses() // wrap the unit in getStatUnitClasses()
getStatLabelClasses()
getSectionHeadClasses({ layout: 'split' })
getSectionHeadTitleClasses() // eyebrow and heading
getSectionHeadLeadClasses() // lead and actionsThe quote reads component.quote: edge draws a leading rail at
component.quote.accent.thickness, mark draws a decorative opening quote
mark (hidden from screen readers), and none keeps the neutral
component.quote.border rule. The accent tone, the default, keeps the
component.quote.mark role for the mark; on the inverse surface the text,
rail, and mark switch to text.onInverse.*. A stat's unit takes the accent
role of its surface (text.onPage, text.onSurface, or text.onInverse).
The split section head puts the title block in columns 1–6 and the lead in
columns 7–12 of a twelve-column grid from the lg breakpoint, and stacks them
below it; the stacked head takes align: 'center'.
spectre-tokens 4.15.0 adds action.*, the theme's own action colour. A theme
picks the colour family with one attribute on the root element, and the hover
and active shades follow:
<html data-spectre-action="emerald">getButtonClasses({ variant: 'action' }) // fill, hover, active, focus ring
getStatClasses({ unitTone: 'action' }) // unit in action.onPage / onSurface
getQuoteClasses({ accentColor: 'action' }) // rail and mark in action.railgetFooterClasses({ surface: 'action' }) paints the footer in the action
colour with action.text for every text role, and links underline on hover.
An inverse stat keeps its onInverse accent unit, because action has no
inverse role. The action button has no disabled colour; the shared disabled
opacity applies.
getDisplayClasses({ level }) (1–6) applies the typography.display
presets, for hero and marketing headings one scale step larger than the matching
heading level. Headings and display text are mobile first (spectre-tokens
4.15.0): below 768px they use smaller sizes, and from md up the full ones. getLeadClasses() applies typography.lead to an introductory
paragraph.
tests/token-parity.test.ts fails if @phcdevworks/spectre-tokens publishes a
CSS variable that no spectre-ui stylesheet references. The only exemption is
--sp-breakpoint-sm/-xl/-2xl: CSS cannot use var() in @media queries,
so the generated responsive utilities consume them by value, and the test checks
those values instead. Two more groups are consumed through spectre-tokens' own
remap blocks rather than by name: --sp-layout-responsive-lg-* (re-points the
xl–4xl layout steps at the lg breakpoint) and --sp-control-compact-*
(re-points the --sp-control-* steps under [data-spectre-density="compact"]).
--sp-typography-responsive-md-* re-points the heading and display roles at
the md breakpoint, and the --sp-action-{50..900} ramp sits behind the
action.* roles.
Motion utilities (.sp-animate-*) switch to their published
animations.reducedMotion counterparts under prefers-reduced-motion.
These primitives are plain CSS classes in src/styles/utilities.css. Each one
is also reachable through a recipe (getLinkClasses, getSurfaceClasses,
getDividerClasses, and the getTextClasses decoration, underlineOffset,
tabularNums, and balance options), because framework adapters such as
spectre-ui-astro can only wrap recipes. tests/recipe-reachability.test.ts
guards this. In plain HTML, apply the class name directly.
| Class | Tokens | Usage |
|---|---|---|
.sp-link |
--sp-link-default --sp-link-hover --sp-link-active --sp-link-visited --sp-link-underline --sp-link-underline-hover |
Inline text links (<a>). |
.sp-link--on-inverse |
--sp-link-on-inverse --sp-link-on-inverse-hover |
Inline text links on a surface.inverse-backed background. |
.sp-surface--hover |
--sp-surface-hover |
Clickable list items, menu items, table rows on hover. |
.sp-surface--selected |
--sp-surface-selected |
Selected list items, menu items, table rows. |
.sp-surface--active |
--sp-surface-active |
Pressed/active state for clickable surfaces. |
.sp-surface--hero |
--sp-surface-hero |
Gradient background for a hero/jumbotron band; pair with the on-inverse text roles. |
.sp-surface--input |
--sp-surface-input |
An input-like well that is not itself a form control. |
.sp-surface--inverse |
--sp-surface-inverse |
Background for an on-dark/inverse content island; pairs with the .sp-text--on-inverse*/.sp-link--on-inverse/inverse Badge/Button variants. |
.sp-divider |
--sp-surface-divider |
<hr>, section separators, table borders. |
.sp-text-balance |
— | Balanced line wrapping (text-wrap: balance) so a short headline does not end on an orphan word. |
.sp-tabular-nums |
— | Tabular numerals for figures that repaint in place (countdowns, live prices). |
.sp-decoration-{thin|base|thick} |
--sp-text-decoration-thickness-* |
Underline thickness for links and annotated text. |
.sp-underline-offset-{sm|md|lg} |
--sp-text-decoration-offset-* |
Gap between text and its underline. |
.sp-{md|lg}-grid-cols-{n} |
--sp-breakpoint-md/-lg (by value) |
n equal grid columns (1 2 3 4 6 12) from that breakpoint up, overriding the sp-grid-cols-* step-down preset. |
src/styles/utilities.generated.css is a build-time generated file
(npm run build:utilities, wired into npm run build) that expands finite
token scales and the fixed layout contract into flat utility classes. It is not
hand-edited; regenerate it after a @phcdevworks/spectre-tokens bump and commit
the result (npm run validate:utilities fails CI if it drifts). No arbitrary
visual values are supported — a design need that doesn't fit an existing token
step needs a token proposal to spectre-tokens, not a raw value in markup.
The same build writes src/internal/utility-scales.generated.ts, the option
maps the utility recipes validate against (getSpacingClasses,
getLayoutClasses, getColorClasses, getPaletteClasses,
getBorderClasses, getShadowClasses, getOpacityClasses,
getZIndexClasses, getIconSizeClasses, getAspectRatioClasses, and the
token axes of the other recipes). Every class in this table is reachable
through a recipe; tests/utility-recipe-parity.test.ts fails if a generated
or utilities.css class has no recipe path.
| Family | Class pattern | Token/value source |
|---|---|---|
| Layout | .sp-{block|flex|grid|hidden|relative|...} |
CSS layout keywords and --sp-space-0 |
| Flexbox | .sp-{flex-row|flex-wrap|justify-between|items-center|self-start|content-between|order-first|order-{1-12}|...} |
CSS layout keywords |
| Sizing | .sp-{w|min-w|max-w|h|min-h|max-h}-{auto|0|full|fit|none} |
CSS intrinsic and percentage sizing |
| Overflow | .sp-overflow-{auto|hidden|clip|visible|scroll} with -x- and -y- variants |
CSS overflow keywords |
| Spacing | .sp-{p|px|py|pt|pr|pb|pl|m|mx|my|mt|mr|mb|ml|gap|gap-x|gap-y|basis}-{step} |
--sp-space-*; auto-margin variants are added |
| Palette | .sp-{text|bg|border}-{hue}-{step}, including multi-segment hues such as integration-gunmetal |
--sp-color-palette-* |
| Color scale | .sp-{text|bg|border}-color-{scale}-{step}, plus .sp-{text|bg|border}-{black|white} |
--sp-color-{brand,accent,neutral,success,warning,error,info,indigo,violet}-* — an opt-in raw scale; prefer semantic roles |
| Duration | .sp-duration-{step} |
--sp-duration-* (transition-duration) |
| Easing | .sp-ease-{step} |
--sp-easing-* (transition-timing-function) |
| Border style | .sp-border-style-{solid|dashed|dotted|none} |
--sp-border-style-* |
| Border width | .sp-border-width-{none|base|thick} |
--sp-border-width-* |
| Icon size | .sp-icon-{xs|sm|md|lg|xl|2xl|3xl} |
--sp-icon-* (width and height) |
| Radius | .sp-rounded-{step} |
--sp-radius-* |
| Shadow | .sp-shadow-{step} |
--sp-shadow-* |
| Shadow (inset) | .sp-shadow-inset-{step} |
--sp-shadow-inset-* |
| Opacity | .sp-opacity-{role} |
--sp-opacity-* |
| Z-index | .sp-z-{role} |
--sp-z-index-* |
| Surface bg | .sp-bg-surface-{role} |
--sp-surface-* (page, card, input, subtle, hover, selected, active, divider, inverse, overlay, hero) |
| Thick border | .sp-border-thick and -t/-r/-b/-l; add any .sp-border-{color} utility for a role color |
--sp-border-width-thick, divider color by default (no responsive variants) |
| Chart color | .sp-{text|bg|border|fill|stroke}-chart-{role} |
--sp-chart-* (bg, grid, axis, label, series-1–8, sequential-1–7, diverging-1–7) |
| Elevation | .sp-elevation-{flat|raised|overlay|modal} |
--sp-elevation-* (shadow, surface, and z-index together) |
| Font weight | .sp-font-{weight} |
distinct values already published across --sp-font-{step}-weight / --sp-heading-h{n}-weight |
| Font size | .sp-font-size-{12|14|16|18|20|24|30|36|48|60|72|96|128} |
--sp-font-size-* (font-size only; no line height or weight) |
Responsive variants use the sp-{breakpoint}-{utility} prefix form (for
example, sp-md-p-4, sp-lg-gap-8, sp-md-flex, and sp-lg-justify-between)
at every published breakpoint (sm, md, lg, xl, 2xl), matching the
step-down convention Grid already established. Grid's hand-authored column-count
utilities keep their own md/lg-only convention. This prefix syntax is a
locked decision (see TODO.md Phase 7 P0); it will not change without a
major-version breaking release.
All exported stylesheets declare the cascade order tokens, base,
components, then utilities. Utilities therefore override component defaults
regardless of whether consumers load the standalone component and utility
bundles in the opposite order, and every Spectre layer — including the package's
own :root[data-spectre-theme="dark"] token defaults — overrides only within
that order. Unlayered application CSS still overrides all Spectre layers, and
any layered override a consumer declares (including a scoped
:root[data-spectre-theme="dark"] block in the app's own CSS) wins over the
package's tokens layer without needing !important.
Color modes can be scoped. Put data-spectre-theme="dark", "light",
"high-contrast", or "system" on any element (a header, footer, or hero band),
and every Spectre component inside it switches mode, because component variables
are re-declared on each themed element. system follows the visitor's
prefers-color-scheme. Base styles also take the text selection, caret, and
scrollbar colors from the mode tokens. Buttons, inputs, and selects are sized by
--sp-control-*. Put data-spectre-density="compact" on any element to use the
compact steps inside it. Use .sp-prose kbd to style keycaps.
Base and prose styles cover the spectre-tokens 4.13.0 element map
(spectre-tokens/element-coverage.json). The base layer sets <b>/<strong>
to --sp-font-weight-bold, underlines <abbr title> and <u> with the
textDecoration steps, draws ::spelling-error and ::grammar-error in the
component.proofing colours, styles <meter> from component.meter, and
colours Prism (.token.*) and highlight.js (.hljs-*) code inside <pre>
from component.code.syntax at zero specificity, so a highlighter theme still
wins. .sp-prose spaces its blocks with layout.prose.flow (paragraphs,
lists, list items, blockquotes, figures, rules, and the space before and after
headings, which get no type styling) and styles <del>, <ins>, <s>, list
markers (getProseClasses({ accentMarkers: true }) for the accent colour),
the body text size (getProseClasses({ size: 'lg' }); md, lg, or xl),
<dl>/<dt>/<dd>, <figcaption>, and <caption>. .sp-table styles
<caption> and <tfoot>, and .sp-select styles options and option groups
where the engine allows it (appearance: base-select). The sp-font-{weight}
utilities resolve through --sp-font-weight-*.
The root package exports CSS path constants plus the recipe functions
re-exported from src/recipes/index.ts.
Root constants:
spectreStylesspectreBaseStylesPathspectreComponentsStylesPathspectreIndexStylesPathspectreUtilitiesStylesPathspectreThemeModes— everydata-spectre-themevalue spectre-tokens publishes (lightdarkhigh-contrastsystem), typed asSpectreThemeModespectreDensityModes— everydata-spectre-densityvalue (compact), typed asSpectreDensityMode
Both mode lists are generated from the installed tokens, so an adapter can type a theme or density prop without hardcoding the strings.
Root recipe functions:
getAccordionClassesgetAlertClassesgetAvatarClassesgetBadgeClassesgetBreadcrumbClassesgetButtonClassesgetCardBleedClassesgetCardClassesgetCarouselClassesgetCheckboxClassesgetChoiceCardClassesgetContainerClassesgetDatepickerClassesgetDayClassesgetDisplayClassesgetDropdownClassesgetExternalAuthButtonClassesgetFieldsetClassesgetFileInputClassesgetFooterClassesgetGridClassesgetHeadingClassesgetIconBoxClassesgetInputClassesgetInputGroupClassesgetLabelClassesgetLeadClassesgetListGroupClassesgetModalClassesgetNavClassesgetOffcanvasClassesgetPaginationClassesgetPopoverClassesgetPricingCardClassesgetProgressClassesgetProseClassesgetQuoteClassesgetRadioClassesgetRangeClassesgetRatingClassesgetSectionClassesgetSectionHeadClassesgetSelectClassesgetSidebarClassesgetSkeletonClassesgetLinkClassesgetSurfaceClassesgetDividerClassesgetElevationClassesgetChartColorClassesgetMotionClassesgetSpacingClassesgetLayoutClassesgetColorClassesgetPaletteClassesgetBorderClassesgetShadowClassesgetOpacityClassesgetZIndexClassesgetIconSizeClassesgetAspectRatioClassesgetSpinnerClassesgetStackClassesgetStatClassesgetStepperClassesgetSwitchClassesgetTableClassesgetTabsClassesgetTagClassesgetTestimonialClassesgetTextareaClassesgetTextClassesgetToastClassesgetTooltipClasses
Root recipe helper functions:
getAccordionHeaderClassesgetAccordionIconClassesgetAccordionItemClassesgetAccordionPanelClassesgetAlertDismissClassesgetAlertIconClassesgetBreadcrumbItemClassesgetBreadcrumbLinkClassesgetBreadcrumbSeparatorClassesgetCarouselCaptionClassesgetCarouselControlClassesgetCarouselIndicatorClassesgetCarouselIndicatorsClassesgetCarouselSlideClassesgetCarouselViewportClassesgetDatepickerGridClassesgetDatepickerHeaderClassesgetDatepickerWeekdayClassesgetDropdownDividerClassesgetDropdownHeaderClassesgetDropdownItemClassesgetDropdownMenuClassesgetExternalAuthButtonIconClassesgetFieldsetLegendClassesgetFooterChipClassesgetFooterDividerClassesgetFooterHeadingClassesgetFooterLinkClassesgetFooterLinksClassesgetFooterMutedClassesgetFooterTextClassesgetInputErrorMessageClassesgetInputGroupAddonClassesgetInputHelperTextClassesgetInputLabelClassesgetInputWrapperClassesgetListGroupItemClassesgetListGroupItemHeadingClassesgetListGroupItemTextClassesgetLogoCloudClassesgetLogoCloudItemClassesgetModalOverlayClassesgetNavLinkClassesgetNavLinksClassesgetOffcanvasBackdropClassesgetOffcanvasBodyClassesgetOffcanvasFooterClassesgetOffcanvasHeaderClassesgetPaginationEllipsisClassesgetPaginationItemClassesgetPopoverArrowClassesgetPopoverBodyClassesgetPopoverHeaderClassesgetPricingCardBadgeClassesgetPricingCardDescriptionClassesgetPricingCardPriceClassesgetPricingCardPriceContainerClassesgetProgressBarClassesgetProgressLabelClassesgetQuoteAttributionClassesgetQuoteAttributionNameClassesgetQuoteAttributionRoleClassesgetQuoteTextClassesgetRatingStarClassesgetRatingStarsClassesgetRatingTextClassesgetSidebarBackdropClassesgetSidebarGroupClassesgetSidebarGroupContentClassesgetSidebarGroupIconClassesgetSidebarGroupSummaryClassesgetSidebarHeaderClassesgetSidebarLinkClassesgetSidebarToggleClassesgetSectionHeadLeadClassesgetSectionHeadTitleClassesgetStatFigureClassesgetStatLabelClassesgetStatUnitClassesgetStepperIndicatorClassesgetStepperLabelClassesgetStepperStepClassesgetTableRowClassesgetTableWrapperClassesgetTabsItemClassesgetTabsListClassesgetTabsPanelClassesgetTestimonialAuthorClassesgetTestimonialAuthorInfoClassesgetTestimonialAuthorNameClassesgetTestimonialAuthorTitleClassesgetTestimonialQuoteClassesgetToastIconClasses
The root package also re-exports the related recipe option, variant, size, and state TypeScript types defined by those recipes.
@phcdevworks/spectre-ui/index.css@phcdevworks/spectre-ui/base.css@phcdevworks/spectre-ui/components.css@phcdevworks/spectre-ui/utilities.css
The ./icons subpath ships the Lucide icon set as plain
SVG data. It comes with the package (lucide is a pinned dependency), so there
is nothing extra to install, and it needs no React, Astro, Lit, or DOM runtime.
The root @phcdevworks/spectre-ui entry does not include it.
import {
House,
Search,
Settings,
iconAttributes,
type IconNode
} from '@phcdevworks/spectre-ui/icons'Every Lucide icon is a named export in PascalCase (house is House,
circle-alert is CircleAlert). Each one is an IconNode: the children of an
<svg>, as [tag, attributes] pairs.
House
// [
// ['path', { d: 'M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8' }],
// ['path', { d: 'M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' }]
// ]Draw them inside an <svg> with iconAttributes, Lucide's root attributes
(viewBox="0 0 24 24", fill="none", stroke="currentColor",
stroke-width="2", round caps and joins). Size the icon with a .sp-icon-*
class (getIconSizeClasses) or inside getIconBoxClasses; CSS width and
height override the 24px attributes, and currentColor takes the text colour.
// Vanilla DOM
const ns = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(ns, 'svg')
for (const [key, value] of Object.entries(iconAttributes)) {
svg.setAttribute(key, String(value))
}
svg.setAttribute('class', 'sp-icon-md')
for (const [tag, attrs] of House) {
const child = document.createElementNS(ns, tag)
for (const [key, value] of Object.entries(attrs)) {
child.setAttribute(key, String(value))
}
svg.append(child)
}---
// Astro (spectre-ui-astro)
import { House, iconAttributes } from '@phcdevworks/spectre-ui/icons'
---
<svg {...iconAttributes} class="sp-icon-md" aria-hidden="true">
{House.map(([Tag, attrs]) => <Tag {...attrs} />)}
</svg>Lit (spectre-components) can serialise the nodes into an svg template the
same way, for example with unsafeSVG. This package renders nothing itself:
framework components belong in the adapters.
Import only the icons you use. The subpath is free of side effects, so a
production bundle keeps just those icons (one icon is well under 2 KB);
import * as icons pulls in the whole set.
Also exported:
iconAttributes: Lucide's root<svg>attributes.iconAliases: older and alternate names, each mapped to its current name. Each alias is the same data as its target.lucideVersion: the pinned Lucide release.iconSet: the package, version, licence, and icon count.- Types
IconNodeandIconAttributes.
Names stay stable. lucide is pinned to an exact version. When Lucide
renames an icon it keeps the old name as an alias, so the name keeps working.
scripts/icon-snapshots/lucide.json records every exported name, and
npm run validate:icons fails if an update removes one. Removing a name only
happens deliberately, in a major release.
License. Lucide is ISC-licensed (copyright Lucide Icons and Contributors),
and parts are MIT-licensed from Feather (copyright Cole Bemis); its license
installs with the lucide package. Keep that notice when you redistribute
the icons.
Five more sets ship the same way: each subpath re-exports a pinned npm
package's own icon values by name, so a bundle keeps only the icons you
import, and no framework runtime comes with them. Each one also exports
iconSet (package, version, licence, count).
| Subpath | Package | Icons | Each value |
|---|---|---|---|
@phcdevworks/spectre-ui/icons |
lucide (ISC) |
2,167 | IconNode (see above) |
@phcdevworks/spectre-ui/icons/mdi |
@mdi/js, Material Design Icons (Apache-2.0) |
7,447 | SVG path string for a 0 0 24 24 viewBox |
@phcdevworks/spectre-ui/icons/brands |
simple-icons, brand logos (CC0) |
3,362 | { title, slug, path, hex, source, guidelines }, 0 0 24 24 |
@phcdevworks/spectre-ui/icons/fa-solid |
Font Awesome Free solid (CC-BY-4.0 icons) | 2,001 | { prefix, iconName, icon: [width, height, aliases, unicode, path] } |
@phcdevworks/spectre-ui/icons/fa-regular |
Font Awesome Free regular | 273 | same |
@phcdevworks/spectre-ui/icons/fa-brands |
Font Awesome Free brands | 609 | same |
import { mdiHome } from '@phcdevworks/spectre-ui/icons/mdi'
import { siGithub } from '@phcdevworks/spectre-ui/icons/brands'
import { faHouse } from '@phcdevworks/spectre-ui/icons/fa-solid'
// <svg viewBox="0 0 24 24" fill="currentColor"><path d={mdiHome} /></svg>
// <svg viewBox="0 0 24 24" fill="currentColor"><path d={siGithub.path} /></svg>
const [width, height, , , path] = faHouse.icon
// <svg viewBox={`0 0 ${width} ${height}`} fill="currentColor"><path d={path} /></svg>Names follow each package: mdi*, si*, fa*. Size with .sp-icon-* and
colour with currentColor, as with Lucide. Do not style with siX.hex: it is
the brand's own colour, not a Spectre token. Each set has its own name snapshot
in scripts/icon-snapshots/, checked by npm run validate:icons.
Licences: Material Design Icons is Apache-2.0. Font Awesome Free icons are
CC-BY-4.0 and need attribution to Font Awesome (fontawesome.com) where you use
them. Simple Icons is CC0, but brand logos are trademarks; follow each brand's
guidelines (siX.guidelines). Logos that Simple Icons lists under a
copyleft, share-alike, non-commercial, no-derivatives, or custom licence are
left out (104 in 16.35.0); CC-BY logos are kept and need attribution to their
owner. Each licence installs with its package.
ui-contract.manifest.json defines the public styling contract for this
package.
It covers:
- CSS entry points
- the
./iconssubpath and its icon-name snapshot - root package constants and recipe function exports
- recipe families, variants, sizes, and public states
Every contract-facing surface must match that manifest. Validation fails when README documentation omits manifest-declared exports, when export snapshots drift, or when CSS contract coverage no longer matches the declared surface.
getSidebarClasses is the first recipe family with an interactive-state CSS
contract. Below breakpoints.md (768px), .sp-sidebar renders off-canvas
(transform: translateX(-100%)). This package owns only the CSS reaction to
that state — it does not own toggle behavior, click handlers, or open/closed
state management.
Consumers (typically a framework adapter) toggle the sidebar by setting a
data-sidebar-open="true" attribute on an ancestor element wrapping
.sp-sidebar and .sp-sidebar-backdrop (from getSidebarBackdropClasses):
[data-sidebar-open="true"] .sp-sidebarslides the sidebar into view (transform: translateX(0)).[data-sidebar-open="true"] .sp-sidebar-backdropshows the backdrop overlay (display: block).- Above
breakpoints.md, the sidebar docks inline and the backdrop is always hidden, regardless of thedata-sidebar-openvalue.
A consumer-rendered toggle button that opens/closes the sidebar must carry
getSidebarToggleClasses(). .sp-sidebar-toggle stacks above
.sp-sidebar-backdrop (--sp-component-sidebar-toggle-z-index, above
--sp-component-sidebar-backdrop-z-index) so the backdrop never intercepts
clicks meant for the toggle once the sidebar is open. The class also supplies
the token-backed button layout, color, hover, and focus-visible treatment;
adapters only supply the control markup and behavior.
Adapters own the hamburger/toggle control, click handling, and SSR-safe initial closed state.
Above breakpoints.md, .sp-sidebar stretches to height: 100% so a short
link list matches the height of a taller sibling content column when docked
inline in a Stack row (see align: 'stretch' on getStackClasses).
getSidebarLinkClasses accepts a level option ('parent' | 'child', default
'parent') for nested link indentation — e.g. a package name with "Overview" /
"Reference" links beneath it. getSidebarHeaderClasses styles a section label
(e.g. "Tokens", "UI", "Guides") as a muted eyebrow, visually distinct from
.sp-sidebar__link.
For collapsible navigation sections, apply getSidebarGroupClasses() to a
native details element and getSidebarGroupSummaryClasses() to its summary.
The CSS removes the browser marker, styles the summary as an interactive section
label, and rotates a consumer-provided icon (getSidebarGroupIconClasses())
when the group is open. Wrap the nested links in
getSidebarGroupContentClasses() for the standard bottom spacing. The summary
and getSidebarToggleClasses accept hovered and focused to force those
states. Open/closed behavior remains native to details; this package
returns class strings and does not render markup.
Downstream packages should never redefine locally:
- Spectre design token meaning
- CSS class semantics already provided by this package
- recipe option names, variants, sizes, or states
- package CSS entry point behavior
Downstream packages may:
- compose application UI with the exported classes
- wrap recipe functions in framework-specific adapters
- import CSS entry points directly in applications or adapter packages
- extend app-specific layout around Spectre contracts
Consumers should treat this package as a SemVer-governed styling contract.
Practical guidance:
- additive recipes, variants, states, and helpers are intended to be safe for existing consumers
- semantic shifts may keep the same class or option name but still affect visual output
- renames, removals, and behavior changes to existing classes or options are breaking
- generated JS, TypeScript declarations, CSS bundles, README docs, and
ui-contract.manifest.jsonare expected to stay aligned
If a downstream package depends on a class, recipe option, or CSS entry point:
- read
CHANGELOG.mdfor contract change classification - prefer documented public exports over internal paths
- run consuming app validation after package upgrades
Contract-affecting changes should be classified in CHANGELOG.md [Unreleased]
before release.
| Classification | When to use | Examples |
|---|---|---|
additive |
New public styling surface that does not break existing consumers | Adding a recipe helper, variant, state, or CSS entry point |
semantic change |
Public name remains but behavior or visual meaning shifts | Adjusting an existing class or recipe option to map to different token intent |
breaking |
Existing consumers may need code changes | Renaming or removing a class, option, export, or CSS entry point |
Renames and removals are always breaking regardless of perceived scope.
Spectre keeps responsibilities separate:
@phcdevworks/spectre-tokensdefines design values and semantic meaning@phcdevworks/spectre-uiturns those tokens into reusable CSS and type-safe class recipes@phcdevworks/spectre-componentsturns those styling contracts into framework-agnostic Lit web components- Adapters and apps consume
@phcdevworks/spectre-uiinstead of re-implementing its styling layer, or wrap Spectre component contracts for a specific runtime
That separation keeps recipe behavior consistent across frameworks and reduces implementation drift.
For downstream packages and compatible apps:
- import
@phcdevworks/spectre-ui/index.cssfor the full styling contract - import split CSS entry points only when the consumer needs bundle-level control
- use recipe functions when framework adapters need stable class strings
- consume tokens from
@phcdevworks/spectre-tokensinstead of inventing visual values locally - treat
dist/as generated package output, not an authoring surface - do not add framework runtime logic to this package
git clone https://github.com/phcdevworks/spectre-ui.git
cd spectre-ui
nvm use # picks up .nvmrc (Node 22.22.2)
npm install
npm run ci:verifyDeveloping this project requires Node.js ^22.18.0 || ^24.11.0 || >=26.0.0
(the build tooling's floor, tsup 9) and npm >=10.0.0. The published package
itself still supports ^22.13.0 || >=24.0.0 (engines). The checked-in
package manager is npm@11.17.0.
| Command | What it does |
|---|---|
npm run check |
Full validation gate — run before every PR |
npm run ci:verify |
Underlying verification sequence used by npm run check |
npm test |
Build then run the contract and regression test suite |
npm run build |
Emit TypeScript and CSS artifacts to dist/ |
npm run lint |
ESLint with TypeScript-aware config |
npm run validate:exports |
Verify root export surface against snapshot |
npm run validate:exports:update |
Update the export snapshot after adding a public export |
npm run validate:tokens |
Check for token drift against latest published release |
validate:runtime fails — you are on the wrong Node version. Run nvm use
to switch to the version in .nvmrc, or install Node 22 or 24.
validate:tokens fails with a network error — the check requires outbound
npm registry access. In a restricted environment, run the other validators
individually; this step is the only network-dependent one in ci:verify.
Tests pass but the build shows stale output — npm test rebuilds
automatically via the pretest hook. If you ran vitest directly, run
npm run build first.
Lint fails locally but passes in CI — confirm you are on the same Node version as CI (Node 22.x or 24.x). ESLint plugin resolution can differ across runtimes.
Export snapshot out of date — run npm run validate:exports:update after
adding a public export, then commit the updated scripts/export-snapshot.json.
src/styles/for source CSSsrc/recipes/for class recipestests/for contract and regression coverageexamples/for visual demos and verification fixtures
Planning artifacts for contract hardening live in ROADMAP.md and TODO.md.
All scoped roadmap phases through Phase 5 are delivered — there is no open
implementation phase right now. New recipe and CSS work is planned proactively,
and every token group
@phcdevworks/spectre-tokens
publishes becomes work for this package as soon as it ships. This package
synchronizes only against published npm releases, not in-progress upstream work.
Use examples/examples.html as the visual index for
the package demos.
Available examples include:
vanilla.htmlfor the broad component showcaseshowroom.htmlfor a richer marketing-style compositionverification.htmland focused verification fixtures for regression checks
Run the full validation gate before any pull request:
npm run checkThis runs: runtime check → lint → changelog validation → export validation → README validation → token drift check → build → CSS contract → tests. All steps must pass.
Claude Code (claude-sonnet-4-6) is the primary development agent for this
repository. Codex handles releases, including cutting tagged releases and GitHub
Releases, and production stabilization. Jules handles small automated fixes and
token sync passes. GitHub Copilot provides development support.
All AI agents with repository access (Claude Code, Codex, Copilot, Jules) have commit, push, and tag authority in this repository. Publishing to npm remains Bradley Potts's sole authority. See AGENTS.md for the full commit-policy and release-authority grant.
Protected from automated change: CSS contracts, recipe public API surface, and the zero-hex policy (no hardcoded color/spacing values). See AGENTS.md for full agent governance and boundary rules.
PHCDevworks maintains this package as part of the Spectre suite.
When contributing:
- keep styling token-driven
- keep recipe APIs and CSS classes in sync
- avoid local visual values unless clearly intentional
- run
npm run checkbefore opening a pull request
See CONTRIBUTING.md for the full workflow.
MIT © PHCDevworks. See LICENSE.