Primitive components are the lowest-level UI building blocks. They live in src/core/components/primitives/ and are CORE-OWNED. Higher-level sections and page layouts compose these primitives.
Who this is for
- Implementors building pages or section components
- Developers extending the component library
- AI agents composing UI from primitives
Icon
File: src/core/components/primitives/Icon.astro
Renders an inline SVG icon by name. Icons are resolved at build time using a layered lookup: src/site/assets/icons/ first, then src/core/assets/icons/, across utility/ and social/ subdirectories. Site icons shadow core icons of the same name.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
name | string | required | Icon filename without extension (e.g., "envelope") |
size | string | "1.25em" | CSS size applied to width and height |
label | string | — | Accessible label. When set, icon is announced to screen readers. When omitted, icon is decorative (aria-hidden). |
class | string | — | Additional CSS classes |
Behavior
- Icons are resolved in order:
site/utility/→site/social/→core/utility/→core/social/. The first match wins, so site icons shadow core icons of the same name. - The component throws a build error if the icon name cannot be resolved — no silent failures.
- SVG color inherits from the parent’s
colorproperty viacurrentColor. Set color on the parent element.
Heading
File: src/core/components/primitives/Heading.astro
Semantic heading with visual size decoupling. The level prop controls the HTML element (h1–h6), while size overrides the visual styling independently.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
level | 1|2|3|4|5|6 | required | Semantic heading level (HTML element) |
size | "xs"|"sm"|"md"|"lg"|"xl"|"2xl"|"3xl"|"4xl"|"5xl" | mapped from level | Visual size. Decoupled from semantic level using primitive typography tokens. |
toc | boolean | true | When false, renders data-toc="exclude" on the heading element. |
class | string | — | Additional CSS classes |
Behavior
- Semantic–visual split: Use
levelfor document outline correctness andsizefor visual hierarchy. Example: a sidebarh2that should look like anh4→<Heading level={2} size="lg">. - Size classes apply
font-sizeandline-heightfrom primitive tokens. - TOC exclusion: When
toc={false}, the heading rendersdata-toc="exclude"as an HTML attribute. The docs route client-side fallback script filters out headings with this attribute. Use this for component-rendered headings that should not appear in the table of contents (e.g., section headings in showcase pages, widget titles).
<!-- This heading appears in the TOC (default) -->
<Heading level={2} id="visible">Visible heading</Heading>
<!-- This heading is excluded from the TOC -->
<Heading level={2} id="hidden" toc={false}>Hidden from TOC</Heading>
Note: TOC exclusion applies only to component-rendered headings in .astro templates. Markdown-authored headings (## Foo) cannot carry the data-toc="exclude" attribute. See the Table of Contents documentation for details.
PageHeading
File: src/core/components/primitives/PageHeading.astro
Lightweight page heading band for route subheaders. Use instead of Hero when no actions, images, or complex layout are needed. Composes Heading with an optional subheading paragraph.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
heading | string | required | Page title text |
subheading | string | — | Optional subtitle rendered as a paragraph below the heading |
headingLevel | 1|2|3|4|5|6 | 1 | Semantic heading level (HTML element) |
headingSize | "xs"|"sm"|"md"|"lg"|"xl"|"2xl"|"3xl"|"4xl"|"5xl" | mapped from level | Visual size override, passed through to Heading |
class | string | — | Additional CSS classes on the wrapper |
Behavior
- Renders a
Headingand an optional<p>inside a flex column withgap-sm. - Subheading uses
--pt-text-lgfont size, relaxed line height, and 90% opacity — matching the same visual treatment asHero’s simple render path. - Subheading has
max-inline-size: 60chto maintain comfortable reading width. - No
<style>block, no JavaScript, no additional component imports beyondHeading.
When to use
Use PageHeading for route subheader bands where the page needs only a title and optional description. Use Hero when the heading band requires call-to-action buttons, images, or complex grid layouts.
<LayoutSection
as="div"
slot="subheader"
contentWidth="wide"
verticalPadding="lg"
>
<PageHeading
heading="Documentation"
subheading="Browse docs and showcase pages by topic, type, and audience."
/>
</LayoutSection>
Button
File: src/core/components/primitives/Button.astro
Polymorphic button that renders as <a> when href is provided, or <button> otherwise.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | "primary"|"outline"|"secondary"|"secondary-outline"|"tertiary"|"tertiary-outline"|"neutral"|"inverted"|"on-primary"|"on-secondary"|"on-tertiary" | "primary" | Visual style variant |
size | "sm"|"md"|"lg" | "md" | Size tier |
href | string | — | When set, renders <a> instead of <button> |
disabled | boolean | false | Disables interaction. On links, removes href and adds aria-disabled. |
fullWidth | boolean | false | Stretches to 100% container width |
tokenOverrides | ButtonTokenOverrides | — | One-off semantic-token overrides for rare treatments. Values must be var(--st-color-...) refs |
class | string | — | Additional CSS classes |
Variant roles
primary— default high-emphasis action on standard surfacesoutline— hollow/ghost primary action on standard surfaces (primary action token family)secondary— secondary-brand action on standard surfacessecondary-outline— hollow/ghost secondary action on standard surfacestertiary— tertiary-brand action on standard surfacestertiary-outline— hollow/ghost tertiary action on standard surfacesneutral— non-brand low-emphasis action on standard surfacesinverted— theme-opposite high-contrast action oncontrastandinvertedsurfaceson-primary— contextual action forprimarybrand surfaceson-secondary— contextual action forsecondarybrand surfaceson-tertiary— contextual action fortertiarybrand surfaces
Behavior
- Polymorphic rendering: With
href, emits<a>(notypeattribute). Withouthref, emits<button type="button">(overridable viatypeprop). - Disabled links: When
disabledistrueon a link variant,hrefis removed andaria-disabled="true"is set. The cursor changes tonot-allowedand opacity is reduced. - Border radius: Controlled via Tailwind radius utility classes on
class(e.g.,rounded-none,rounded-sm,rounded-lg,rounded-full). The component stylesheet provides a default radius; consumers override with utility classes as needed. - Token override escape hatch:
tokenOverridesaccepts an object with optionalbg,fg,border,bgHover, andfgHoverfields. Each field must be a semantic token reference (var(--st-color-...)). Use this for rare one-off treatments not covered by the named variants — for example, a destructive action button usingvar(--st-color-status-error). - Style passthrough: The
styleprop is preserved for non-Button-owned layout concerns (e.g.,align-self). Button-owned CSS variables (--_btn-*,--btn-*) are stripped from incomingstyleto keeptokenOverridesas the only authorized color-override channel. - Color tokens use the
--st-color-action-*family. Focus ring uses--st-color-focus-ring. - Each Button variant maps its focus ring to the matching
--st-color-action-*-focustoken so keyboard focus follows the active action palette.
Link
File: src/core/components/primitives/Link.astro
Internal link wrapper with token-driven colors and accessible focus states.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
href | string | required | Destination URL |
class | string | — | Additional CSS classes |
Behavior
- Applies
--st-color-link,--st-color-link-hover, and--st-color-link-visitedtokens. - Focus ring styled with
--st-color-focus-ring. - All additional props are spread onto the
<a>element.
ExternalLink (REQ-00166, REQ-00167)
File: src/core/components/primitives/ExternalLink.astro
External link with security attributes, screen reader announcement, and optional icon.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
href | string | required | External URL |
showIcon | boolean | true | Show external-link icon after link text |
class | string | — | Additional CSS classes |
Behavior
- Always applies
rel="noopener noreferrer"(REQ-00166, REQ-00167). Does not forcetarget; passtarget="_blank"explicitly when needed. - Appends
<span class="sr-only">(external link)</span>for all external links. Whentarget="_blank"is set, the text becomes(external link, opens in new tab). - When
showIconistrue, appends an inline SVG external-link icon (0.65em,aria-hidden).
Badge
File: src/core/components/primitives/Badge.astro
Inline metadata badge with color variants aligned to the semantic token system.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | "default"|"success"|"warning"|"error"|"info"|"brand-primary"|"brand-secondary"|"brand-tertiary" | "default" | Color variant |
size | "sm"|"md" | "md" | Size tier |
outline | boolean | false | Transparent background with border and text in the variant color. Use with high-contrast variants (error, info, brand-*). |
class | string | — | Additional classes |
Behavior
- Status variants use
--st-color-status-*backgrounds with--st-color-on-status-*text. - Brand variants use
--st-color-brand-*and--st-color-on-brand-*tokens. - Default variant uses
--st-color-surface-strongbackground. - All variants pass WCAG AA contrast in both light and dark mode.
FormatDate
File: src/core/components/primitives/FormatDate.astro
Date formatting component that renders a <time> element with machine-readable datetime attribute.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
date | Date | string | required | Date to format. Strings are parsed via new Date(). |
format | "short"|"long"|"iso" | "short" | Preset format |
options | Intl.DateTimeFormatOptions | — | Custom Intl options. Overrides format preset. |
class | string | — | Additional CSS classes |
Format presets
| Preset | Example output | Description |
|---|---|---|
short | Jan 15, 2026 | Month abbreviation, day, year |
long | January 15, 2026 | Full month name, day, year |
iso | 2026-01-15 | ISO 8601 date string |
Locale is sourced from siteConfig.locale. The datetime attribute always contains the ISO string regardless of display format.
LayoutSection
File: src/core/components/primitives/LayoutSection.astro
Full-width section wrapper with background variants, vertical-padding tiers, and a built-in inner content frame.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
as | "section"|"div" | "section" | HTML element to render. Use "div" for non-landmark wrappers |
label | string | — | Accessible name for the section. Applied as aria-label only when as="section". Ignored on <div>. Prefer over direct aria-label; if both are present, label wins. |
background | "default"|"soft"|"strong"|"contrast"|"inverted"|"primary"|"secondary"|"tertiary" | "default" | Surface color variant via shared .surface--bg-* classes |
contentWidth | "content"|"content-wide"|"wide"|"full"|"inherit" | "inherit" | Inner frame max-width. inherit defers to page-level section width context. |
gutter | "default"|"none" | "default" | Inner frame horizontal padding. default uses --pt-layout-container-gutter |
verticalPadding | "inherit"|"none"|"sm"|"md"|"lg"|"xl"|"2xl"|"3xl" | "inherit" | Vertical padding tier. inherit applies no explicit section padding class. |
class | string | — | Additional classes on the outer element |
Behavior
- The outer element (default
<section>, configurable viaas) is full-width with the selected background color. LayoutSectionalways renders an innerContainerframe for consistent content alignment.- Default frame width is inherited from
PageGridLayoutcontentWidthvia CSS variable; if unavailable, it falls back towide. LayoutSectionis recommended for semantic page bands, but not required for basic layout framing:PageGridLayoutalso applies a default frame to direct non-LayoutSectionchildren in the main slot.- Background variants map to
--st-color-surface-*tokens. Theinvertedvariant also sets text color to--st-color-text-on-inverted. - Vertical-padding tiers use primitive space tokens (
--pt-space-*). gutter="none"removes inner horizontal padding while preserving max-width behavior.
Choosing as and label
Each LayoutSection usage falls into one of three categories:
| Category | When to use | Example |
|---|---|---|
as="div" | Parent slot already provides a landmark (subheader inside <header>, main-header/main-footer inside a labeled <section>); or content is not a thematic grouping (pagination, individual demos, error messages). | <LayoutSection as="div" slot="subheader"> |
Keep as="section" (default, no label) | Content is a thematic grouping with a visible heading — either via a sibling <Heading> component or markdown headings. The heading provides the accessible name. | <LayoutSection> with a sibling <Heading> before a section component |
as="section" + label | Genuine thematic grouping without a visible heading (listing grids, search interfaces, filter UIs). | <LayoutSection label="Article listing"> |
The label prop is conditionally applied: it sets aria-label only when as="section" (the default). When as="div", the label is ignored because a generic <div> has no implicit ARIA role to label. If both label and a direct aria-label are passed, label takes precedence and a dev-time warning is emitted.
Dropdown
File: src/core/components/primitives/Dropdown.astro
Accessible dropdown using the disclosure pattern, powered by vanilla TypeScript. Tab navigates items; arrow keys provide supplemental navigation.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | "Menu" | Accessible label for trigger |
position | "bottom-start"|"bottom-end" | "bottom-start" | Panel alignment |
class | string | — | Additional classes |
Slots
| Slot | Purpose |
|---|---|
trigger | Custom trigger element. If omitted, a default button with label renders. |
| default | Menu items. Use <a> or <button> elements. |
Behavior
- Uses the disclosure pattern (not ARIA menu). Tab/Shift+Tab navigates between items; arrow keys and Home/End provide supplemental navigation.
- Closes when focus leaves the panel (
focusin.window), on click outside, or on Escape (returns focus to trigger). - Uses vanilla JS for focus-leave detection.
- Panel appears/hides with CSS transitions. Hidden by default to prevent flash of unstyled content.
- No-JS: Without JavaScript, the dropdown does not render (hidden by default).
- The trigger and panel consume the nav dropdown semantic surface (
--st-color-nav-dropdown-bg/--st-color-nav-dropdown-border) rather than generic surface tokens.
ListBox
Relocated: ListBox has moved from
src/core/components/primitives/tosrc/core/components/forms/as part of the forms system (D-C8-11). See the Forms & Submission documentation for usage.
Vanilla JS implementation
Dropdown and ListBox (now in src/core/components/forms/) use vanilla TypeScript for state management, keyboard navigation, focus handling, and ARIA attributes. No framework dependencies are required.