Phase 5 - Code Examples
Code Examples
Section titled “Code Examples”These examples are the template’s shipped components, organised by atomic design (ADR-003):
src/components/{atoms,molecules,structural}/.
Button Component
Section titled “Button Component”---interface Props { variant?: "primary" | "secondary" | "ghost"; size?: "sm" | "md" | "lg"; class?: string; href?: string; disabled?: boolean; [key: string]: any;}
const { variant = "primary", size = "md", class: className, href, disabled = false, ...rest} = Astro.props as Props;
const baseStyles = "inline-flex items-center justify-center rounded-lg font-semibold no-underline transition-colors duration-200 motion-reduce:transition-none focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 forced-colors:border forced-colors:text-[ButtonText]";
function getVariantStyles(v: Props["variant"]) { switch (v) { case "secondary": // border-emphasis (gray-600 light, gray-300 dark) maintains ≥3:1 against // both the page bg and the button bg, satisfying WCAG AA non-text // contrast. return "bg-surface text-foreground border border-border-emphasis hover:bg-background active:bg-background focus:ring-primary-500 shadow-sm hover:shadow"; case "ghost": // Subtle default border for high-contrast support return "text-muted-foreground border border-border/40 hover:border-border hover:bg-surface hover:text-foreground active:bg-surface focus:ring-primary-500"; default: return "bg-primary-600 text-primary-foreground hover:bg-primary-700 active:bg-primary-800 focus:ring-primary-500 shadow-sm hover:shadow-md"; }}
function getSizeStyles(s: Props["size"]) { // Remove fixed heights to avoid clipping; rely on padding for accessible hit areas switch (s) { case "sm": return "text-xs px-3 py-2 min-h-[2rem]"; case "lg": return "text-base px-6 py-3 min-h-[2.75rem]"; default: return "text-sm px-4 py-2.5 min-h-[2.75rem]"; }}
// Render appropriate element without a dynamic uppercase variable to satisfy lint rulesconst classList = [baseStyles, getVariantStyles(variant), getSizeStyles(size), className];---{href ? ( <a class:list={classList} href={href} aria-disabled={disabled} tabindex={disabled ? -1 : undefined} {...rest} > <slot /> </a>) : ( <button class:list={classList} type="button" aria-disabled={disabled} disabled={disabled} {...rest} > <slot /> </button>)}Card Component
Section titled “Card Component”---interface Props { class?: string; animated?: boolean;}
const { class: className, animated = false } = Astro.props;---
<div class:list={[ "card overflow-hidden rounded-lg border border-border bg-surface shadow-sm", animated && "card--animated", className,]}> <slot /></div>
<style> /* * @starting-style enables entry animations when the card first renders — * particularly visible during View Transitions page navigation. * Previously required JavaScript (requestAnimationFrame or Intersection Observer). */ @media (prefers-reduced-motion: no-preference) { .card--animated { transition: opacity 0.3s ease-out, transform 0.3s ease-out; }
@starting-style { .card--animated { opacity: 0; transform: translateY(0.5rem); } } }</style>Section Component
Section titled “Section Component”---interface Props { class?: string; id?: string; fullHeight?: boolean; // Optional: allow disabling full viewport height when needed ariaLabel?: string; // Optional: ARIA label for screen readers (recommended for snap sections) ariaLabelledBy?: string; // Optional: reference to heading ID that labels this section}
const { class: className, id, fullHeight = false, ariaLabel, ariaLabelledBy,} = Astro.props as Props;---
<section id={id} class:list={[ "py-16 sm:py-24 lg:py-32", fullHeight && "min-h-screen flex flex-col justify-center snap-start", className, ]} aria-label={ariaLabel} aria-labelledby={ariaLabelledBy}> <slot /></section>Container Component
Section titled “Container Component”---interface Props { class?: string;}
const { class: className } = Astro.props;---
<div class:list={["mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8", className]}> <slot /></div>Grid Component
Section titled “Grid Component”---interface Props { class?: string;}
const { class: className } = Astro.props;---
<div class:list={["@container grid grid-cols-1 gap-8 @md:grid-cols-2 @lg:grid-cols-3", className]}> <slot /></div>Image Component Wrapper
Section titled “Image Component Wrapper”The template’s Image atom wraps astro:assets with responsive-srcset defaults, AVIF-first format
resolution, and an SVG pass-through. Interface excerpt — see src/components/atoms/Image.astro
for the full implementation:
---// src/components/atoms/Image.astro (excerpt)import { Image as AstroImage } from "astro:assets";import type { ImageMetadata } from "astro";import { resolveImageFormat } from "@/utils/resolveImageFormat";
interface Props { src: ImageMetadata | string | Promise<{ default: ImageMetadata }>; alt: string; class?: string; format?: "avif" | "webp" | "png" | "jpeg" | "jpg" | "svg" | "gif"; quality?: number | "low" | "mid" | "high" | "max"; width?: number; height?: number; sizes?: string; widths?: number[]; densities?: number[]; loading?: "lazy" | "eager"; decoding?: "async" | "sync" | "auto"; /** Optional shadow styles for performance modularity */ hasShadow?: boolean;}---Key behaviors:
- Defaults to AVIF output (via
resolveImageFormat) withloading="lazy"anddecoding="async" - Generates a widths-based srcset (
[320, 640, 1024]) unless fixed dimensions are passed, in which case it switches to a densities-based srcset ([1.5, 2]) - SVG sources pass through unprocessed — a vector stays a vector
- String
srcpaths fall back to a native<img>element
Badge Component
Section titled “Badge Component”---import type { HTMLAttributes } from "astro/types";
interface Props extends Omit<HTMLAttributes<"span">, "class"> { class?: string; variant?: "primary" | "secondary" | "neutral"; size?: "xs" | "sm" | "md";}
const { class: className, role, variant = "primary", size = "sm", ...rest } = Astro.props;
const variantClasses: Record<"primary" | "secondary" | "neutral", string> = { primary: "bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200", secondary: "bg-secondary-100 text-secondary-800 dark:bg-secondary-900 dark:text-secondary-200", neutral: "bg-surface text-muted-foreground border border-border",};
const sizeClasses: Record<"xs" | "sm" | "md", string> = { xs: "text-xs px-[0.5rem] py-0.5", sm: "text-sm px-2.5 py-0.5", md: "text-base px-[0.75rem] py-[0.25rem]",};---<span role={role} class:list={[ 'badge inline-flex items-center rounded-full font-medium', variantClasses[variant], sizeClasses[size], className, ]} {...rest}> <slot /></span>
<style> /* * prefers-contrast: more — strengthens badge visibility for users who * request higher contrast. */ @media (prefers-contrast: more) { .badge { border: 2px solid currentColor; font-weight: 700; } }</style>Link Component
Section titled “Link Component”The Link component has been moved into a dedicated tutorial to empower you to build it yourself and understand the template’s architecture. This approach avoids providing an overly opinionated component out-of-the-box.
This guide will walk you through creating a flexible Link component that can handle internal and external links, apply consistent styling, and manage accessibility attributes.
Interactive Components
Section titled “Interactive Components”Dialog (Modal) Component
Section titled “Dialog (Modal) Component”The template ships its modal as src/components/molecules/Dialog.astro, built on the native
<dialog> element — the browser provides focus trapping, backdrop rendering, Escape-to-close,
and stacking context, so no modal library is needed. Interface excerpt:
---// src/components/molecules/Dialog.astro (excerpt)interface Props { id: string; title: string; size?: "sm" | "md" | "lg"; class?: string; /** * Heading level for the dialog title only. Defaults to "h2" for top-level * dialogs. Pass a deeper level when the dialog is nested inside a section * that already uses h2/h3 — heading hierarchy should not skip levels. */ headingLevel?: "h2" | "h3" | "h4" | "h5" | "h6";}
const { id, title, size = "md", class: className, headingLevel = "h2" } = Astro.props;
const sizeClasses = { sm: "max-w-sm", md: "max-w-lg", lg: "max-w-2xl",} as const;---Usage — open it with the native showModal() API:
<Dialog id="my-dialog" title="Dialog Title" size="md"> <p>Dialog content here</p></Dialog>
<button onclick="document.getElementById('my-dialog').showModal()"> Open Dialog</button>Tabs Component
Section titled “Tabs Component”The template’s Tabs live at src/components/molecules/Tabs.astro. Panel switching is CSS-only
(hidden radio inputs plus :has(...:checked) selectors); a ~30-line inline script adds
arrow-key navigation and ARIA wiring per the WAI-ARIA tabs pattern. Interface excerpt:
---// src/components/molecules/Tabs.astro (excerpt)interface TabItem { id: string; label: string;}
interface Props { tabs: TabItem[]; defaultTab?: string; class?: string;}
const { tabs, defaultTab, class: className } = Astro.props;---Usage — panels are slotted content matched by data-tab-panel:
<Tabs tabs={[ { id: "overview", label: "Overview" }, { id: "usage", label: "Usage" },]} defaultTab="overview"> <div data-tab-panel="overview" role="tabpanel">First panel</div> <div data-tab-panel="usage" role="tabpanel">Second panel</div></Tabs>Showcase Page (Living Style Guide)
Section titled “Showcase Page (Living Style Guide)”The starter documents components on a real page instead of a separate tool: src/pages/showcase.astro
renders each component’s variants directly (ADR-049). To add a component to the style guide, import it
and render its variants:
---// src/pages/showcase.astro (excerpt)import Button from "@/components/atoms/Button.astro";import Card from "@/components/molecules/Card.astro";---
<section> <h2>Buttons</h2> <Button variant="primary">Primary</Button> <Button variant="secondary">Secondary</Button> <Button variant="ghost">Ghost</Button></section>Because the showcase is a normal Astro page, it ships through the same performance and accessibility gates as the rest of the site — no extra tooling to configure.