| Field | Value |
|---|---|
| Project team | project-design |
| Repository role | Spectre L3b Astro adapter |
| Package/artifact | @phcdevworks/spectre-ui-astro |
| Current version/status | 2.10.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 |
| Roadmap | ROADMAP.md |
| Todo | TODO.md |
| Changelog | CHANGELOG.md |
| Security | SECURITY.md |
Astro-native components for the Spectre UI design system. Drop Spectre components into any Astro project — SSR, SSG, or hybrid — without writing CSS, redefining tokens, or reimplementing recipe logic.
Contributing | Code of Conduct | Changelog | Roadmap | Security Policy
- Twenty-three ready-to-use Astro components — alerts, avatars, badges, buttons, cards, app shell layout, forms, navigation, overlays, feedback, pricing, ratings, and testimonials
- SSR-safe by default — deterministic markup, no client-side JavaScript, stable accessibility wiring
- Thin wrapper pattern — styling comes entirely from
@phcdevworks/spectre-ui; this package adds Astro slots, typed props, and framework ergonomics - Re-exported recipe helpers — use the same class functions the components use, directly from your Astro frontmatter or TypeScript
npm install @phcdevworks/spectre-ui-astro @phcdevworks/spectre-ui@phcdevworks/spectre-ui is a required peer dependency. It owns the CSS, class
recipes, and design system behavior that powers every component in this package.
Install this package inside an Astro project; astro is also a peer dependency
supplied by the consuming app.
If your project works with Spectre design tokens directly:
npm install @phcdevworks/spectre-tokensThis package ships no CSS. Add the Spectre UI stylesheet once in your Astro layout:
---
// src/layouts/BaseLayout.astro
import '@phcdevworks/spectre-ui/index.css'
interface Props {
title?: string
}
const { title = 'My Astro site' } = Astro.props
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>All Spectre components pick up the stylesheet through the layout. Do not import
it per-component — CSS ownership stays with @phcdevworks/spectre-ui.
---
import BaseLayout from '../layouts/BaseLayout.astro'
import {
SpBadge,
SpButton,
SpCard,
SpIconBox,
SpInput,
SpPricingCard,
} from '@phcdevworks/spectre-ui-astro'
---
<BaseLayout title="My page">
<SpCard variant="elevated">
<SpIconBox variant="primary" size="md">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l7 4v12l-7 4-7-4V6l7-4z" fill="currentColor" />
</svg>
</SpIconBox>
<SpBadge variant="success" size="sm">Stable</SpBadge>
<h2>Build faster with Spectre</h2>
<SpButton variant="primary" size="lg">Get started</SpButton>
</SpCard>
<SpInput
id="email"
label="Email"
name="email"
type="email"
placeholder="you@example.com"
helperText="We will never share your email."
/>
<SpPricingCard featured>
<h3 slot="header">Pro</h3>
<span slot="price">$29/mo</span>
<span slot="description">For growing teams.</span>
<SpButton variant="primary" fullWidth>Choose plan</SpButton>
</SpPricingCard>
</BaseLayout>All components are SSR-safe. Styling comes from the upstream Spectre UI
stylesheet — this package adds no local CSS. Every component accepts a class
prop for additional classes and spreads unknown props onto the root element.
SpSidebar is the one exception to "no client-side JavaScript": it owns the
toggle/backdrop-close interaction for its off-canvas drawer behavior, rendering
closed by default with no layout shift on hydration.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
ButtonVariant |
— | Visual style: "primary" "secondary" "ghost" |
size |
ButtonSize |
— | Size: "sm" "md" "lg" |
as |
"button" | "a" | "span" | "div" | "li" |
"button" |
Rendered element |
href |
string |
— | URL when as="a" |
type |
"button" | "submit" | "reset" |
"button" |
Button type (button elements only) |
disabled |
boolean |
— | Disables the element; suppresses navigation on anchors |
loading |
boolean |
— | Loading state; implies disabled |
fullWidth |
boolean |
— | Stretches to fill its container |
iconOnly |
boolean |
— | Removes text padding for icon-only buttons |
pill |
boolean |
— | Fully rounded corners |
hovered |
boolean |
— | Force-applies hover styling |
focused |
boolean |
— | Force-applies focus styling |
active |
boolean |
— | Force-applies active styling |
aria-label |
string |
— | Accessible label |
tabindex |
number |
— | Tab index override |
class |
string |
— | Additional CSS classes |
<SpButton variant="primary" size="lg">Get started</SpButton>
<SpButton variant="ghost" as="a" href="/docs">Read docs</SpButton>
<SpButton variant="primary" type="submit">Save</SpButton>
<SpButton variant="primary" loading>Saving…</SpButton>
<SpButton variant="secondary" as="a" href="/item" disabled>Unavailable</SpButton>| Prop | Type | Default | Description |
|---|---|---|---|
variant |
CardVariant |
— | Visual style: "elevated" "outline" "flat" "ghost" |
as |
"div" | "section" | "article" | "aside" | "a" | "button" | "li" | … |
"div" |
Rendered element |
interactive |
boolean |
— | Adds hover/focus styles; adds role="button" for non-native elements |
padded |
boolean |
— | Applies inner padding |
fullHeight |
boolean |
— | Stretches to full container height |
disabled |
boolean |
— | Disables the card; suppresses navigation on anchors |
loading |
boolean |
— | Loading state |
href |
string |
— | URL when as="a" |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpCard variant="elevated">
<h2>Card title</h2>
<p>Card content goes here.</p>
</SpCard>
<!-- Semantic article markup -->
<SpCard variant="outline" as="article">
<h2>Blog post title</h2>
</SpCard>
<!-- Linked card -->
<SpCard variant="elevated" as="a" href="/post/1" interactive aria-label="Read post">
<h2>Clickable card</h2>
</SpCard>The default slot renders any child content.
| Prop | Type | Default | Description |
|---|---|---|---|
maxWidth |
ContainerMaxWidth |
— | "prose" bounds content to a readable line length |
as |
"div" | "section" | "main" | "article" | "aside" |
"div" |
Rendered element |
id |
string |
— | Element id |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpContainer>
<p>Centered, max-width content.</p>
</SpContainer>
<SpContainer as="main" aria-label="Main content">
<p>Semantic main wrapper.</p>
</SpContainer>
<SpContainer maxWidth="prose">
<p>Bounded to a readable line length.</p>
</SpContainer>The default slot renders any child content.
| Prop | Type | Default | Description |
|---|---|---|---|
direction |
StackDirection |
"vertical" |
"vertical" | "horizontal" |
basis |
StackBasis |
— | "sidebar" gives the stack a fixed sidebar width |
as |
"div" | "section" | "ul" | "ol" | "nav" |
"div" |
Rendered element |
id |
string |
— | Element id |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpStack>
<p>Item one</p>
<p>Item two</p>
</SpStack>
<SpStack direction="horizontal" as="nav" aria-label="Primary">
<a href="/">Home</a>
<a href="/about">About</a>
</SpStack>
<SpStack direction="horizontal" basis="sidebar">
<p>Fixed-width sidebar-shaped flex child.</p>
</SpStack>The default slot renders any child content.
| Prop | Type | Default | Description |
|---|---|---|---|
as |
"section" | "div" | "article" | "aside" | "main" |
"section" |
Rendered element |
id |
string |
— | Element id |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpSection aria-label="Features">
<SpContainer>
<h2>Features</h2>
</SpContainer>
</SpSection>The default slot renders any child content.
| Prop | Type | Default | Description |
|---|---|---|---|
bordered |
boolean |
— | Applies a right border |
as |
"aside" | "div" | "nav" |
"aside" |
Rendered element for the sidebar itself |
id |
string |
— | Element ID for the sidebar |
aria-label |
string |
— | Accessible label for the sidebar |
toggleLabel |
string |
"Toggle sidebar" |
Accessible label for the hamburger toggle button |
class |
string |
— | Additional CSS classes for the sidebar |
SpSidebar is the first adapter component to own interactive state. It renders
a wrapper element with data-sidebar-open="false" (closed by default,
SSR-safe), a hamburger toggle button, a backdrop element
(getSidebarBackdropClasses), and the sidebar element itself
(getSidebarClasses). Below breakpoints.md (768px), upstream
@phcdevworks/spectre-ui CSS renders the sidebar off-canvas; toggling the
button or tapping the backdrop flips data-sidebar-open, which upstream CSS
reacts to. Above breakpoints.md, the sidebar docks inline and the toggle has
no visible effect, matching the upstream CSS contract.
Build sidebar links in the default slot using the re-exported
getSidebarLinkClasses helper, since per-link active/disabled/hover/focus state
is consumer-driven.
---
import { SpSidebar, getSidebarLinkClasses } from '@phcdevworks/spectre-ui-astro'
const linkClass = getSidebarLinkClasses()
const activeLinkClass = getSidebarLinkClasses({ active: true })
---
<SpSidebar bordered aria-label="Primary">
<a class={activeLinkClass} href="/" aria-current="page">Home</a>
<a class={linkClass} href="/about">About</a>
</SpSidebar>| Prop | Type | Default | Description |
|---|---|---|---|
columns |
GridColumns |
1 |
1 | 2 | 3 | 4 | 6 | 12 |
gap |
GridGap |
"md" |
"sm" | "md" | "lg" |
as |
"div" | "section" | "ul" | "ol" |
"div" |
Rendered element |
id |
string |
— | Element id |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpGrid columns={3} gap="lg">
<SpCard>One</SpCard>
<SpCard>Two</SpCard>
<SpCard>Three</SpCard>
</SpGrid>The default slot renders any child content.
SpInput renders a labeled input group: wrapper, optional label, input,
optional helper text, and optional error message.
SpInput requires an explicit id whenever label, helperText, or
errorMessage is present. This is an SSR invariant — without a stable id, the
for/aria-describedby associations would be nondeterministic. The component
throws at render time if the requirement is violated.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string |
— | Required when using label, helperText, or errorMessage |
label |
string |
— | Renders a <label> associated with the input |
helperText |
string |
— | Renders helper text below the input |
errorMessage |
string |
— | Renders an error message; suppresses helperText |
state |
InputState |
— | "default" "success" "error" "disabled" "loading" |
size |
InputSize |
— | Size: "sm" "md" "lg" |
fullWidth |
boolean |
— | Stretches input to fill its container |
pill |
boolean |
— | Fully rounded corners |
disabled |
boolean |
— | Disables the input |
loading |
boolean |
— | Loading state |
as |
"div" | "form" | "fieldset" | … |
"div" |
Rendered wrapper element |
class |
string |
— | Additional CSS classes on the <input> element |
…rest |
— | — | Any HTML input attribute (type, name, placeholder, required, etc.) |
<!-- Standalone — no label, no id required -->
<SpInput type="search" placeholder="Search…" />
<!-- Labeled with helper text — id required -->
<SpInput
id="email"
label="Email"
name="email"
type="email"
placeholder="you@example.com"
helperText="We will never share your email."
/>
<!-- Validation error — errorMessage suppresses helperText -->
<SpInput
id="password"
label="Password"
type="password"
state="error"
errorMessage="Password must be at least 8 characters."
/>
<!-- Disabled field -->
<SpInput
id="api-key"
label="API Key"
value="••••••••"
state="disabled"
disabled
/>
<!-- Pill shape, small size -->
<SpInput id="search-pill" label="Search" size="sm" pill placeholder="Search…" />| Prop | Type | Default | Description |
|---|---|---|---|
variant |
AlertVariant |
"info" |
Visual style: "info" "success" "warning" "danger" "neutral" |
size |
AlertSize |
"md" |
Size: "sm" "md" "lg" |
as |
"div" | "section" | "aside" | "article" |
"div" |
Rendered element |
dismissed |
boolean |
— | Applies dismissed state styling |
interactive |
boolean |
— | Adds hover/focus styles and tabindex="0" |
fullWidth |
boolean |
— | Stretches to full width |
disabled |
boolean |
— | Disables the alert |
loading |
boolean |
— | Loading state (also sets disabled behavior) |
id |
string |
— | Element ID |
aria-label |
string |
— | Accessible label |
aria-describedby |
string |
— | Associates a description element |
class |
string |
— | Additional CSS classes |
<SpAlert variant="success">Your changes have been saved.</SpAlert>
<SpAlert variant="warning" size="sm">Session expires soon.</SpAlert>
<SpAlert variant="danger" dismissed>This alert has been dismissed.</SpAlert>
<SpAlert variant="info" as="aside" aria-label="Info notice">Read the docs.</SpAlert>| Prop | Type | Default | Description |
|---|---|---|---|
shape |
AvatarShape |
"circle" |
Shape: "circle" "square" |
size |
AvatarSize |
"md" |
Size: "xs" "sm" "md" "lg" "xl" |
as |
"div" | "span" | "figure" | "a" | "button" |
"div" |
Rendered element |
interactive |
boolean |
— | Adds hover/focus styles |
disabled |
boolean |
— | Disables the avatar |
loading |
boolean |
— | Loading state |
fullWidth |
boolean |
— | Stretches to full width |
placeholder |
boolean |
— | Applies placeholder styling |
href |
string |
— | URL when as="a" |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpAvatar size="lg">
<img src="/avatars/jane.jpg" alt="Jane Doe" />
</SpAvatar>
<SpAvatar shape="square" size="sm" placeholder>
JD
</SpAvatar>
<SpAvatar as="a" href="/profile" interactive>
<img src="/avatars/jane.jpg" alt="View profile" />
</SpAvatar>| Prop | Type | Default | Description |
|---|---|---|---|
variant |
BadgeVariant |
— | Visual style: "primary" "success" "warning" "danger" "info" |
size |
BadgeSize |
— | Size: "sm" "md" "lg" |
as |
"span" | "div" | "a" | "button" | "li" | "time" | "mark" |
"span" |
Rendered element |
interactive |
boolean |
— | Adds hover/focus styles |
fullWidth |
boolean |
— | Stretches to full width |
disabled |
boolean |
— | Disables the badge |
loading |
boolean |
— | Loading state |
href |
string |
— | URL when as="a" |
datetime |
string |
— | Datetime value when as="time" |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpBadge variant="success">Active</SpBadge>
<SpBadge variant="warning" size="sm">Beta</SpBadge>
<SpBadge variant="primary" as="a" href="/changelog" interactive>New</SpBadge>
<SpBadge variant="danger" as="time" datetime="2025-01-01">Jan 2025</SpBadge>| Prop | Type | Default | Description |
|---|---|---|---|
variant |
IconBoxVariant |
— | Color: "primary" "success" "warning" "danger" "info" |
size |
IconBoxSize |
— | Size: "sm" "md" "lg" |
as |
"span" | "div" | "i" | "a" | "button" | "li" |
"span" |
Rendered element |
pill |
boolean |
— | Fully rounded corners |
interactive |
boolean |
— | Adds hover/focus styles |
disabled |
boolean |
— | Disables the icon box |
loading |
boolean |
— | Loading state |
href |
string |
— | URL when as="a" |
aria-label |
string |
— | Accessible label (use when the icon conveys standalone meaning) |
class |
string |
— | Additional CSS classes |
<!-- Decorative icon — mark the icon aria-hidden -->
<SpIconBox variant="primary" size="md">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l7 4v12l-7 4-7-4V6l7-4z" fill="currentColor" />
</svg>
</SpIconBox>
<!-- Standalone meaning — label the component -->
<SpIconBox variant="success" size="sm" aria-label="Success">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d="M20 6 9 17l-5-5" />
</svg>
</SpIconBox>Use aria-hidden="true" on the icon when surrounding context already describes
it. Use aria-label on the component when the icon box conveys standalone
meaning.
Named slots map to the structural sections of the pricing card. Slot wrappers render only when their slot is populated — empty slots produce no markup.
| Prop | Type | Default | Description |
|---|---|---|---|
featured |
boolean |
— | Highlights the card as the recommended tier |
fullHeight |
boolean |
— | Stretches to full container height |
interactive |
boolean |
— | Adds hover/focus styles |
disabled |
boolean |
— | Disables the card |
loading |
boolean |
— | Loading state |
as |
"div" | "section" | "article" | … |
"div" |
Rendered element |
class |
string |
— | Additional CSS classes |
| Slot | Description |
|---|---|
header |
Plan name or heading |
badge |
Tag or label (e.g., "Popular") |
price |
Price string or element |
description |
Short plan description |
| (default) | Feature list or body content |
footer |
CTA button or footer action |
<SpPricingCard featured>
<h3 slot="header">Pro</h3>
<span slot="badge">Popular</span>
<span slot="price">$29/mo</span>
<span slot="description">For growing teams and businesses.</span>
<ul>
<li>Unlimited projects</li>
<li>Advanced analytics</li>
<li>Priority support</li>
</ul>
<SpButton slot="footer" variant="primary" fullWidth>Choose Pro</SpButton>
</SpPricingCard>Renders a star rating. Stars are built from value/max. Provide a custom star
SVG via the star-icon slot. The star container is aria-hidden="true" —
always pass aria-label for screen readers.
| Prop | Type | Default | Description |
|---|---|---|---|
value |
number |
0 |
Number of filled stars |
max |
number |
5 |
Total star count |
size |
RatingSize |
— | Size |
interactive |
boolean |
— | Adds hover/focus styles |
disabled |
boolean |
— | Disables the rating |
loading |
boolean |
— | Loading state |
fullWidth |
boolean |
— | Stretches to fill container |
pill |
boolean |
— | Fully rounded corners |
as |
"div" | "span" | "section" | … |
"div" |
Rendered element |
aria-label |
string |
— | Screen-reader description of the rating value |
class |
string |
— | Additional CSS classes |
| Slot | Description |
|---|---|
star-icon |
Custom star icon. Receives isFilled as a slot prop. Defaults to ★. |
| (default) | Optional text shown after the stars (e.g., "4.8 out of 5") |
<!-- Basic rating -->
<SpRating value={4} max={5} aria-label="4 out of 5 stars" />
<!-- With visible text -->
<SpRating value={4} max={5} aria-label="4 out of 5 stars">
4.0 out of 5
</SpRating>
<!-- Custom star icon -->
<SpRating value={3} max={5} aria-label="3 out of 5">
<svg slot="star-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l3 6.5 7 1-5 4.9 1.2 7L12 18l-6.2 3.4 1.2-7L2 9.5l7-1z" fill="currentColor" />
</svg>
</SpRating>Named slots map to the structural sections of the testimonial. Slot wrappers render only when their slot is populated.
| Prop | Type | Default | Description |
|---|---|---|---|
fullHeight |
boolean |
— | Stretches to full container height |
interactive |
boolean |
— | Adds hover/focus styles |
disabled |
boolean |
— | Disables the testimonial |
loading |
boolean |
— | Loading state |
as |
"div" | "section" | "article" | "blockquote" | … |
"div" |
Rendered element |
class |
string |
— | Additional CSS classes |
| Slot | Description |
|---|---|
quote |
Quotation text |
author-image |
Author avatar or image element |
author-name |
Author display name |
author-title |
Author job title or affiliation |
<SpTestimonial as="blockquote">
<p slot="quote">
"Spectre UI cut our Astro prototype time in half."
</p>
<img
slot="author-image"
src="/avatars/jane.jpg"
alt="Jane Doe"
width="40"
height="40"
/>
<span slot="author-name">Jane Doe</span>
<span slot="author-title">Frontend Lead at Acme Corp</span>
</SpTestimonial>A non-interactive status indicator. Renders as <div role="status"> with a
default aria-label of "Loading".
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
SpinnerVariant |
— | Color variant: "primary" "secondary" "success" "warning" "danger" "info" "neutral" "accent" "cta" |
size |
SpinnerSize |
"md" |
Size: "sm" "md" "lg" |
disabled |
boolean |
— | Disabled state |
loading |
boolean |
— | Loading state (also sets disabled behavior) |
aria-label |
string |
"Loading" |
Accessible label for the spinner |
id |
string |
— | Element ID |
class |
string |
— | Additional CSS classes |
<SpSpinner />
<SpSpinner variant="primary" size="lg" />
<SpSpinner loading aria-label="Saving changes" />| Prop | Type | Default | Description |
|---|---|---|---|
variant |
TagVariant |
"default" |
Visual style: "default" "primary" "secondary" "success" "warning" "danger" "info" "neutral" "accent" "cta" "outline" "ghost" |
size |
TagSize |
— | Size: "sm" "md" "lg" |
as |
"span" | "div" | "li" | "a" | "button" |
"span" |
Rendered element |
dismissible |
boolean |
— | Applies dismissible styling |
selected |
boolean |
— | Applies selected state and aria-pressed |
interactive |
boolean |
— | Adds hover/focus styles and tabindex="0" |
fullWidth |
boolean |
— | Stretches to full width |
disabled |
boolean |
— | Disables the tag |
loading |
boolean |
— | Loading state (also sets disabled behavior) |
href |
string |
— | URL when as="a" |
id |
string |
— | Element ID |
aria-label |
string |
— | Accessible label |
aria-describedby |
string |
— | Associates a description element |
class |
string |
— | Additional CSS classes |
<SpTag>Default</SpTag>
<SpTag variant="primary" size="sm">New</SpTag>
<SpTag variant="success" selected>Active</SpTag>
<SpTag variant="info" dismissible>Removable</SpTag>
<SpTag as="a" href="/docs" variant="neutral" interactive>Docs</SpTag>
<SpTag as="button" variant="primary" disabled>Unavailable</SpTag>| Prop | Type | Default | Description |
|---|---|---|---|
fullWidth |
boolean |
— | Stretches to full width |
as |
"div" | "nav" |
"div" |
Rendered element |
id |
string |
— | Element ID |
class |
string |
— | Additional CSS classes |
SpDropdown renders the dropdown container only. Build the menu and items in
the default slot using the re-exported getDropdownMenuClasses and
getDropdownItemClasses helpers, since open/closed state and per-item
active/disabled/hover/focus state are consumer-driven.
---
import { SpDropdown, getDropdownMenuClasses, getDropdownItemClasses } from '@phcdevworks/spectre-ui-astro'
const menuClass = getDropdownMenuClasses({ placement: 'bottom-start', open: true })
const itemClass = getDropdownItemClasses()
const activeItemClass = getDropdownItemClasses({ active: true })
---
<SpDropdown>
<button>Options</button>
<div class={menuClass} role="menu">
<a class={activeItemClass} href="/profile" role="menuitem">Profile</a>
<a class={itemClass} href="/settings" role="menuitem">Settings</a>
</div>
</SpDropdown>| Prop | Type | Default | Description |
|---|---|---|---|
bordered |
boolean |
— | Applies a top border |
fullWidth |
boolean |
— | Stretches to full width |
as |
"footer" | "div" | "section" |
"footer" |
Rendered element |
id |
string |
— | Element ID |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
<SpFooter bordered>
<p>© 2026 PHCDevworks</p>
</SpFooter>The default slot renders any child content.
| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
— | Applies open styling to the overlay and modal, and toggles aria-hidden |
fullWidth |
boolean |
— | Stretches the modal to full width |
as |
"div" | "section" |
"div" |
Rendered element for the modal |
id |
string |
— | Element ID for the modal |
aria-label |
string |
— | Accessible label for the modal |
aria-labelledby |
string |
— | Associates a title element |
aria-describedby |
string |
— | Associates a description element |
class |
string |
— | Additional CSS classes for the modal |
SpModal renders an overlay element (getModalOverlayClasses) wrapping the
modal element (getModalClasses), with role="dialog" and aria-modal="true".
Toggling open is consumer-driven (no client-side JS is included).
<SpModal open aria-labelledby="modal-title">
<h2 id="modal-title">Confirm deletion</h2>
<p>This action cannot be undone.</p>
</SpModal>| Prop | Type | Default | Description |
|---|---|---|---|
bordered |
boolean |
— | Applies a border |
sticky |
boolean |
— | Applies sticky positioning |
fullWidth |
boolean |
— | Stretches to full width |
as |
"nav" | "div" | "header" | "section" |
"nav" |
Rendered element |
id |
string |
— | Element ID |
aria-label |
string |
— | Accessible label for the nav landmark |
class |
string |
— | Additional CSS classes |
SpNav renders the nav container only. Build links in the default slot using
the re-exported getNavLinksClasses and getNavLinkClasses helpers, since
per-link active/disabled/hover/focus state is consumer-driven.
---
import { SpNav, getNavLinksClasses, getNavLinkClasses } from '@phcdevworks/spectre-ui-astro'
const linksClass = getNavLinksClasses()
const activeLinkClass = getNavLinkClasses({ active: true })
const linkClass = getNavLinkClasses()
---
<SpNav bordered sticky aria-label="Main">
<div class={linksClass}>
<a class={activeLinkClass} href="/" aria-current="page">Home</a>
<a class={linkClass} href="/about">About</a>
</div>
</SpNav>| Prop | Type | Default | Description |
|---|---|---|---|
variant |
ToastVariant |
"info" |
Visual style: "info" "success" "warning" "danger" |
dismissed |
boolean |
— | Applies dismissed state styling |
fullWidth |
boolean |
— | Stretches to full width |
as |
"div" | "li" | "section" |
"div" |
Rendered element |
id |
string |
— | Element ID |
aria-label |
string |
— | Accessible label |
class |
string |
— | Additional CSS classes |
SpToast renders role="status", aria-live="polite", and
aria-atomic="true" by default. Pass content to a named icon slot to wrap it
in getToastIconClasses styling; the wrapper is only rendered when the slot is
used.
<SpToast variant="success">Changes saved.</SpToast>
<SpToast variant="danger">
<Fragment slot="icon"><svg aria-hidden="true">...</svg></Fragment>
Something went wrong.
</SpToast>| Prop | Type | Default | Description |
|---|---|---|---|
placement |
TooltipPlacement |
"top" |
Placement: "top" "bottom" "left" "right" |
visible |
boolean |
— | Applies visible state styling |
as |
"div" | "span" |
"div" |
Rendered element |
id |
string |
— | Element ID |
role |
string |
"tooltip" |
ARIA role |
class |
string |
— | Additional CSS classes |
<SpTooltip placement="bottom" visible>Save your changes</SpTooltip>Most components accept an as prop to change the rendered HTML element without
changing component behavior or styling.
<!-- SpButton: renders <button> by default -->
<SpButton variant="primary">Submit</SpButton>
<!-- SpButton: renders <a> with all button styling -->
<SpButton variant="primary" as="a" href="/get-started">Get started</SpButton>
<!-- SpCard: semantic article markup -->
<SpCard variant="elevated" as="article">
<h2>Article title</h2>
</SpCard>
<!-- SpBadge: inline time element -->
<SpBadge variant="primary" as="time" datetime="2026-01-01">Jan 2026</SpBadge>Disabled navigation: When disabled is set on an anchor (as="a"), the
href is suppressed so the element is not keyboard-navigable.
aria-disabled="true" is always set alongside disabled.
Button type: When as="button" (the default for SpButton), type
defaults to "button" to prevent accidental form submission. Pass
type="submit" explicitly when needed.
Non-native interactive elements: Setting interactive={true} on a div or
span adds role="button" and tabindex="0" automatically. Use this only when
a native button or a is genuinely impractical.
@phcdevworks/spectre-ui-astro works in all Astro output modes: "static",
"server", and "hybrid".
No client-side JavaScript. Every component renders entirely at build time or
in the server step. No hydration directives are needed or used. Interactive
styles (hover, focus, active) are driven by CSS from @phcdevworks/spectre-ui.
Deterministic markup. All IDs, class names, and attributes are computed from explicit props. Every render of the same props produces identical HTML — safe for SSR streaming and static generation.
SpInput explicit id requirement. When any of label, helperText, or
errorMessage are passed, an explicit id prop is required. Without it, the
component throws during render to prevent nondeterministic
for/aria-describedby wiring in server-rendered and statically generated
output.
<!-- id is required whenever label, helperText, or errorMessage is set -->
<SpInput id="email" label="Email" name="email" />Omitting id when label is set throws at render time with a clear error
message. This applies in both SSR and static builds.
CSS in SSR. Import the Spectre UI stylesheet in your Astro layout once. Astro handles CSS bundling and injection in both SSR and static builds — no runtime style injection occurs from this package.
The package re-exports class recipe functions from @phcdevworks/spectre-ui.
Use these when you need Spectre-aligned class names outside of the Astro
components — in layout markup, in headless patterns, or when mapping over
dynamic data.
---
// Using getButtonClasses to style plain anchor tags in a nav
import { getButtonClasses } from '@phcdevworks/spectre-ui-astro'
const navClass = getButtonClasses({ variant: 'ghost', size: 'sm' })
const links = [
{ href: '/', label: 'Home' },
{ href: '/about', label: 'About' },
{ href: '/blog', label: 'Blog' },
]
---
<nav>
{links.map(link => (
<a class={navClass} href={link.href}>{link.label}</a>
))}
</nav>| Helper | For |
|---|---|
getAlertClasses |
Alert class generation |
getAvatarClasses |
Avatar class generation |
getButtonClasses |
Button class generation |
getCardClasses |
Card class generation |
getBadgeClasses |
Badge class generation |
getContainerClasses |
Container class generation |
getDropdownClasses |
Dropdown root classes |
getDropdownMenuClasses |
Dropdown menu container |
getDropdownItemClasses |
Individual dropdown item |
getFooterClasses |
Footer class generation |
getIconBoxClasses |
Icon box class generation |
getInputClasses |
Input class generation |
getModalClasses |
Modal root classes |
getModalOverlayClasses |
Modal overlay/backdrop classes |
getNavClasses |
Nav root classes |
getNavLinksClasses |
Nav links container |
getNavLinkClasses |
Individual nav link |
getPricingCardClasses |
Pricing card root classes |
getPricingCardBadgeClasses |
Pricing card badge wrapper |
getPricingCardPriceContainerClasses |
Pricing card price container |
getPricingCardPriceClasses |
Pricing card price element |
getPricingCardDescriptionClasses |
Pricing card description |
getRatingClasses |
Rating root classes |
getRatingStarsClasses |
Rating star container |
getRatingStarClasses |
Individual star element |
getRatingTextClasses |
Rating text element |
getSectionClasses |
Section class generation |
getSidebarClasses |
Sidebar root classes |
getSidebarLinkClasses |
Individual sidebar link |
getSidebarBackdropClasses |
Sidebar off-canvas backdrop |
getStackClasses |
Stack class generation |
getTestimonialClasses |
Testimonial root classes |
getTestimonialQuoteClasses |
Quote wrapper |
getTestimonialAuthorClasses |
Author section wrapper |
getTestimonialAuthorInfoClasses |
Author info wrapper |
getTestimonialAuthorNameClasses |
Author name element |
getTestimonialAuthorTitleClasses |
Author title element |
getToastClasses |
Toast root classes |
getToastIconClasses |
Toast icon wrapper |
getTooltipClasses |
Tooltip class generation |
Recipe option and variant types are also re-exported: AlertRecipeOptions,
AlertVariant, AlertSize, AvatarRecipeOptions, AvatarShape, AvatarSize,
BadgeRecipeOptions, BadgeVariant, BadgeSize, ButtonRecipeOptions,
ButtonVariant, ButtonSize, CardRecipeOptions, CardVariant,
ContainerRecipeOptions, ContainerMaxWidth, DropdownRecipeOptions,
DropdownMenuRecipeOptions, DropdownItemRecipeOptions, DropdownPlacement,
FooterRecipeOptions, IconBoxRecipeOptions, IconBoxVariant, IconBoxSize,
InputRecipeOptions, InputState, InputSize, ModalRecipeOptions,
ModalOverlayRecipeOptions, NavRecipeOptions, NavLinkRecipeOptions,
PricingCardRecipeOptions, RatingRecipeOptions, SectionRecipeOptions,
SidebarRecipeOptions, SidebarLinkRecipeOptions, StackRecipeOptions,
StackDirection, StackBasis, TestimonialRecipeOptions,
ToastRecipeOptions, ToastIconRecipeOptions, ToastVariant,
TooltipRecipeOptions, TooltipPlacement.
- Astro-native component delivery for Spectre UI recipes and classes
- Astro-friendly, SSR-safe component interfaces and composition patterns
- Type-safe framework bindings for the upstream Spectre UI contract
- Adapter-level ergonomics that make
@phcdevworks/spectre-uistraightforward to consume in Astro projects - A reference implementation for future Spectre framework adapters
Golden rule: bind the upstream Spectre UI contract for Astro, do not redefine it.
- Design values or token meaning —
@phcdevworks/spectre-tokens - Core CSS, utilities, Tailwind helpers, or class recipe logic —
@phcdevworks/spectre-ui - Local styling systems that diverge from the shared Spectre contract
Use @phcdevworks/spectre-ui-astro when:
- you are building an Astro project and want Spectre UI components as first-class Astro components
- you need SSR-safe, type-safe component interfaces that bind the upstream
@phcdevworks/spectre-uirecipe contract without reimplementing it - you want to compose with Spectre's shared recipe helpers from TypeScript in an Astro project
Do not use this package when:
- you are using a different framework (React, Vue, Svelte, etc.) — this package is Astro-only
- you want to define custom tokens or override Spectre's design values — that
belongs in
@phcdevworks/spectre-tokens - you want to add or change class recipes, CSS utilities, or Tailwind helpers —
that belongs in
@phcdevworks/spectre-ui - you need a framework-agnostic styling contract — consume
@phcdevworks/spectre-uidirectly
import {
SpAlert,
SpAvatar,
SpBadge,
SpButton,
SpCard,
SpContainer,
SpDropdown,
SpFooter,
SpGrid,
SpIconBox,
SpInput,
SpModal,
SpNav,
SpPricingCard,
SpRating,
SpSection,
SpSidebar,
SpSpinner,
SpStack,
SpTag,
SpTestimonial,
SpToast,
SpTooltip
} from '@phcdevworks/spectre-ui-astro'
import {
getButtonClasses,
getBadgeClasses,
getCardClasses
// …all recipe helpers and types
} from '@phcdevworks/spectre-ui-astro'import SpAlert from '@phcdevworks/spectre-ui-astro/components/SpAlert.astro'
import SpAvatar from '@phcdevworks/spectre-ui-astro/components/SpAvatar.astro'
import SpBadge from '@phcdevworks/spectre-ui-astro/components/SpBadge.astro'
import SpButton from '@phcdevworks/spectre-ui-astro/components/SpButton.astro'
import SpCard from '@phcdevworks/spectre-ui-astro/components/SpCard.astro'
import SpContainer from '@phcdevworks/spectre-ui-astro/components/SpContainer.astro'
import SpDropdown from '@phcdevworks/spectre-ui-astro/components/SpDropdown.astro'
import SpFooter from '@phcdevworks/spectre-ui-astro/components/SpFooter.astro'
import SpGrid from '@phcdevworks/spectre-ui-astro/components/SpGrid.astro'
import SpIconBox from '@phcdevworks/spectre-ui-astro/components/SpIconBox.astro'
import SpInput from '@phcdevworks/spectre-ui-astro/components/SpInput.astro'
import SpModal from '@phcdevworks/spectre-ui-astro/components/SpModal.astro'
import SpNav from '@phcdevworks/spectre-ui-astro/components/SpNav.astro'
import SpPricingCard from '@phcdevworks/spectre-ui-astro/components/SpPricingCard.astro'
import SpRating from '@phcdevworks/spectre-ui-astro/components/SpRating.astro'
import SpSection from '@phcdevworks/spectre-ui-astro/components/SpSection.astro'
import SpSidebar from '@phcdevworks/spectre-ui-astro/components/SpSidebar.astro'
import SpSpinner from '@phcdevworks/spectre-ui-astro/components/SpSpinner.astro'
import SpStack from '@phcdevworks/spectre-ui-astro/components/SpStack.astro'
import SpTag from '@phcdevworks/spectre-ui-astro/components/SpTag.astro'
import SpTestimonial from '@phcdevworks/spectre-ui-astro/components/SpTestimonial.astro'
import SpToast from '@phcdevworks/spectre-ui-astro/components/SpToast.astro'
import SpTooltip from '@phcdevworks/spectre-ui-astro/components/SpTooltip.astro'The adapter does not export a CSS helper or path. Import the stylesheet directly
from @phcdevworks/spectre-ui/index.css.
Each component family is classified by its support status in this adapter.
| Family | Status | Notes |
|---|---|---|
| alert | stable | Full prop, slot, ARIA, and SSR coverage |
| avatar | stable | Full prop, slot, ARIA, and SSR coverage |
| badge | stable | Full prop, slot, ARIA, and SSR coverage |
| button | stable | Full prop, slot, ARIA, and SSR coverage |
| card | stable | Full prop, slot, ARIA, and SSR coverage |
| container | stable | Full prop, slot, ARIA, and SSR coverage |
| dropdown | stable | Full prop, slot, and SSR coverage |
| footer | stable | Full prop, slot, ARIA, and SSR coverage |
| grid | stable | Full prop, slot, and SSR coverage |
| icon-box | stable | Full prop, slot, ARIA, and SSR coverage |
| input | stable | Full prop, ARIA, SSR, and explicit id invariant coverage |
| modal | stable | Full prop, slot, ARIA, and SSR coverage |
| nav | stable | Full prop, slot, ARIA, and SSR coverage |
| pricing-card | stable | Full prop, slot, ARIA, and SSR coverage |
| rating | stable | Full prop, slot, ARIA, and SSR coverage |
| section | stable | Full prop, slot, ARIA, and SSR coverage |
| sidebar | stable | Full prop, slot, ARIA, and SSR coverage; owns toggle interaction |
| spinner | stable | Full prop, ARIA, and SSR coverage |
| stack | stable | Full prop, slot, ARIA, and SSR coverage |
| tag | stable | Full prop, slot, ARIA, and SSR coverage |
| testimonial | stable | Full prop, slot, ARIA, and SSR coverage |
| toast | stable | Full prop, slot, ARIA, and SSR coverage |
| tooltip | stable | Full prop, slot, ARIA, and SSR coverage |
stable — the component family is fully wired to upstream recipes, covered by
SSR and unit tests, and declared in astro-adapter.contract.json. Breaking
changes require a semver major bump.
provisional — a family that is partially implemented or pending full test coverage. Not yet safe to depend on across minor releases.
not yet supported — families present in the upstream Spectre UI surface that this adapter has not yet bound.
The machine-readable classification lives in astro-adapter.contract.json under
componentFamilies.
| Package | Owns |
|---|---|
@phcdevworks/spectre-tokens |
Design values, semantic token meaning, and token contracts |
@phcdevworks/spectre-ui |
CSS, utilities, Tailwind helpers, and type-safe class recipes |
@phcdevworks/spectre-components |
Framework-agnostic Lit web component behavior |
@phcdevworks/spectre-ui-astro |
Astro-native adapter delivery and framework ergonomics |
Tokens define meaning. UI defines the styling contract. Components define framework-agnostic custom element behavior. This package defines Astro delivery and consumes the upstream UI contract directly.
git clone https://github.com/phcdevworks/spectre-ui-astro.git
cd spectre-ui-astro
npm install| Command | Purpose |
|---|---|
npm run check |
Full pre-merge check: lint → build → typecheck → test |
npm run ci:verify |
Underlying verification sequence |
npm run build |
Build the distributable package |
npm run typecheck |
Type-check without emitting |
npm test |
Run the Vitest test suite |
npm run lint |
Run ESLint |
npm run dev |
Watch mode for development |
Run npm run check before opening any pull request. It is the single gate used
by CI.
This project requires Node.js ^22.13.0 || >=24.0.0.
src/components/— Astro component implementationssrc/recipes/— re-exported recipe bindings from@phcdevworks/spectre-uisrc/index.ts— package exportstests/— unit, SSR, and contract testsexamples/— demo Astro app for manual validation (not a contract authority; seeexamples/README.md)scripts/— packaging and contract validation scripts
npm run build fails with a missing recipe or type The upstream
@phcdevworks/spectre-ui peer dependency must be installed. Run npm install
from the repo root. If a recipe or type is missing from upstream, do not add it
locally — open an issue in @phcdevworks/spectre-ui first.
Tests fail after pulling upstream changes Run npm install to sync
installed peer versions, then re-run npm test. If exports.test.ts fails, the
public contract has drifted — check src/index.ts and package.json exports
against the test expectations.
SpInput renders without accessible label associations SpInput requires
an explicit id prop whenever label, helperText, or errorMessage is
passed. Without it, the component throws at render time to prevent broken
accessibility wiring.
Example app fails to build Run npm install from within examples/ (not
npm ci) because the example depends on the parent package through a local
file:.. link. Do not commit an example package-lock.json as a CI contract.
Run the full validation gate before any pull request:
npm run checkThis runs: lint → build → typecheck → tests. All steps must pass.
Claude Code (claude-sonnet-4-6) is the primary development agent for this
repository. Codex handles releases and production stabilization. Jules handles
small automated fixes and micro-updates. GitHub Copilot provides development
support.
Claude Code, Codex, and Copilot do not create git commits by default. Jules may
commit only bounded automated maintenance when the JULES.md scope and
validation gates pass. Release decisions, tags, and publishing remain with
Bradley Potts.
Protected from automated change: SSR rendering invariants, the thin-adapter rule (no local CSS, no token redefinition, no recipe reimplementation), and the public export surface. See AGENTS.md for full agent governance and boundary rules.
PHCDevworks maintains this package as part of the Spectre suite.
When contributing:
- keep Astro components aligned with the upstream
@phcdevworks/spectre-uicontract - do not redefine tokens, CSS behavior, or recipe logic in this package
- keep the adapter SSR-friendly, type-safe, and framework-appropriate
- run
npm run checkbefore opening a pull request — it runs lint, build, typecheck, and tests in one step
See CONTRIBUTING.md for the full workflow.
MIT © PHCDevworks. See LICENSE.