The 4 files this component needs and nothing else, each fenced with the path it belongs at, followed by what the library's own verification covered and what it did not. No instructions attached — for placing it yourself, or handing it to something that has its own.
Read it first
Install kit: "Service photo cards"
Library page: https://astro.baysixmedia.com/components/service-photo-cards/
Files: 4 required, 1 supporting
Write each block below to the path named in its header, relative to the project root. A block marked (required) is one the component does not work without; (supporting) is documentation. Nothing here needs downloading except a block that says so in as many words.
===== FILE (required): src/components/ServicePhotoCards.astro =====
---
/**
* ServicePhotoCards
*
* A services section: an orange eyebrow and a heading on the left with an
* orange pill link on the right, over three tall photo cards. Each card is a
* single link: a photo with a white panel pinned to its bottom holding an
* icon tile that pokes out above it, the service name and a pink arrow tile.
*
* Desktop hover (and keyboard focus): the photo zooms to 1.1, the icon tile
* fills orange, the arrow slides through, and a short description opens in
* the panel, which grows upward and lifts the icon tile with it. On touch
* screens and below 1200px the description is always shown, as the
* reference's phone layout does.
*
* On scroll the header and the cards rise 40px into place, cards 0.1s apart.
*
* Reads --tg-* design tokens from any ancestor, with fallbacks.
*/
export type ServicePhotoIcon = "ship" | "truck" | "train" | "plane" | "warehouse";
export interface ServicePhotoCard {
title: string;
description?: string;
href: string;
icon?: ServicePhotoIcon;
/** `src: ""` renders the labelled placeholder. */
image?: { src?: string; alt?: string };
}
export interface ServicePhotoCardsProps {
eyebrow?: string;
heading?: string;
headingLevel?: "h2" | "h3";
/** The orange pill beside the heading. `null` drops it. */
cta?: { label: string; href: string } | null;
cards?: ServicePhotoCard[];
background?: string;
fontStylesheet?: string;
class?: string;
id?: string;
}
interface Props extends ServicePhotoCardsProps {}
const {
eyebrow = "Our services",
heading = "",
headingLevel = "h2",
cta = null,
cards = [],
background = "#ffffff",
fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap",
class: className,
id,
} = Astro.props;
const Heading = headingLevel;
/* Each card is one link: it needs a title and an href, or it is dropped rather
than drawn half-empty. A pill needs a label and an href. No cards, no section:
the eyebrow and heading alone never render. */
const list = cards.filter((card) => card?.title && card?.href);
const pill = cta?.label && cta?.href ? cta : null;
const uid = id ?? `spc-${Math.random().toString(36).slice(2, 8)}`;
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
/* Measured card: 388 × 430 at desktop; the files are 776 × 860. */
const IMAGE_SPEC = "~0.9:1 · 776 × 860 px";
---
{list.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section class:list={["spc", className]} id={id} style={`--spc-bg: ${background};`} data-spc-root>
<div class="spc-inner">
<div class="spc-head" data-spc-rise>
<div class="spc-titles">
{
eyebrow && (
<p class="spc-eyebrow">
{eyebrow}
<span class="spc-rule" aria-hidden="true" />
</p>
)
}
{heading && <Heading class="spc-heading">{heading}</Heading>}
</div>
{
pill && (
<a class="spc-btn" href={safeHref(pill.href)}>
{pill.label}
<svg class="spc-btn-arrow" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
<path d="M2 10 10 2M3.5 2H10v6.5" />
</svg>
</a>
)
}
</div>
<ul class="spc-grid" role="list">
{
list.map((card, index) => {
const titleId = `${uid}-t${index}`;
const descId = `${uid}-d${index}`;
const img = card.image ?? {};
return (
<li class="spc-item" data-spc-rise style={`--i: ${index};`}>
<a
class="spc-card"
href={safeHref(card.href)}
aria-labelledby={titleId}
aria-describedby={card.description ? descId : undefined}
>
<span class="spc-media" aria-hidden={img.src && img.alt ? undefined : "true"}>
{img.src ? (
<img src={img.src} alt={img.alt ?? ""} width="776" height="860" loading="lazy" decoding="async" />
) : (
<span class="spc-ph">
<span class="spc-ph-label">Image</span>
<span class="spc-ph-hint">{card.title}, tall photo</span>
<span class="spc-ph-spec">{IMAGE_SPEC}</span>
</span>
)}
</span>
<span class="spc-body">
<span class="spc-icon" aria-hidden="true">
<span class="spc-icon-hover" />
<svg viewBox="0 0 40 40" width="40" height="40" focusable="false">
{card.icon === "truck" ? (
<g>
<rect x="10" y="5" width="20" height="26" rx="3" />
<path d="M10 13h20M10 22h20M14 9h12" />
<circle cx="15" cy="26.5" r="1.5" />
<circle cx="25" cy="26.5" r="1.5" />
<path d="M13 31v4M27 31v4M7 14v6M33 14v6" />
</g>
) : card.icon === "train" ? (
<g>
<path d="M12 5h16a3 3 0 0 1 3 3v19a3 3 0 0 1-3 3H12a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Z" />
<path d="M9 17h22M15 9h10" />
<rect x="13" y="11" width="14" height="6" />
<circle cx="14" cy="24" r="1.5" />
<circle cx="26" cy="24" r="1.5" />
<path d="m13 30-4 6M27 30l4 6M11 33h18M4 36h32" />
</g>
) : card.icon === "plane" ? (
<g>
<path d="M4 22h12l8-12h4l-4 12h8l3-4h3l-1.5 6 1.5 6h-3l-3-4h-8l4 12h-4l-8-12H4" transform="translate(0 -4)" />
</g>
) : card.icon === "warehouse" ? (
<g>
<path d="M4 15 20 6l16 9v19H4Z" />
<path d="M10 34V20h20v14M10 25h20M10 30h20" />
</g>
) : (
<g>
<path d="M4 27h32l-4 7H9Z" />
<path d="M9 27v-8h22v8" />
<path d="M13 19v-6h10v6M18 13V7h4M26 23h1M14 23h1M20 23h1" />
<path d="M3 37c2.5 0 2.5-1.5 5-1.5s2.5 1.5 5 1.5 2.5-1.5 5-1.5 2.5 1.5 5 1.5 2.5-1.5 5-1.5 2.5 1.5 5 1.5 2.5-1.5 4-1.5" />
</g>
)}
</svg>
</span>
<span class="spc-text">
<span class="spc-title-row">
<span class="spc-title" id={titleId}>{card.title}</span>
<span class="spc-arrow" aria-hidden="true">
<svg class="spc-arrow-a" viewBox="0 0 18 16" width="18" height="16" focusable="false">
<path d="M1.5 8h15M10 1.5 16.5 8 10 14.5" />
</svg>
<svg class="spc-arrow-b" viewBox="0 0 18 16" width="18" height="16" focusable="false">
<path d="M1.5 8h15M10 1.5 16.5 8 10 14.5" />
</svg>
</span>
</span>
{card.description && (
<span class="spc-desc-wrap">
<span class="spc-desc" id={descId}>{card.description}</span>
</span>
)}
</span>
</span>
</a>
</li>
);
})
}
</ul>
</div>
</section>
)}
<style>
.spc {
--spc-navy: var(--tg-navy, #020e28);
--spc-orange: var(--tg-orange, #fd5523);
--spc-muted: var(--tg-muted, #61697d);
--spc-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
--spc-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
--spc-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
box-sizing: border-box;
padding: 80px 20px;
background: var(--spc-bg, #fff);
color: var(--spc-navy);
}
.spc *,
.spc *::before,
.spc *::after {
box-sizing: border-box;
}
.spc-inner {
display: flex;
flex-direction: column;
gap: 30px;
max-width: 1224px;
margin: 0 auto;
}
/* ---- header ---- */
.spc-head {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.spc-titles {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
max-width: 645px;
}
.spc .spc-eyebrow {
display: inline-flex;
align-items: center;
gap: 12px;
margin: 0;
font: 600 16px/1 var(--spc-display);
text-transform: uppercase;
color: var(--spc-orange);
}
.spc-rule {
display: none;
width: 40px;
height: 1px;
background: var(--spc-orange);
}
.spc-heading {
margin: 0;
font: 600 32px/1.2 var(--spc-display);
letter-spacing: normal;
text-transform: capitalize;
color: var(--spc-navy);
}
.spc .spc-btn {
display: inline-flex;
flex: none;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 18px 24px;
border-radius: 100px;
background: var(--spc-orange);
font: 600 14px/12.46px var(--spc-body);
text-transform: capitalize;
color: #fff;
text-decoration: none;
transition: background-color 0.3s var(--spc-ease);
}
.spc-btn-arrow {
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.3s var(--spc-ease);
}
.spc .spc-btn:hover {
background: var(--spc-navy);
}
.spc .spc-btn:hover .spc-btn-arrow {
transform: rotate(45deg);
}
.spc .spc-btn:focus-visible {
outline: 2px solid var(--spc-navy);
outline-offset: 3px;
}
/* ---- cards ---- */
.spc .spc-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 24px;
margin: 0;
padding: 0;
list-style: none;
}
.spc-card {
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
justify-content: flex-end;
aspect-ratio: 350 / 335;
padding: 12px;
border-radius: 20px;
overflow: hidden;
color: var(--spc-navy);
text-decoration: none;
background: #dfe5e7;
}
.spc-card:focus-visible {
outline: 3px solid var(--spc-orange);
outline-offset: 4px;
}
.spc-media {
position: absolute;
inset: 0;
z-index: -1;
display: block;
}
.spc-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s var(--spc-ease);
}
.spc-ph {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 100%;
padding: 16px 16px 45%;
text-align: center;
background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
color: rgb(2 14 40 / 0.62);
}
.spc-ph-label {
font: 600 12px/1 var(--spc-body);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.spc-ph-hint {
max-width: 28ch;
font: 500 14px/1.4 var(--spc-body);
}
.spc-ph-spec {
font: 400 12px/1.2 var(--spc-body);
}
.spc-body {
position: relative;
display: block;
margin-top: 60px;
padding: 40px 12px 16px;
border-radius: 10px;
background: #fff;
}
.spc-icon {
position: absolute;
top: -39px;
left: 12px;
display: grid;
place-items: center;
width: 60px;
height: 60px;
border-radius: 5px;
background: #fff;
box-shadow:
rgb(12 12 13 / 0.05) 0 1px 4px 0,
rgb(12 12 13 / 0.1) 0 1px 4px 0;
color: var(--spc-navy);
transition: color 0.4s var(--spc-ease);
}
.spc-icon-hover {
position: absolute;
inset: 0;
border-radius: 5px;
background: var(--spc-orange);
opacity: 0;
transition: opacity 0.4s var(--spc-ease);
}
.spc-icon svg {
position: relative;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.spc-text {
display: block;
}
.spc-title-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 16px;
}
.spc-title {
max-width: 180px;
font: 700 20px/24px var(--spc-display);
color: var(--spc-navy);
}
.spc-arrow {
position: relative;
flex: none;
display: block;
width: 40px;
height: 40px;
border-radius: 10px;
background: #ffe0e0;
overflow: hidden;
}
.spc-arrow svg {
position: absolute;
top: 12px;
left: 11px;
fill: none;
stroke: var(--spc-orange);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.4s var(--spc-ease);
}
.spc-arrow-b {
transform: translateX(-30px);
}
.spc-desc-wrap {
display: grid;
grid-template-rows: 1fr;
margin-top: 12px;
transition:
grid-template-rows 0.4s var(--spc-ease),
margin-top 0.4s var(--spc-ease),
opacity 0.4s var(--spc-ease);
}
.spc-desc {
display: block;
min-height: 0;
overflow: hidden;
font: 400 15px/20px var(--spc-body);
color: var(--spc-muted);
}
/* ---- hover / focus ---- */
.spc-card:hover .spc-media img,
.spc-card:focus-visible .spc-media img {
transform: scale(1.1);
}
.spc-card:hover .spc-icon,
.spc-card:focus-visible .spc-icon {
color: #fff;
}
.spc-card:hover .spc-icon-hover,
.spc-card:focus-visible .spc-icon-hover {
opacity: 1;
}
.spc-card:hover .spc-arrow-a,
.spc-card:focus-visible .spc-arrow-a {
transform: translateX(30px);
}
.spc-card:hover .spc-arrow-b,
.spc-card:focus-visible .spc-arrow-b {
transform: translateX(0);
}
/* ---- appear ---- */
[data-spc-rise] {
transition:
opacity 0.8s var(--spc-ease) calc(var(--i, 0) * 0.1s),
transform 0.8s var(--spc-ease) calc(var(--i, 0) * 0.1s);
}
.spc[data-spc-ready]:not([data-in]) [data-spc-rise] {
opacity: 0;
transform: translateY(40px);
}
/* ---- tablet ---- */
@media (min-width: 810px) {
.spc {
padding: 100px 30px;
}
.spc-inner {
gap: 50px;
}
.spc-head {
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
}
.spc .spc-eyebrow {
font-size: 18px;
line-height: 12.6px;
}
.spc-rule {
display: block;
}
.spc-heading {
font-size: 40px;
}
.spc .spc-btn {
min-height: 56px;
padding: 22px 30px;
}
.spc .spc-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 30px;
}
.spc-card {
aspect-ratio: 388 / 430;
padding: 20px;
}
.spc-body {
padding: 40px 30px 20px;
}
.spc-icon {
left: 30px;
}
.spc-title {
max-width: 150px;
font-size: 22px;
line-height: 26px;
}
}
/* ---- desktop: three 388 × 430 cards ---- */
@media (min-width: 1200px) {
.spc {
padding: 120px 20px;
}
.spc-inner {
gap: 60px;
}
.spc-heading {
font-size: 48px;
line-height: 57.6px;
}
.spc .spc-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.spc-title {
max-width: 140px;
}
}
/* Desktop with a real pointer: the description stays folded until the
card is hovered or focused, and the panel grows upward to show it. */
@media (min-width: 1200px) and (hover: hover) and (pointer: fine) {
.spc-desc-wrap {
grid-template-rows: 0fr;
margin-top: 0;
opacity: 0;
}
.spc-card:hover .spc-desc-wrap,
.spc-card:focus-visible .spc-desc-wrap {
grid-template-rows: 1fr;
margin-top: 20px;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
[data-spc-rise],
.spc-media img,
.spc-arrow svg,
.spc-desc-wrap,
.spc-btn-arrow {
transition: none;
}
.spc-card:hover .spc-media img,
.spc-card:focus-visible .spc-media img,
.spc .spc-btn:hover .spc-btn-arrow {
transform: none;
}
.spc[data-spc-ready]:not([data-in]) [data-spc-rise] {
opacity: 1;
transform: none;
}
}
@media (forced-colors: active) {
.spc-body,
.spc .spc-btn {
border: 1px solid CanvasText;
}
}
</style>
<script>
/*
* Appear-on-scroll. One IntersectionObserver per instance, scoped to its
* root, torn down on astro:before-swap; sets `data-in` once. The hidden
* start pose applies only after the root is marked ready, so no-JS and
* reduced motion show everything at rest. Hovers are pure CSS.
*/
function initServicePhotoCards(root: HTMLElement) {
if (root.dataset.spcReady === "") return;
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduced || !("IntersectionObserver" in window)) {
root.dataset.in = "";
return;
}
const controller = new AbortController();
const { signal } = controller;
root.dataset.spcReady = "";
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
root.dataset.in = "";
observer.disconnect();
}
},
{ threshold: 0.15 },
);
observer.observe(root);
signal.addEventListener("abort", () => observer.disconnect());
document.addEventListener(
"astro:before-swap",
() => {
controller.abort();
delete root.dataset.spcReady;
},
{ once: true, signal },
);
}
const boot = () => document.querySelectorAll<HTMLElement>("[data-spc-root]").forEach(initServicePhotoCards);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
document.addEventListener("astro:page-load", boot);
</script>
===== END src/components/ServicePhotoCards.astro =====
===== FILE (supporting): src/components/ServicePhotoCards.md =====
# Service photo cards
A services section: an orange eyebrow and heading on the left, an orange
"View All Services" pill on the right, and three tall photo cards below.
Each card is one link: a photo with a white panel pinned to its bottom — an
icon tile poking out of the panel's top edge, the service name and a pink
arrow tile. On hover the photo zooms, the icon tile turns orange, the arrow
slides through and a short description opens in the panel.
Catalog page: `/components/service-photo-cards`
## Files required
**`ServicePhotoCards.astro`** — the whole component, icons inline, one small
script for the appear. No imports, no npm packages.
`ServicePhotoCards.md` — this file.
`public/service-photo-cards/ocean.webp`, `truck.webp`, `rail.webp` — the catalog
demo's card photos, passed as props (not defaults).
## Basic usage
```astro
---
import ServicePhotoCards from "../components/ServicePhotoCards.astro";
---
<ServicePhotoCards
heading="Freight & Logistics Services"
cta={{ label: "View All Services", href: "/services" }}
cards={[
{
title: "Ocean Freight",
description: "Full and part container loads…",
href: "/services/ocean-freight",
icon: "ship",
image: { src: "/img/ocean.webp" },
},
// …
]}
/>
```
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `"Our services"` | `""` drops it. |
| `heading` | `string?` | `""` | `""` drops it. |
| `headingLevel` | `"h2" \| "h3"?` | `"h2"` | Card titles are not headings; they name the links. |
| `cta` | `{ label, href } \| null?` | `null` | The pill. Needs both `label` and `href`; `null` drops it. |
| `cards` | `ServicePhotoCard[]?` | `[]` | Any count; three per row on desktop. A card without a `title` or `href` is dropped. With no cards the component renders nothing — not the eyebrow, heading, pill or font `<link>`. The catalog demo passes its three cards as props. |
| `background` | `string?` | `#ffffff` | Section surface. |
| `fontStylesheet` | `string?` | Outfit + DM Sans | `""` skips it. |
| `class`, `id` | `string?` | — | `id` also seeds the title/description ids. |
```ts
interface ServicePhotoCard {
title: string; // keep to two short words per line, it wraps at ~140px
description?: string;
href: string;
icon?: "ship" | "truck" | "train" | "plane" | "warehouse";
image?: { src?: string; alt?: string }; // src "" = labelled placeholder
}
```
## Behaviour
**Card hover and keyboard focus** (from the measured states, 0.4s):
- the photo scales to **1.1** (0.6s, clipped by the 20px card radius);
- the icon tile's orange "Hover Bg" layer fades **0 → 1** and the icon turns
white;
- the pink arrow tile's arrow slides out to the right as a second arrow
slides in from the left;
- the **description opens** (a `0fr → 1fr` grid-row transition plus fade,
20px gap), so the panel grows upward and lifts the icon tile with it —
the "icon wrap shifts" the reference shows.
The same state applies on `:focus-visible`, so keyboard users see the
description too. The card gets a 3px orange focus ring.
**Pill hover** (measured): bg `#fd5523` → `#020e28`, the ↗ arrow rotates 45°.
**Appear** (measured): the header and each card start at `opacity 0,
translateY(40px)` and rise over 0.8s, cards 0.1s apart, once the section is
15% in view. Hidden start only after the script marks the root ready.
**Reduced motion:** no rise, no zoom, no slides; the description still
opens (it's content, not decoration), without a transition.
## Layout
| Width | Layout |
| :--- | :--- |
| ≥ 1200 | Header row (heading left, pill bottom-right); 3 × 388×430 cards, 30px gap, in 1224px; panel inset 20px with 20/30px padding; description folded until hover on fine pointers. |
| 810–1199 | Two columns; description always shown. |
| < 810 | Header stacked, pill under the heading; one column of 350:335 cards, panel inset 12px, description always shown — as on the reference phone. |
Cards grow taller than their aspect ratio if the panel needs the room.
## Accessibility
- Each card is a single `<a>` named by its title (`aria-labelledby`) and
described by its description (`aria-describedby`), so a screen reader
hears "Ocean Freight, link" followed by the description, not the whole
card text run together.
- The description is only visually folded on desktop; it is always in the
accessibility tree.
- Photos are decorative by default; pass `alt` to give one meaning.
## Images and credits
| File | Size | Unsplash id |
| :--- | :--- | :--- |
| `public/service-photo-cards/ocean.webp` | 776×860 | jOqJbvo1P9g |
| `public/service-photo-cards/truck.webp` | 776×860 | S4FXynMI_bU |
| `public/service-photo-cards/rail.webp` | 776×860 | ziTbznvI-9g |
Unsplash License (free commercial use, no attribution required).
## Approximations
- **The −194px "offset" is centring, not a reveal.** In the dump the body
wrapper's `translateX(-194px)` is exactly half the 388px card — Framer's
`left: 50%; translate(-50%)`. The reveal is the description: the desktop
DOM has none, the phone layout shows it, and the text block's
`overflow: hidden` + 20px gap is where it opens. The hover-open is
inferred from that structure; its timing wasn't measured.
- The **"Hover Bg" overlay** is the icon tile's orange layer (the dump's
only `opacity: 0` nodes), not a wash over the photo.
- The **arrow swap** is inferred from the two stacked arrow images inside an
`overflow: hidden` tile; direction and timing are a judgement call. The
tile's pink background is left unchanged on hover (not measured).
- Icons are original line drawings. Tablet was not captured.
===== END src/components/ServicePhotoCards.md =====
===== FILE (required): public/service-photo-cards/ocean.webp =====
[binary asset — download https://astro.baysixmedia.com/service-photo-cards/ocean.webp and save it to public/service-photo-cards/ocean.webp]
===== FILE (required): public/service-photo-cards/truck.webp =====
[binary asset — download https://astro.baysixmedia.com/service-photo-cards/truck.webp and save it to public/service-photo-cards/truck.webp]
===== FILE (required): public/service-photo-cards/rail.webp =====
[binary asset — download https://astro.baysixmedia.com/service-photo-cards/rail.webp and save it to public/service-photo-cards/rail.webp]
--- VERIFIED IN THE COMPONENT LIBRARY ---
Enrolled for verification but never run.
No verification run has been recorded.
No checks have passed yet.
No usable source fingerprint, so these results cannot be tied to the files below.
--- NOT PROVEN ---
- ClientRouter: event-tested only. Handlers respond to dispatched lifecycle events; this has NOT been certified against a real <ClientRouter /> transition.
--- WHAT THIS DOES NOT CERTIFY ---
Verification was performed on the unmodified component, in an isolated fixture, in the
library. It says nothing about this project's CSS, routes, layout or data, and nothing
about the component once you adapt it. A verified component is a good starting point,
not a guarantee about the installation you are about to build.
--- TEST IN THIS PROJECT AFTER INSTALLING OR CUSTOMISING ---
- Horizontal overflow at 1440, 1280, 1024, 834, 390 and 320px.
- Keyboard operation end to end, including visible focus.
- The no-JavaScript render.
- prefers-reduced-motion: reduce.
- Two instances on one page, if this project renders more than one.
- If this project uses <ClientRouter />: teardown and reinitialisation across a real navigation.