@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.3.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 accent 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 |
| 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 |
| 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 |
| PricingCard | getPricingCardClasses |
— | — | featured interactive disabled loading fullHeight, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta |
| 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 |
| 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 |
| Nav | getNavClasses |
— | — | bordered sticky fullWidth align: start|center|end, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta |
| Toast | getToastClasses |
info success warning danger neutral |
— | dismissed fullWidth, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta |
| Tooltip | getTooltipClasses |
placement: top bottom left right |
— | visible, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta |
| 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 |
| Modal | getModalClasses |
— | — | open fullWidth, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta |
| Container | getContainerClasses |
maxWidth: prose wide, padding: sm md lg |
— | — |
| Stack | getStackClasses |
direction: vertical horizontal, basis: sidebar, align: center stretch |
— | — |
| Section | getSectionClasses |
spacing: sm md lg, gap: sm md lg |
— | — |
| Prose | getProseClasses |
— | — | — |
| Grid | getGridClasses |
columns: 1 2 3 4 6 12 auto |
gap/columnGap/rowGap: sm md lg |
span/offset/rowSpan/rowOffset/order: 1–12/0–11/first|last|none|1–12, per-breakpoint { base, md, lg } |
| Sidebar | getSidebarClasses |
— | — | bordered |
| Footer | getFooterClasses |
— | — | bordered fullWidth, accent: top|right|bottom|left, accentColor: neutral|brand|info|success|warning|danger|cta |
| 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 onInverse onInverseMuted onSurface onSurfaceMuted onSurfaceSubtle onSurfaceMeta onSurfaceBrand, family: sans serif mono |
xs–6xl |
— |
| Text state | getTextClasses |
transform: none uppercase lowercase capitalize |
— | — |
| 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 |
— | — |
| 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) 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: 70vh; 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.
getDisplayClasses({ level }) (1–6) applies the typography.display
presets, for hero and marketing headings one scale step larger than the matching
heading level. 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.
Motion utilities (.sp-animate-*) switch to their published
animations.reducedMotion counterparts under prefers-reduced-motion.
These primitives are intentionally plain CSS classes in
src/styles/utilities.css with no recipe function — there is no variant or size
axis to validate, so a recipe wrapper would add indirection without a
type-safety benefit. Apply the class name directly.
| Class | Tokens | Usage |
|---|---|---|
.sp-link |
--sp-link-default --sp-link-hover --sp-link-active --sp-link-visited |
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. |
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.
| 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-* |
| Font weight | .sp-font-{weight} |
distinct values already published across --sp-font-{step}-weight / --sp-heading-h{n}-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.
The root package exports CSS path constants plus the recipe functions
re-exported from src/recipes/index.ts.
Root constants:
spectreStylesspectreBaseStylesPathspectreComponentsStylesPathspectreIndexStylesPathspectreUtilitiesStylesPath
Root recipe functions:
getAccordionClassesgetAlertClassesgetAvatarClassesgetBadgeClassesgetBreadcrumbClassesgetButtonClassesgetCardBleedClassesgetCardClassesgetCarouselClassesgetCheckboxClassesgetChoiceCardClassesgetContainerClassesgetDatepickerClassesgetDayClassesgetDisplayClassesgetDropdownClassesgetExternalAuthButtonClassesgetFieldsetClassesgetFileInputClassesgetFooterClassesgetGridClassesgetHeadingClassesgetIconBoxClassesgetInputClassesgetInputGroupClassesgetLabelClassesgetLeadClassesgetListGroupClassesgetModalClassesgetNavClassesgetOffcanvasClassesgetPaginationClassesgetPopoverClassesgetPricingCardClassesgetProgressClassesgetProseClassesgetRadioClassesgetRangeClassesgetRatingClassesgetSectionClassesgetSelectClassesgetSidebarClassesgetSpinnerClassesgetStackClassesgetStepperClassesgetSwitchClassesgetTableClassesgetTabsClassesgetTagClassesgetTestimonialClassesgetTextareaClassesgetTextClassesgetToastClassesgetTooltipClasses
Root recipe helper functions:
getAccordionHeaderClassesgetAccordionIconClassesgetAccordionItemClassesgetAccordionPanelClassesgetAlertDismissClassesgetAlertIconClassesgetBreadcrumbItemClassesgetBreadcrumbLinkClassesgetBreadcrumbSeparatorClassesgetCarouselCaptionClassesgetCarouselControlClassesgetCarouselIndicatorClassesgetCarouselIndicatorsClassesgetCarouselSlideClassesgetCarouselViewportClassesgetDatepickerGridClassesgetDatepickerHeaderClassesgetDatepickerWeekdayClassesgetDropdownDividerClassesgetDropdownHeaderClassesgetDropdownItemClassesgetDropdownMenuClassesgetExternalAuthButtonIconClassesgetFieldsetLegendClassesgetFooterChipClassesgetFooterDividerClassesgetFooterHeadingClassesgetFooterLinkClassesgetFooterLinksClassesgetFooterMutedClassesgetFooterTextClassesgetInputErrorMessageClassesgetInputGroupAddonClassesgetInputHelperTextClassesgetInputLabelClassesgetInputWrapperClassesgetListGroupItemClassesgetListGroupItemHeadingClassesgetListGroupItemTextClassesgetModalOverlayClassesgetNavLinkClassesgetNavLinksClassesgetOffcanvasBackdropClassesgetOffcanvasBodyClassesgetOffcanvasFooterClassesgetOffcanvasHeaderClassesgetPaginationEllipsisClassesgetPaginationItemClassesgetPopoverArrowClassesgetPopoverBodyClassesgetPopoverHeaderClassesgetPricingCardBadgeClassesgetPricingCardDescriptionClassesgetPricingCardPriceClassesgetPricingCardPriceContainerClassesgetProgressBarClassesgetProgressLabelClassesgetRatingStarClassesgetRatingStarsClassesgetRatingTextClassesgetSidebarBackdropClassesgetSidebarGroupClassesgetSidebarGroupSummaryClassesgetSidebarHeaderClassesgetSidebarLinkClassesgetSidebarToggleClassesgetStepperIndicatorClassesgetStepperLabelClassesgetStepperStepClassesgetTableRowClassesgetTableWrapperClassesgetTabsItemClassesgetTabsListClassesgetTabsPanelClassesgetTestimonialAuthorClassesgetTestimonialAuthorInfoClassesgetTestimonialAuthorNameClassesgetTestimonialAuthorTitleClassesgetTestimonialQuoteClassesgetToastIconClasses
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
ui-contract.manifest.json defines the public styling contract for this
package.
It covers:
- CSS entry points
- 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 .sp-sidebar__group-icon when the group
is open. Wrap the nested links in .sp-sidebar__group-content for the standard
bottom spacing. 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:verifyThis project requires Node.js ^22.13.0 || >=24.0.0 and npm >=10.0.0. 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.