npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@phcdevworks/spectre-ui-astro

v4.12.0

Published

@phcdevworks/spectre-ui-astro is the Astro integration for the Spectre system. It brings Spectre's design contracts and interface components into Astro applications through a native, server-rendering-friendly API.

Readme

@phcdevworks/spectre-ui-astro

@phcdevworks/spectre-ui-astro is the Astro integration for the Spectre system. It brings Spectre's design contracts and interface components into Astro applications through a native, server-rendering-friendly API.

Maintained by PHCDevworks. It depends on @phcdevworks/spectre-ui for its CSS and recipe contracts, so Astro applications consume Spectre's design system through typed components instead of hand-rolling markup or styling against the recipes directly.

Repository Snapshot

| Field | Value | | ---------------------- | ------------------------------- | | Project team | project-design | | Repository role | Spectre L3b Astro adapter | | Package/artifact | @phcdevworks/spectre-ui-astro | | Current version/status | 4.12.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

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

What Astro Developers Get

  • Forty-seven 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

What This Package Owns

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

What This Package Does Not Own

When To Use This Package

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-ui recipe contract without reimplementing it
  • you want to compose with Spectre's shared recipe helpers from TypeScript in an Astro project

When Not To Use This Package

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 or CSS utilities — that belongs in @phcdevworks/spectre-ui
  • you need a framework-agnostic styling contract — consume @phcdevworks/spectre-ui directly

Installation

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.

Spectre no longer ships a Tailwind CSS integration. Use the precompiled @phcdevworks/spectre-ui CSS entry points and recipe helpers shown below.

If your project works with Spectre design tokens directly:

npm install @phcdevworks/spectre-tokens

CSS Setup

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

Quick Start

---
import BaseLayout from '../layouts/BaseLayout.astro'
import {
  SpAccordion,
  SpAccordionItem,
  SpAlert,
  SpAvatar,
  SpBadge,
  SpButton,
  SpBreadcrumb,
  SpCard,
  SpCardBleed,
  SpCarousel,
  SpCarouselIndicator,
  SpCarouselSlide,
  SpCheckbox,
  SpChoiceCard,
  SpContainer,
  SpDatepicker,
  SpDay,
  SpDisplay,
  SpDropdown,
  SpDropdownDivider,
  SpDropdownHeader,
  SpDropdownItem,
  SpDropdownMenu,
  SpExternalAuthButton,
  SpFieldset,
  SpFileInput,
  SpFooter,
  SpFooterChip,
  SpFooterDivider,
  SpFooterHeading,
  SpFooterLink,
  SpFooterLinks,
  SpFooterText,
  SpGrid,
  SpHeading,
  SpIconBox,
  SpInput,
  SpInputGroup,
  SpInputGroupAddon,
  SpLabel,
  SpLead,
  SpListGroup,
  SpListGroupItem,
  SpLogoCloud,
  SpLogoCloudItem,
  SpModal,
  SpNav,
  SpNavItem,
  SpNavLinks,
  SpOffcanvas,
  SpPagination,
  SpPopover,
  SpPricingCard,
  SpProgress,
  SpProse,
  SpQuote,
  SpRadio,
  SpRange,
  SpRating,
  SpSection,
  SpSectionHead,
  SpSelect,
  SpSidebar,
  SpSidebarGroup,
  SpSidebarHeader,
  SpSidebarLink,
  SpSidebarToggle,
  SpSkeleton,
  SpSpinner,
  SpStack,
  SpStat,
  SpStepper,
  SpSwitch,
  SpTableRow,
  SpTabPanel,
  SpTable,
  SpTabs,
  SpTag,
  SpTestimonial,
  SpText,
  SpTextarea,
  SpToast,
  SpTooltip
} 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="[email protected]"
    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>

Components

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.


SpButton

| Prop | Type | Default | Description | | ------------ | -------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ | | variant | ButtonVariant | — | Visual style: "primary" "secondary" "ghost" "inverse" "accentOutline" and the other upstream variants | | 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 | | compact | boolean | — | Shrinks the visible box below the min touch target; an invisible ::after preserves the accessible hit area | | 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>

SpCard

| 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 \| "sm" \| "md" \| "lg" | — | Applies inner padding | | accent | CardAccentEdge | — | Decorative rail edge: "top" "right" "bottom" "left" | | accentColor | CardAccentColor | "brand" | Rail color, used when accent is set | | 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>

<!-- Accent rail -->
<SpCard variant="outline" accent="left" accentColor="success">
  <h2>Highlighted card</h2>
</SpCard>

The default slot renders any child content. Every component with an accent rail (card, badge, testimonial, pricing card, nav, footer, modal, toast, tooltip, dropdown menu, list group) also accepts accentColor="accent".

SpCard does not have a dedicated bleed slot. For a child (media, a flush internal surface) that should run through the card's padding on one or more edges, style it directly with the re-exported getCardBleedClasses helper:

---
import { SpCard, getCardBleedClasses } from '@phcdevworks/spectre-ui-astro'

const bleedClass = getCardBleedClasses({ edges: 'top', padded: 'md' })
---

<SpCard padded="md">
  <img class={bleedClass} src="/media/hero.jpg" alt="" />
  <p>Card body content, still padded normally.</p>
</SpCard>

SpContainer

| Prop | Type | Default | Description | | ------------ | ------------------------------------------------------ | ------- | ---------------------------------------------------------------------- | | maxWidth | ContainerMaxWidth | — | "prose" bounds content to a readable line length; "wide" widens it | | padding | ContainerPadding | — | Inline padding step: "sm" "md" "lg"; omission keeps the default | | 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.


SpStack

| Prop | Type | Default | Description | | ------------ | --------------------------------------------- | ------------ | ------------------------------------------------- | | direction | StackDirection | "vertical" | "vertical" | "horizontal" | | basis | StackBasis | — | "sidebar" gives the stack a fixed sidebar width | | align | StackAlign | "center" | "center" | "stretch" cross-axis alignment | | gap | StackGap | "md" | "sm" | "md" | "lg" spacing between items | | 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>

<SpStack direction="horizontal" align="stretch">
  <SpSidebar>...</SpSidebar>
  <main>Stretches to match the sidebar's height.</main>
</SpStack>

<SpStack gap="lg">
  <p>Item one</p>
  <p>Item two</p>
</SpStack>

The default slot renders any child content.


SpSection

| Prop | Type | Default | Description | | ------------ | ------------------------------------------------------ | ----------- | ---------------------------------------------------------------------- | | spacing | SectionSpacing | — | Block padding step: "sm" "md" "lg"; omission keeps the default | | gap | SectionGap | — | Stacks direct children with the section gap step: "sm" "md" "lg" | | hero | "sm" \| "md" \| "lg" | — | Asymmetric hero padding (more above than below); replaces spacing | | attached | boolean | — | Drops the top padding of a band that belongs to the section above | | 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. spacing and gap also accept the "xl" "2xl" "3xl" "4xl" steps from @phcdevworks/spectre-ui 5.4.0, as do the SpStack and SpGrid gaps and the SpContainer padding.


SpSectionHead

The heading block at the top of a section. The eyebrow and title slots fill the title part; the default slot (the lead) and the actions slot fill the lead part. Each part renders only when one of its slots is populated. Typography comes from SpText, SpHeading, and SpLead.

| Prop | Type | Default | Description | | ------------ | -------------------------------- | ----------- | --------------------------------------------------------------------------- | | layout | "stacked" \| "split" | "stacked" | split puts the title left and the lead right from the lg breakpoint | | align | "start" \| "center" | "start" | Stacked layout only; center centres the head at prose width | | as | "div" \| "header" \| "hgroup" | "div" | Rendered element | | aria-label | string | — | Accessible label; on a div it also sets role="group" | | class | string | — | Additional CSS classes |

<SpSectionHead layout="split">
  <SpText slot="eyebrow" as="span" transform="uppercase" size="sm">Services</SpText>
  <SpHeading slot="title" level="h2">What we build</SpHeading>
  <SpLead>Design systems, web platforms, and the tooling between them.</SpLead>
  <SpButton slot="actions" variant="accentOutline">See the work</SpButton>
</SpSectionHead>

SpText

| Prop | Type | Default | Description | | ----------- | --------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ | | as | "h1" \| "h2" \| "h3" \| "h4" \| "h5" \| "h6" \| "p" \| "span" | "p" | Rendered element | | size | TextSize | — | Upstream type scale | | variant | TextVariant | — | Upstream color role, including "onInverse"/"onInverseMuted" for on-dark surfaces | | family | TextFamily | — | Upstream font family | | transform | TextTransform | — | "none" | "uppercase" | "lowercase" | "capitalize" | | weight | TextWeight | — | 400–900; overrides the weight the size preset carries | | leading | TextLeading | — | "none" "tight" "snug" "normal" "relaxed" "loose"; overrides line height | | id | string | — | Element id | | class | string | — | Additional CSS classes |

<SpText as="h2" size="2xl" variant="brand">Section heading</SpText>
<SpText variant="muted">Supporting copy.</SpText>
<SpText as="span" transform="uppercase" size="sm">Eyebrow label</SpText>

The default slot renders any child content. SpText maps directly to getTextClasses; use its as prop to render any heading level or p/span when text-recipe styling is appropriate. Use SpHeading for the dedicated heading recipe. variant also takes the "accent", "onSurfaceAccent", "onInverseBrand", and "onInverseAccent" roles.


SpSidebar

| 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 | | hideToggle | boolean | false | Suppresses the built-in toggle button, e.g. when placing SpSidebarToggle inside SpNav instead | | 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.

To integrate the hamburger toggle into a top SpNav bar instead of leaving it next to the off-canvas sidebar, pass hideToggle to SpSidebar and render SpSidebarToggle inside SpNav, pointing its for prop at the sidebar's id:

<SpNav bordered sticky fullWidth>
  <SpSidebarToggle for="docs-sidebar" />
</SpNav>

<SpSidebar id="docs-sidebar" hideToggle bordered aria-label="Primary">
  ...
</SpSidebar>

Both buttons (the built-in one and SpSidebarToggle) share the same .sp-sidebar-toggle styling, so they always match the current theme via --sp-component-sidebar-toggle-* tokens.

Build sidebar nav groups in the default slot using SpSidebarLink for each link and the re-exported getSidebarHeaderClasses helper for section headers, since section headers are consumer-driven.

---
import { SpSidebar, SpSidebarLink, getSidebarHeaderClasses } from '@phcdevworks/spectre-ui-astro'

const headerClass = getSidebarHeaderClasses()
---

<SpSidebar bordered aria-label="Primary">
  <span class={headerClass}>Guides</span>
  <SpSidebarLink href="/" active>Home</SpSidebarLink>
  <SpSidebarLink href="/about">About</SpSidebarLink>
  <SpSidebarLink href="/about/team" level="child">Team</SpSidebarLink>
</SpSidebar>

SpSidebarLink

| Prop | Type | Default | Description | | ------------ | ------------------ | ---------- | --------------------------------------------------------- | | href | string | — | Link target; suppressed when disabled | | active | boolean | false | Marks the link current (aria-current="page") | | disabled | boolean | false | Suppresses href, sets aria-disabled and tabindex=-1 | | hovered | boolean | false | Forces hover-state classes | | focused | boolean | false | Forces focus-state classes | | level | SidebarLinkLevel | "parent" | "parent" | "child" — indents nested links | | id | string | — | Element id | | title | string | — | Native title attribute | | aria-label | string | — | Accessible label | | class | string | — | Additional CSS classes |

SpSidebarLink renders a single <a>, backed by getSidebarLinkClasses. The level option indents nested links under a section header.


SpSidebarToggle

| Prop | Type | Default | Description | | ------- | -------- | ------------------ | -------------------------------------------- | | for | string | — | Required id of the SpSidebar to control | | label | string | "Toggle sidebar" | Accessible label for the toggle button | | class | string | — | Additional CSS classes for the toggle button |

SpSidebarToggle renders a button that can live outside SpSidebar, such as inside SpNav. Its for prop must match the target sidebar's id. The sidebar wiring binds again after Astro client-side navigation so newly swapped sidebar shells remain interactive.


SpGrid

| Prop | Type | Default | Description | | ------------------ | ------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | columns | GridColumns | 1 | 1 | 2 | 3 | 4 | 6 | 12 | | gap | GridGap | "md" | "sm" | "md" | "lg" | | columnGap | GridGap | — | Column-axis gap override | | rowGap | GridGap | — | Row-axis gap override | | span | GridSpan \| GridSpanOptions | — | Column span for a grid item: a single value or { base?, md?, lg? } per breakpoint | | offset | GridOffset \| GridOffsetOptions | — | Column offset for a grid item: 0-11 or { base?, md?, lg? } | | colStart | GridColStart \| GridColStartOptions | — | Explicit starting column line for a grid item: 1-12 or { base?, md?, lg? } | | rowSpan | GridSpan \| GridSpanOptions | — | Row span for a grid item, same shape as span | | rowOffset | GridOffset \| GridOffsetOptions | — | Row offset for a grid item, same shape as offset | | order | GridOrder \| GridOrderOptions | — | Visual order for a grid item: "first" | "last" | "none" | 1-12, or per breakpoint | | align | GridAlign | — | "start" | "center" | "end" | "baseline" | "stretch" cross-axis cell alignment | | leadingTracks | GridLeadingTracksOptions | — | { weight } — proportional leading-track sizing without hand-rolled grid-template-columns | | fixedTracks | GridFixedTracksOptions | — | { count } — fixed track count for custom track layouts | | explicitTemplate | GridExplicitTemplateOptions | — | { template, weight? } — named asymmetric column template ("edge-fluid-edge" | "label-fluid-fluid"); mutually exclusive with columns/leadingTracks/fixedTracks | | 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>

