Skip to content

About

@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.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

 

History

676 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@phcdevworks/spectre-ui

@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.

Repository Snapshot

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

Standard Workflow

  1. Read AGENTS.md, then the agent-specific guide for the task.
  2. Check TODO.md and ROADMAP.md for current scope.
  3. Make the smallest repo-local change that satisfies the task.
  4. Run npm run check when validation is required or practical.
  5. Update docs and CHANGELOG.md only when behavior, public contracts, or release-relevant metadata changed.

Documentation Map

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

npm version CI License Node

@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

Source Of Truth

@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.

Architecture

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.

Key Capabilities

  • Ships precompiled CSS: index.css, base.css, components.css, and utilities.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

What This Package Owns

  • 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.

What This Package Does Not Own

  • 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.

When To Use This Package

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

When Not To Use This Package

Do not use @phcdevworks/spectre-ui when you need to:

  • Define new design values — add them to @phcdevworks/spectre-tokens instead.
  • Deliver framework components — use an adapter package such as @phcdevworks/spectre-ui-astro that wraps this package in framework-native components.

Installation

npm install @phcdevworks/spectre-ui

Quick Start

Vanilla HTML — CSS classes only

No 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>

CSS import (bundler or framework)

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 recipe usage

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 Belongs Here Vs Elsewhere

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.

Package Exports / API Surface

Recipe quick reference

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).

Bootstrap-scale component inventory

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 width to the current value. indeterminate animates instead.
  • Range — Firefox paints the filled track natively. For WebKit/Blink, mirror the value as a percentage in --sp-component-range-value on the input (e.g. style="--sp-component-range-value: 40%").
  • Carousel — the viewport is a CSS scroll-snap track and works without script. fade stacks the slides and shows the one marked active, which the caller toggles.
  • Popover — like the tooltip, place it inside a position: relative trigger 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.

Accent, quote, stat, and section head

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 actions

The 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'.

Action colour

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.rail

