The 3 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: "Checklist feature grid"
Library page: https://astro.baysixmedia.com/components/checklist-feature-grid/
Files: 3 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/ChecklistFeatureGrid.astro =====
---
/**
* ChecklistFeatureGrid
*
* A "why choose us" section on a pale band: outlined eyebrow and a big
* uppercase heading over a 2×2 grid of white cards (round orange icon, title,
* hairline, two check-marked points), with two stacked photos in a column on
* the right.
*
* Heading words rise in and the cards and photos lift in, staggered, the first
* time they scroll into view.
*
* Heading syntax: wrap the orange words in asterisks.
*
* Reads the --tw-* tokens from any ancestor, with fallbacks.
*/
export type ChecklistFeatureIcon = "shapes" | "team" | "chart" | "support" | "shield" | "clock";
export interface ChecklistFeature {
title: string;
points: string[];
icon?: ChecklistFeatureIcon;
}
export interface ChecklistFeatureGridProps {
eyebrow?: string;
heading?: string;
headingLevel?: "h2" | "h3";
features?: ChecklistFeature[];
/** Top and bottom photos. `src: ""` renders a placeholder; `null` drops the column. */
images?: [{ src?: string; alt?: string }, { src?: string; alt?: string }] | null;
background?: string;
fontStylesheet?: string;
class?: string;
id?: string;
}
interface Props extends ChecklistFeatureGridProps {}
const {
eyebrow = "Why Choose Us",
heading = "",
headingLevel = "h2",
features: featuresProp = [],
images: imagesProp = null,
background = "#f0f8fa",
fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800;900&family=Noto+Sans:wght@400;500;600;700&family=Inter:wght@500;600;700&display=swap",
class: className,
id,
} = Astro.props;
/*
* Only the photos passed, never topped up from a demo pair. None passed drops
* the column; an entry with `src: ""` still draws its placeholder.
*/
const passedImages = (imagesProp ?? [])
.slice(0, 2)
.filter(Boolean)
.map((p) => ({ src: p.src ?? "", alt: p.alt ?? "" }));
const images = passedImages.length > 0 ? passedImages : null;
/* A card needs its title; one without is dropped. No cards, nothing renders. */
const features = featuresProp
.filter((f) => f && f.title)
.map((f) => ({ ...f, points: (f.points ?? []).filter(Boolean) }));
const words = heading
.split("*")
.flatMap((seg, i) => seg.split(/\s+/).filter(Boolean).map((w) => ({ w, accent: i % 2 === 1 })));
const Heading = headingLevel;
const Title = headingLevel === "h2" ? "h3" : "h4";
const ICONS: Record<ChecklistFeatureIcon, string> = {
shapes: "M7 4l4 7H3Z M17 14a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z M4 14h7v7H4Z M14 17h7v4h-7Z",
team: "M12 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z M6 20v-2a6 6 0 0 1 12 0v2 M4 8l8-4 8 4 M12 14v2",
chart: "M4 20V4 M4 20h16 M7 15l4-4 3 3 5-6 M16 8h3v3",
support: "M9 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z M3 19v-1a5 5 0 0 1 10 0v1 M15 8h6 M15 12h6 M17 16h4",
shield: "M12 3 19 6v5c0 5-3 8.5-7 10-4-1.5-7-5-7-10V6Z M9 12l2 2 4-4",
clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z M12 7v5l3 2",
};
---
{features.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{features.length > 0 && (
<section class:list={["cfg", className]} id={id} style={`--cfg-band:${background}`} data-cfg-root>
<div class:list={["cfg-inner", { "cfg-inner--solo": !images }]}>
<div class="cfg-main">
{eyebrow && <p class="cfg-eyebrow">{eyebrow}</p>}
{words.length > 0 && (
<Heading class="cfg-heading" data-cfg-reveal="words">
{
words.map((t, i) => (
<>
{i > 0 && " "}
<span class:list={["cfg-w", { "cfg-accent": t.accent }]} style={`--i:${i}`}>
{t.w}
</span>
</>
))
}
</Heading>
)}
<ul class="cfg-grid">
{
features.map((f, i) => (
<li class="cfg-card" data-cfg-reveal="up" style={`--d:${i * 0.1}s`}>
<div class="cfg-card-head">
<span class="cfg-ic" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d={ICONS[f.icon ?? "shield"]} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
<Title class="cfg-title">{f.title}</Title>
</div>
{f.points.length > 0 && (
<ul class="cfg-points">
{f.points.map((p) => (
<li>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m5 12.5 4.5 4.5L19 6" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{p}
</li>
))}
</ul>
)}
</li>
))
}
</ul>
</div>
{
images && (
<div class="cfg-photos">
{images.map((img, i) => (
<div class:list={["cfg-photo", `cfg-photo--${i}`]} data-cfg-reveal="up" style={`--d:${0.1 + i * 0.12}s`}>
{img.src ? (
<img src={img.src} alt={img.alt} width="834" height={i === 0 ? 514 : 874} loading="lazy" decoding="async" />
) : (
<div class="cfg-ph" role="img" aria-label="Image placeholder">
<span>Image · {i === 0 ? "834 × 514" : "834 × 874"}</span>
</div>
)}
</div>
))}
</div>
)
}
</div>
</section>
)}
<style>
.cfg {
--cfg-navy: var(--tw-navy, #0b4654);
--cfg-accent: var(--tw-accent, #ff6d45);
--cfg-muted: var(--tw-muted, #616161);
--cfg-display: var(--tw-display, Outfit, ui-sans-serif, system-ui, sans-serif);
--cfg-body: var(--tw-body, "Noto Sans", ui-sans-serif, system-ui, sans-serif);
--cfg-card-font: var(--tw-card, Inter, ui-sans-serif, system-ui, sans-serif);
--cfg-ease: cubic-bezier(0.44, 0, 0.56, 1);
background: var(--cfg-band);
color: var(--cfg-navy);
font-family: var(--cfg-body);
}
.cfg *,
.cfg *::before,
.cfg *::after {
box-sizing: border-box;
}
/* Step 39.2: --tw-grid-max / --tw-grid-gutter put the content on a site grid; the fallbacks are the original values. */
.cfg-inner {
display: grid;
gap: 40px;
max-width: var(--tw-grid-max, 1440px);
margin: 0 auto;
padding: 64px 16px;
}
.cfg-main {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
}
.cfg-eyebrow {
margin: 0 0 22px;
padding: 4px 14px;
border: 1px solid var(--cfg-accent);
border-radius: 999px;
color: var(--cfg-accent);
font: 500 15px/24px var(--cfg-body);
letter-spacing: -0.3px;
text-transform: uppercase;
}
.cfg-heading {
max-width: 800px;
margin: 0;
font: 700 28px/1.2 var(--cfg-display);
letter-spacing: -0.56px;
text-transform: uppercase;
}
.cfg-w {
display: inline-block;
}
.cfg-accent {
color: var(--cfg-accent);
}
.cfg-grid {
display: grid;
gap: 20px;
width: 100%;
margin: 40px 0 0;
padding: 0;
list-style: none;
}
.cfg-card {
padding: 26px;
border-radius: 20px;
background: #fff;
}
.cfg-card-head {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 24px;
border-bottom: 1px solid rgb(11 70 84 / 0.12);
}
.cfg-ic {
display: grid;
flex: none;
place-items: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: var(--cfg-accent);
color: #fff;
}
.cfg-ic svg {
width: 26px;
height: 26px;
}
.cfg-title {
margin: 0;
font: 600 22px/1.2 var(--cfg-card-font);
letter-spacing: -0.88px;
}
.cfg-points {
display: grid;
gap: 9px;
margin: 22px 0 0;
padding: 0;
list-style: none;
}
.cfg-points li {
display: flex;
align-items: center;
gap: 12px;
color: var(--cfg-muted);
font: 400 16px/25.6px var(--cfg-body);
letter-spacing: -0.32px;
}
.cfg-points svg {
flex: none;
width: 20px;
height: 20px;
color: var(--cfg-accent);
}
.cfg-photos {
display: grid;
gap: 10px;
}
.cfg-photo {
overflow: hidden;
border-radius: 20px;
}
.cfg-photo--0 {
aspect-ratio: 417 / 257;
}
.cfg-photo--1 {
aspect-ratio: 417 / 437;
}
.cfg-photo img,
.cfg-ph {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.cfg-ph {
display: grid;
place-items: center;
background: repeating-linear-gradient(135deg, #dbe9ed 0 12px, #e6f0f3 12px 24px);
font: 500 13px/1.4 var(--cfg-body);
}
[data-cfg-reveal="up"] {
transition:
opacity 0.8s var(--cfg-ease) var(--d, 0s),
transform 0.8s var(--cfg-ease) var(--d, 0s);
}
.cfg[data-cfg-ready] [data-cfg-reveal="up"]:not([data-in]) {
opacity: 0;
transform: translateY(80px);
}
.cfg-w {
transition:
opacity 0.6s var(--cfg-ease) calc(var(--i) * 0.06s),
transform 0.6s var(--cfg-ease) calc(var(--i) * 0.06s);
}
.cfg[data-cfg-ready] [data-cfg-reveal="words"]:not([data-in]) .cfg-w {
opacity: 0;
transform: translateY(10px);
}
@media (min-width: 810px) {
.cfg-inner {
padding: 90px 40px;
}
.cfg-heading {
font-size: 38px;
letter-spacing: -0.76px;
}
.cfg-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.cfg-photos {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.cfg-photo--0,
.cfg-photo--1 {
aspect-ratio: 4 / 3;
}
}
@media (min-width: 1200px) {
.cfg-inner {
grid-template-columns: minmax(0, 835px) 417px;
justify-content: space-between;
align-items: center;
padding: 110px 0 120px;
padding-inline: var(--tw-grid-gutter, 60px);
}
.cfg-inner--solo {
grid-template-columns: minmax(0, 1fr);
}
.cfg-heading {
font-size: 50px;
line-height: 60px;
letter-spacing: -1px;
}
.cfg-grid {
gap: 21px 24px;
margin-top: 50px;
}
.cfg-card {
min-height: 237px;
padding: 30px;
}
.cfg-title {
font-size: 24px;
line-height: 28.8px;
letter-spacing: -0.96px;
}
.cfg-photos {
grid-template-columns: minmax(0, 1fr);
}
.cfg-photo--0 {
aspect-ratio: 417 / 257;
}
.cfg-photo--1 {
aspect-ratio: 417 / 437;
}
}
@media (prefers-reduced-motion: reduce) {
[data-cfg-reveal="up"],
.cfg-w {
transition: none;
}
}
</style>
<script>
/* Appear-on-scroll; one observer per instance, torn down on astro:before-swap. */
function initChecklistFeatureGrid(root: HTMLElement) {
if (root.dataset.cfgReady === "") return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !("IntersectionObserver" in window)) return;
const controller = new AbortController();
root.dataset.cfgReady = "";
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
(entry.target as HTMLElement).dataset.in = "";
io.unobserve(entry.target);
}
},
{ threshold: 0.1, rootMargin: "0px 0px -5% 0px" },
);
root.querySelectorAll<HTMLElement>("[data-cfg-reveal]").forEach((t) => io.observe(t));
controller.signal.addEventListener("abort", () => io.disconnect());
document.addEventListener(
"astro:before-swap",
() => {
controller.abort();
delete root.dataset.cfgReady;
},
{ once: true, signal: controller.signal },
);
}
const boot = () => document.querySelectorAll<HTMLElement>("[data-cfg-root]").forEach(initChecklistFeatureGrid);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
document.addEventListener("astro:page-load", boot);
</script>
===== END src/components/ChecklistFeatureGrid.astro =====
===== FILE (supporting): src/components/ChecklistFeatureGrid.md =====
# Checklist feature grid
A why-choose-us section on a pale band: outlined eyebrow and a big uppercase
heading over a 2×2 grid of white cards (round orange icon, title, hairline,
two check-marked points), with two stacked photos in a column on the right.
Catalog page: `/components/checklist-feature-grid`
## Files required
- **`ChecklistFeatureGrid.astro`** — the component, with one small appear script.
- **`public/checklist-feature-grid/courier-box.webp`**, **`cargo-plane.webp`** —
the catalog demo's photos, passed through `images`. The component has no
default photos.
- `ChecklistFeatureGrid.md` — this file.
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `"Why Choose Us"` | Generic label. `""` hides it. Never renders on its own. |
| `heading` | `string?` | `""` | Orange words in `*asterisks*`. Hidden when empty. |
| `headingLevel` | `"h2" \| "h3"` | `"h2"` | Card titles are one level below. |
| `features` | `{ title, points, icon? }[]` | `[]` | `icon`: shapes, team, chart, support, shield, clock. A card without a `title` is dropped; empty points are skipped. |
| `images` | `[{src?,alt?},{src?,alt?}] \| null` | `null` | Up to two photos, top then bottom. Omitted, empty or `null` drops the column; `src: ""` draws a neutral placeholder. Never topped up with demo photos. |
| `background` | `string?` | `#f0f8fa` | |
| `fontStylesheet`, `class`, `id` | | | |
Nothing is invented: with no `features` the component renders nothing at all
(not even the font link). One card passed renders one card.
## Behaviour (measured)
Cards and photos rise from 80px, staggered; heading words rise from 10px. No
hover state on the reference.
## Images and credits
| File | Size | Credit |
| :--- | :--- | :--- |
| `courier-box.webp` | 834 × 514 | Unsplash brizG9kR6qI, Unsplash License |
| `cargo-plane.webp` | 834 × 874 | Unsplash 85gDb_IHdAQ, Unsplash License |
The cargo-plane photo shows an airline's tail livery at small size. Swap it
on a client site if that matters.
===== END src/components/ChecklistFeatureGrid.md =====
===== FILE (required): public/checklist-feature-grid/courier-box.webp =====
[binary asset — download https://astro.baysixmedia.com/checklist-feature-grid/courier-box.webp and save it to public/checklist-feature-grid/courier-box.webp]
===== FILE (required): public/checklist-feature-grid/cargo-plane.webp =====
[binary asset — download https://astro.baysixmedia.com/checklist-feature-grid/cargo-plane.webp and save it to public/checklist-feature-grid/cargo-plane.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.