<SpGrid columns={12} gap="lg">
  <SpCard span={{ base: 'full', md: 6, lg: 4 }}>One</SpCard>
  <SpCard span={{ base: 'full', md: 6, lg: 8 }}>Two</SpCard>
</SpGrid>

<SpGrid leadingTracks={{ weight: 2 }} fixedTracks={{ count: 2 }} gap="lg">
  <SpCard offset={2} rowSpan={2}>Leads two fixed trailing tracks</SpCard>
  <SpCard order="last">Second track</SpCard>
</SpGrid>

<SpGrid explicitTemplate={{ template: "edge-fluid-edge" }} gap="md">
  <SpIconBox>Logo</SpIconBox>
  <SpNav>Nav links</SpNav>
  <SpButton>CTA</SpButton>
</SpGrid>

<SpGrid columns={3} align="center">
  <SpCard>One</SpCard>
  <SpCard>Two, taller content</SpCard>
  <SpCard>Three</SpCard>
</SpGrid>

The default slot renders any child content. span, offset, rowSpan, rowOffset, and order are set on individual grid items (not the SpGrid wrapper) and each accepts either a single value or a per-breakpoint { base?, md?, lg? } object; they map directly to the matching getGridClasses option of the same name. leadingTracks, fixedTracks, and explicitTemplate are set on the SpGrid wrapper itself and map to getGridClasses's options of the same name. explicitTemplate selects a named, finite asymmetric column shape ("edge-fluid-edge" for a logo/nav/CTA row, "label-fluid-fluid" for a fixed leading label column plus two differently-weighted fluid columns) for layouts that columns/span/ leadingTracks/fixedTracks cannot express, and is mutually exclusive with those column-sizing options.


SpInput

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="[email protected]"
  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…" />

SpLabel

SpLabel renders a standalone <label>. Association with a form control is the consumer's responsibility, the same as plain HTML — pass htmlFor matching the control's id.

| Prop | Type | Default | Description | | ------------------ | --------- | ------- | -------------------------------- | | disabled | boolean | — | Disabled styling | | required | boolean | — | Required-field styling | | htmlFor | string | — | Renders the for attribute | | id | string | — | Element ID | | aria-label | string | — | Accessible label | | aria-describedby | string | — | Associates a description element | | class | string | — | Additional CSS classes |

<SpLabel htmlFor="email">Email</SpLabel>
<SpInput id="email" type="email" />

<SpLabel htmlFor="agree" required>I agree to the terms</SpLabel>

SpFieldset

SpFieldset renders a <fieldset> with an optional <legend>. The legend only renders when legend is provided and non-empty.

| Prop | Type | Default | Description | | ------------------ | --------- | ------- | ------------------------------------------ | | disabled | boolean | — | Disables every control inside the fieldset | | legend | string | — | Renders a <legend> with the recipe class | | name | string | — | Form field group name | | form | string | — | Associates with a <form> by ID | | id | string | — | Element ID | | aria-label | string | — | Accessible label | | aria-describedby | string | — | Associates a description element | | class | string | — | Additional CSS classes |

<SpFieldset legend="Contact details">
  <SpLabel htmlFor="name">Name</SpLabel>
  <SpInput id="name" name="name" />
</SpFieldset>

<SpFieldset disabled legend="Read-only section">
  <SpInput id="readonly-field" value="Locked" disabled />
</SpFieldset>

SpCheckbox

SpCheckbox renders a native <input type="checkbox">.

| Prop | Type | Default | Description | | ------------------ | --------- | ------- | -------------------------------- | | checked | boolean | — | Checked state | | disabled | boolean | — | Disables the checkbox | | id | string | — | Element ID | | name | string | — | Form field name | | value | string | — | Form field value | | required | boolean | — | Marks the field required | | form | string | — | Associates with a <form> by ID | | aria-label | string | — | Accessible label | | aria-describedby | string | — | Associates a description element | | class | string | — | Additional CSS classes |

<SpLabel htmlFor="agree">I agree to the terms</SpLabel>
<SpCheckbox id="agree" name="terms" />