getFooterClasses({ 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.

Token parity

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.

Semantic utility classes

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.

Generated utility classes

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-*.

Root package

The root package exports CSS path constants plus the recipe functions re-exported from src/recipes/index.ts.

Root constants:

  • spectreStyles
  • spectreBaseStylesPath
  • spectreComponentsStylesPath
  • spectreIndexStylesPath
  • spectreUtilitiesStylesPath
  • spectreThemeModes — every data-spectre-theme value spectre-tokens publishes (light dark high-contrast system), typed as SpectreThemeMode
  • spectreDensityModes — every data-spectre-density value (compact), typed as SpectreDensityMode

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:

  • getAccordionClasses
  • getAlertClasses
  • getAvatarClasses
  • getBadgeClasses
  • getBreadcrumbClasses
  • getButtonClasses
  • getCardBleedClasses
  • getCardClasses
  • getCarouselClasses
  • getCheckboxClasses
  • getChoiceCardClasses
  • getContainerClasses
  • getDatepickerClasses
  • getDayClasses
  • getDisplayClasses
  • getDropdownClasses
  • getExternalAuthButtonClasses
  • getFieldsetClasses
  • getFileInputClasses
  • getFooterClasses
  • getGridClasses
  • getHeadingClasses
  • getIconBoxClasses
  • getInputClasses
  • getInputGroupClasses
  • getLabelClasses
  • getLeadClasses
  • getListGroupClasses
  • getModalClasses
  • getNavClasses
  • getOffcanvasClasses
  • getPaginationClasses
  • getPopoverClasses
  • getPricingCardClasses
  • getProgressClasses
  • getProseClasses
  • getQuoteClasses
  • getRadioClasses
  • getRangeClasses
  • getRatingClasses
  • getSectionClasses
  • getSectionHeadClasses
  • getSelectClasses
  • getSidebarClasses
  • getSkeletonClasses
  • getLinkClasses
  • getSurfaceClasses
  • getDividerClasses
  • getElevationClasses
  • getChartColorClasses
  • getMotionClasses
  • getSpacingClasses
  • getLayoutClasses
  • getColorClasses
  • getPaletteClasses
  • getBorderClasses
  • getShadowClasses
  • getOpacityClasses
  • getZIndexClasses
  • getIconSizeClasses
  • getAspectRatioClasses
  • getSpinnerClasses
  • getStackClasses
  • getStatClasses
  • getStepperClasses
  • getSwitchClasses
  • getTableClasses
  • getTabsClasses
  • getTagClasses
  • getTestimonialClasses
  • getTextareaClasses
  • getTextClasses
  • getToastClasses
  • getTooltipClasses

Root recipe helper functions:

  • getAccordionHeaderClasses
  • getAccordionIconClasses
  • getAccordionItemClasses
  • getAccordionPanelClasses
  • getAlertDismissClasses
  • getAlertIconClasses
  • getBreadcrumbItemClasses
  • getBreadcrumbLinkClasses
  • getBreadcrumbSeparatorClasses
  • getCarouselCaptionClasses
  • getCarouselControlClasses
  • getCarouselIndicatorClasses
  • getCarouselIndicatorsClasses
  • getCarouselSlideClasses
  • getCarouselViewportClasses
  • getDatepickerGridClasses
  • getDatepickerHeaderClasses
  • getDatepickerWeekdayClasses
  • getDropdownDividerClasses
  • getDropdownHeaderClasses
  • getDropdownItemClasses
  • getDropdownMenuClasses
  • getExternalAuthButtonIconClasses
  • getFieldsetLegendClasses
  • getFooterChipClasses
  • getFooterDividerClasses
  • getFooterHeadingClasses
  • getFooterLinkClasses
  • getFooterLinksClasses
  • getFooterMutedClasses
  • getFooterTextClasses
  • getInputErrorMessageClasses
  • getInputGroupAddonClasses
  • getInputHelperTextClasses
  • getInputLabelClasses
  • getInputWrapperClasses
  • getListGroupItemClasses
  • getListGroupItemHeadingClasses
  • getListGroupItemTextClasses
  • getLogoCloudClasses
  • getLogoCloudItemClasses
  • getModalOverlayClasses
  • getNavLinkClasses
  • getNavLinksClasses
  • getOffcanvasBackdropClasses
  • getOffcanvasBodyClasses
  • getOffcanvasFooterClasses
  • getOffcanvasHeaderClasses
  • getPaginationEllipsisClasses
  • getPaginationItemClasses
  • getPopoverArrowClasses
  • getPopoverBodyClasses
  • getPopoverHeaderClasses
  • getPricingCardBadgeClasses
  • getPricingCardDescriptionClasses
  • getPricingCardPriceClasses
  • getPricingCardPriceContainerClasses
  • getProgressBarClasses
  • getProgressLabelClasses
  • getQuoteAttributionClasses
  • getQuoteAttributionNameClasses
  • getQuoteAttributionRoleClasses
  • getQuoteTextClasses
  • getRatingStarClasses
  • getRatingStarsClasses
  • getRatingTextClasses
  • getSidebarBackdropClasses
  • getSidebarGroupClasses
  • getSidebarGroupContentClasses
  • getSidebarGroupIconClasses
  • getSidebarGroupSummaryClasses
  • getSidebarHeaderClasses
  • getSidebarLinkClasses
  • getSidebarToggleClasses
  • getSectionHeadLeadClasses
  • getSectionHeadTitleClasses
  • getStatFigureClasses
  • getStatLabelClasses
  • getStatUnitClasses
  • getStepperIndicatorClasses
  • getStepperLabelClasses
  • getStepperStepClasses
  • getTableRowClasses
  • getTableWrapperClasses
  • getTabsItemClasses
  • getTabsListClasses
  • getTabsPanelClasses
  • getTestimonialAuthorClasses
  • getTestimonialAuthorInfoClasses
  • getTestimonialAuthorNameClasses
  • getTestimonialAuthorTitleClasses
  • getTestimonialQuoteClasses
  • getToastIconClasses

The root package also re-exports the related recipe option, variant, size, and state TypeScript types defined by those recipes.

CSS entry points

  • @phcdevworks/spectre-ui/index.css
  • @phcdevworks/spectre-ui/base.css
  • @phcdevworks/spectre-ui/components.css
  • @phcdevworks/spectre-ui/utilities.css

Icons (@phcdevworks/spectre-ui/icons)

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 IconNode and IconAttributes.

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.

More icon sets

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.

Public Contract Guarantees

ui-contract.manifest.json defines the public styling contract for this package.

It covers:

  • CSS entry points
  • the ./icons subpath 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.

Sidebar interactive-state contract

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-sidebar slides the sidebar into view (transform: translateX(0)).
  • [data-sidebar-open="true"] .sp-sidebar-backdrop shows the backdrop overlay (display: block).
  • Above breakpoints.md, the sidebar docks inline and the backdrop is always hidden, regardless of the data-sidebar-open value.

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 Boundaries

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

Upgrade Expectations For Consumers

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.json are expected to stay aligned

If a downstream package depends on a class, recipe option, or CSS entry point:

  • read CHANGELOG.md for contract change classification
  • prefer documented public exports over internal paths
  • run consuming app validation after package upgrades

Change classification

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.

Relationship To The Rest Of Spectre

Spectre keeps responsibilities separate:

  • @phcdevworks/spectre-tokens defines design values and semantic meaning
  • @phcdevworks/spectre-ui turns those tokens into reusable CSS and type-safe class recipes
  • @phcdevworks/spectre-components turns those styling contracts into framework-agnostic Lit web components
  • Adapters and apps consume @phcdevworks/spectre-ui instead 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.

Consumer Checklist

For downstream packages and compatible apps:

  • import @phcdevworks/spectre-ui/index.css for 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-tokens instead of inventing visual values locally
  • treat dist/ as generated package output, not an authoring surface
  • do not add framework runtime logic to this package

Development

Local setup

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:verify

Developing 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.

Common commands

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

Troubleshooting

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.

Key source areas

  • src/styles/ for source CSS
  • src/recipes/ for class recipes
  • tests/ for contract and regression coverage
  • examples/ 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.

Examples

Use examples/examples.html as the visual index for the package demos.

Available examples include:

  • vanilla.html for the broad component showcase
  • showroom.html for a richer marketing-style composition
  • verification.html and focused verification fixtures for regression checks

Validation

Run the full validation gate before any pull request:

npm run check

This runs: runtime check → lint → changelog validation → export validation → README validation → token drift check → build → CSS contract → tests. All steps must pass.

AI And Automation Boundaries

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.

Contributing

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 check before opening a pull request

See CONTRIBUTING.md for the full workflow.

License

MIT © PHCDevworks. See LICENSE.

About

@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.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages