The UI components layer provides navigation aids, empty-state feedback, and content-relationship widgets that sit between primitives and full section compositions. This page documents the five UI components introduced in work package C6. All components are CORE-OWNED, consume semantic tokens (--st-*), and render no markup when their content conditions are not met.
Who this is for
- Implementors placing breadcrumbs, pagination, or related-content blocks in page templates
- Developers extending or overriding UI component behavior
Breadcrumbs
File: src/core/components/ui/Breadcrumbs.astro
Explicit per-route breadcrumb navigation with CSS-only separators and aria-current="page" on the last item.
Props
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
items | Array<{ label: string; href?: string }> | yes | — | Ordered segments. Last item has no href (current page). |
class | string | no | — | Additional CSS classes |
Usage
---
import Breadcrumbs from "@core/components/ui/Breadcrumbs.astro";
const breadcrumbItems = [
{ label: "Home", href: "/" },
{ label: "Articles", href: "/articles/" },
{ label: "My Article Title" },
];
---
<Breadcrumbs items={breadcrumbItems} />
Behavior
- Renders
<nav aria-label="Breadcrumb">wrapping an<ol>. - Items with
hrefrender as<a>. The last item renders as<span aria-current="page">. - Visual separators (
/) via CSS::beforepseudo-elements — not in the DOM. - Renders nothing when
itemsis empty.
Accessibility
aria-label="Breadcrumb"on the<nav>element.aria-current="page"on the last item.- Focus ring on all links via
--st-color-focus-ring.
EmptyState
File: src/core/components/ui/EmptyState.astro
Standardized empty-state pattern with optional icon and “Go Back” CTA.
Props
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
message | string | yes | — | Empty-state text |
icon | string | no | — | Icon name resolved by the Icon primitive |
ctaLabel | string | no | "Go Back" | CTA button text |
ctaHref | string | no | "/" | Fallback URL when JS is unavailable |
showCta | boolean | no | true | Suppress CTA when false |
class | string | no | — | Additional CSS classes |
Usage
---
import EmptyState from "@core/components/ui/EmptyState.astro";
---
<EmptyState message="No articles found." icon="messages-question" />
<!-- Without CTA -->
<EmptyState message="Nothing here yet." showCta={false} />
Behavior
- Centered layout with optional icon (rendered via the
Iconprimitive), message, and CTA. - CTA renders as a
Button(secondary, sm) withdata-backattribute. - An inline
<script>attaches a click listener that callshistory.back(). Without JS, the button navigates toctaHref. - Renders nothing when
messageis falsy.
Accessibility
- Icon container has no semantic role (decorative).
- CTA is a visible, focusable button with clear label text.
Pagination
File: src/core/components/ui/Pagination.astro
Generalized page-based pagination with clean page-1 URLs.
Props
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
baseHref | string | yes | — | Base path for page URLs (e.g., "/articles") |
currentPage | number | yes | — | Active page number |
totalPages | number | yes | — | Total page count |
label | string | no | "Pagination" | aria-label for the nav element |
class | string | no | — | Additional CSS classes |
Usage
---
import Pagination from "@core/components/ui/Pagination.astro";
---
<Pagination
baseHref="/articles"
currentPage={1}
totalPages={5}
label="Article pages"
/>
Behavior
- Renders
<nav aria-label={label}>with Previous/Next links and numbered page links. aria-current="page"on the active page number.- URL construction: page 1 →
${baseHref}/, pages 2+ →${baseHref}/page/${n}/. Trailing slashes onbaseHrefare normalized. - Previous link hidden on page 1; Next link hidden on last page.
- Renders nothing when
totalPages <= 1. - Page links are centered via
justify-content: center.
Accessibility
aria-current="page"on the active page link.- Focus ring on all links via
--st-color-focus-ring.
PrevNextNav
File: src/core/components/ui/PrevNextNav.astro
Previous/next navigation for ordered collection entries.
Props
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
prev | { label: string; href: string } | no | — | Previous item |
next | { label: string; href: string } | no | — | Next item |
class | string | no | — | Additional CSS classes |
Usage
---
import PrevNextNav from "@core/components/ui/PrevNextNav.astro";
---
<PrevNextNav
prev={{ label: "Getting Started", href: "/theme-docs/getting-started/" }}
next={{ label: "Advanced Usage", href: "/theme-docs/advanced-usage/" }}
/>
Behavior
- Renders
<nav aria-label="Adjacent pages">with a two-column grid layout. - Previous link aligned left with
←indicator; next link aligned right with→indicator. - Directional indicators are
aria-hidden="true". - Renders nothing when both
prevandnextare undefined.
Accessibility
aria-label="Adjacent pages"on the<nav>element.- Focus ring on all links via
--st-color-focus-ring.
RelatedContent
File: src/core/components/ui/RelatedContent.astro
Related content section with heading and link list.
Props
| Prop | Type | Required | Default | Notes |
|---|---|---|---|---|
items | Array<{ label: string; href: string }> | no | — | Resolved related items |
heading | string | no | "Related Content" | Section heading text |
class | string | no | — | Additional CSS classes |
Usage
---
import RelatedContent from "@core/components/ui/RelatedContent.astro";
---
<RelatedContent
items={[
{ label: "Design Tokens", href: "/theme-docs/design-tokens/" },
{ label: "Layouts", href: "/theme-docs/layouts/" },
]}
heading="Related Docs"
/>
Behavior
- Renders
<section aria-labelledby="related-content-heading">with aHeading(level={2}) and a<ul>list. - Renders no markup at all when
itemsis empty or undefined — no section, no heading, nothing in the DOM.
Accessibility
aria-labelledbyconnects the section to its heading.- Focus ring on all links via
--st-color-focus-ring.
Token Usage
All five components consume semantic tokens (--st-*) for colors, borders, and spacing. No raw color values are used. Key token families:
- Links:
--st-color-link,--st-color-link-hover - Text:
--st-color-text-default,--st-color-text-muted - Surfaces:
--st-color-surface-soft,--st-color-surface-strong - Borders:
--st-color-border-default - Focus:
--st-color-focus-ringwith--pt-focus-ring-widthand--pt-focus-ring-offset - Spacing:
--pt-space-*primitives
Modal and Overlay Behavior
Modal and overlay components (dialogs, drawers, lightboxes, popovers) are not yet implemented as standalone components but the following behavioral requirements apply to all future implementations and any custom overlays built with this theme.
Focus Management
- Focus trapping (REQ-00050): When a modal opens, keyboard focus must be constrained within it. Tab and Shift+Tab must not reach elements behind the overlay. Use native
<dialog>withshowModal()which provides this natively, or vanilla JS focus trapping. - Focus restoration (REQ-00053): When a modal or transient overlay closes, focus must return to the element that triggered it. Store a reference to the trigger element before opening and call
.focus()on dismissal.
Scroll Locking
- Scroll lock (REQ-00054): While a modal is active, background page scroll must be suppressed. Apply
overflow: hiddento<body>(or the scroll container) on open and restore it on close. Account for scrollbar width to prevent layout shift.
Implementation Pattern
---
// Example: minimal accessible modal pattern using native <dialog>
---
<!-- Trigger -->
<button id="open-modal" data-action="open-modal">Open</button>
<!-- Modal -->
<dialog id="my-modal" aria-labelledby="modal-title">
<h2 id="modal-title">Modal Title</h2>
<button data-action="close-modal">Close</button>
</dialog>
Native <dialog> with showModal() provides focus trapping and scroll lock automatically. For non-dialog overlays, vanilla JS focus trapping handles keyboard containment.