<SpCheckbox id="newsletter" name="subscribe" checked />
<SpCheckbox id="locked" disabled />

SpRadio

SpRadio renders a native <input type="radio">.

| Prop | Type | Default | Description | | ------------------ | --------- | ------- | --------------------------------------- | | checked | boolean | — | Checked state | | disabled | boolean | — | Disables the radio | | id | string | — | Element ID | | name | string | — | Form field name — shared across a group | | value | string | — | Form field value | | required | boolean | — | Marks the field required | | form | string | — | Associates with a <form> by ID | | aria-label | string | — | Accessible label | | aria-describedby | string | — | Associates a description element | | class | string | — | Additional CSS classes |

<SpLabel htmlFor="plan-pro">Pro</SpLabel>
<SpRadio id="plan-pro" name="plan" value="pro" checked />

<SpLabel htmlFor="plan-free">Free</SpLabel>
<SpRadio id="plan-free" name="plan" value="free" />

SpSelect

SpSelect renders a native <select>. Pass <option> elements as children.

| Prop | Type | Default | Description | | ------------------ | --------- | ------- | -------------------------------- | | disabled | boolean | — | Disables the select | | focused | boolean | — | Applies focus styling | | id | string | — | Element ID | | name | string | — | Form field name | | value | string | — | Selected value | | required | boolean | — | Marks the field required | | multiple | boolean | — | Allows multiple selection | | form | string | — | Associates with a <form> by ID | | aria-label | string | — | Accessible label | | aria-describedby | string | — | Associates a description element | | class | string | — | Additional CSS classes |

<SpLabel htmlFor="country">Country</SpLabel>
<SpSelect id="country" name="country">
  <option value="us">United States</option>
  <option value="ca">Canada</option>
</SpSelect>

SpTextarea

SpTextarea renders a native <textarea>.

| Prop | Type | Default | Description | | ------------------ | --------- | ------- | -------------------------------- | | disabled | boolean | — | Disables the textarea | | focused | boolean | — | Applies focus styling | | id | string | — | Element ID | | name | string | — | Form field name | | value | string | — | Default text content | | placeholder | string | — | Placeholder text | | rows | number | — | Visible row count | | required | boolean | — | Marks the field required | | readonly | boolean | — | Prevents editing | | form | string | — | Associates with a <form> by ID | | aria-label | string | — | Accessible label | | aria-describedby | string | — | Associates a description element | | class | string | — | Additional CSS classes |

<SpLabel htmlFor="bio">Bio</SpLabel>
<SpTextarea id="bio" name="bio" rows={4} placeholder="Tell us about yourself" />

SpAlert

| Prop | Type | Default | Description | | ------------------ | -------------------------------------------- | ----------- | ------------------------------------------------------------------------------- | | variant | AlertVariant | "info" | Visual style: "info" "success" "warning" "danger" "neutral" "brand" | | size | AlertSize | "md" | Size: "sm" "md" "lg" | | as | "div" \| "section" \| "aside" \| "article" | "div" | Rendered element | | dismissed | boolean | — | Applies dismissed state styling | | dismissible | boolean | — | Reserves and renders a dismiss control | | dismissLabel | string | "Dismiss" | Accessible label for the dismiss control | | 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="brand" dismissible><span slot="icon">i</span>New feature available.</SpAlert>
<SpAlert variant="info" as="aside" aria-label="Info notice">Read the docs.</SpAlert>

SpAvatar

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

SpBadge

| Prop | Type | Default | Description | | ----------------- | ---------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------- | | variant | BadgeVariant | — | Visual style: "primary" "success" "warning" "danger" "info" "inverse" | | 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 | | dot | boolean | — | Text-free notification dot (e.g. overlaid on an avatar) | | accentRail | BadgeAccentRailEdge | — | Decorative rail edge: "top" "right" "bottom" "left" | | accentRailColor | BadgeAccentRailColor | "brand" | Rail color, used when accentRail is set | | 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>
<SpBadge accentRail="left" accentRailColor="success">Verified</SpBadge>

accentRail/accentRailColor are named distinctly from variant because variant="accent" already names badge's single-tone brand-accent fill; the rail is an unrelated, additive edge decoration.


SpIconBox

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


SpPricingCard

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 | | accent | PricingCardAccentEdge | — | Decorative rail edge: "top" "right" "bottom" "left" | | accentColor | PricingCardAccentColor | "brand" | Rail color, used when accent is set | | 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>

SpRating

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. Rendered once per star; filled/empty styling comes from the star wrapper class. 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