The 1 file 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: "Partner chip band"
Library page: https://astro.baysixmedia.com/components/partner-chip-band/
Files: 1 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/PartnerChipBand.astro =====
---
/**
* PartnerChipBand
*
* A dark ink band of partner "logos": a lime-dot eyebrow, a centred heading
* and a row of rounded chips, each tinted in its own hue at 0.08 alpha and
* carrying a small glyph, a hue dot and a text wordmark. Two soft glow blobs
* sit in the corners.
*
* Hover lifts a chip (scale 1.02, -4px) and doubles its tint to 0.16.
* The chips fade up 16px, staggered, the first time the band scrolls into
* view. The hidden starting pose only applies once the script has marked the
* root ready, so without JavaScript or with reduced motion every chip is at
* rest from the start.
*
* Reads --nv-* design tokens from any ancestor, with fallbacks.
*/
type Glyph = "peak" | "star" | "bars" | "orbit" | "diamond" | "wave";
export interface PartnerChip {
name: string;
/** Chip hue as "r g b" or any CSS colour. Tint and dot derive from it. */
hue?: string;
glyph?: Glyph;
/** Optional link. Without one the chip is not interactive. */
href?: string;
}
export interface PartnerChipBandProps {
eyebrow?: string;
heading?: string;
headingLevel?: "h2" | "h3" | "h4";
partners?: PartnerChip[];
/** Accessible name of the chip list. */
listLabel?: string;
/** The two corner glow blobs. */
glow?: boolean;
/** The staggered fade-up on first view. Skipped under reduced motion. */
appear?: boolean;
fontStylesheet?: string;
id?: string;
}
interface Props extends PartnerChipBandProps {}
const {
eyebrow = "",
heading = "",
headingLevel = "h2",
partners: rawPartners = [],
listLabel = "Partners",
glow = true,
appear = true,
fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
id,
} = Astro.props;
const Heading = headingLevel;
/* A chip without a name is dropped. No partners, no band: the heading alone would be a claim. */
const partners = rawPartners.filter((p) => p?.name);
const GLYPHS: Record<Glyph, string> = {
peak: '<path d="M4 16L10 5l6 11h-3.2L10 10.6 7.2 16z"/>',
star: '<path d="M10 3.5l1.9 4.3 4.6.4-3.5 3 1.1 4.6L10 13.4l-4.1 2.4 1.1-4.6-3.5-3 4.6-.4z"/>',
bars: '<path d="M4 15V9h2.6v6zM8.7 15V5h2.6v10zM13.4 15v-4H16v4z"/>',
orbit: '<path d="M10 4a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/>',
diamond: '<path d="M10 3.5l6.5 6.5-6.5 6.5L3.5 10z"/>',
wave: '<path d="M3.5 8.2c2.2-1.8 4.3-1.8 6.5 0s4.3 1.8 6.5 0v3c-2.2 1.8-4.3 1.8-6.5 0s-4.3-1.8-6.5 0zM3.5 13.4c2.2-1.8 4.3-1.8 6.5 0s4.3 1.8 6.5 0v2.2c-2.2 1.8-4.3 1.8-6.5 0s-4.3-1.8-6.5 0z"/>',
};
const GLYPH_ORDER: Glyph[] = ["peak", "star", "bars", "orbit", "diamond", "wave"];
const HUES = ["115 245 223", "200 255 56", "168 140 255", "255 184 107", "110 168 255", "247 143 179"];
/* "r g b" triplets get the measured alpha tints; any other colour is mixed. */
const hueStyle = (hue: string, i: number) => {
const rgb = /^\d{1,3}\s+\d{1,3}\s+\d{1,3}$/.test(hue.trim());
const solid = rgb ? `rgb(${hue})` : hue;
const tint = (a: number) =>
rgb ? `rgb(${hue} / ${a})` : `color-mix(in srgb, ${hue} ${a * 100}%, transparent)`;
return `--pcb-hue: ${solid}; --pcb-tint: ${tint(0.08)}; --pcb-tint-hover: ${tint(0.16)}; --pcb-edge: ${tint(0.16)}; --pcb-i: ${i};`;
};
---
{partners.length > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section
class="pcb"
id={id}
data-pcb-root
data-pcb-appear={appear ? "1" : "0"}
>
{
glow && (
<>
<span class="pcb-glow pcb-glow--aqua" aria-hidden="true" />
<span class="pcb-glow pcb-glow--violet" aria-hidden="true" />
</>
)
}
<div class="pcb-inner">
<div class="pcb-head">
{
eyebrow && (
<p class="pcb-eyebrow">
<span class="pcb-dot" aria-hidden="true" />
{eyebrow}
</p>
)
}
{heading && <Heading class="pcb-heading">{heading}</Heading>}
</div>
{
partners.length > 0 && (
<ul class="pcb-chips" aria-label={listLabel}>
{partners.map((p, i) => {
const inner = (
<>
<span class="pcb-glyph" aria-hidden="true">
<svg viewBox="0 0 20 20" focusable="false" set:html={GLYPHS[p.glyph ?? GLYPH_ORDER[i % 6]]} />
</span>
<span class="pcb-hue-dot" aria-hidden="true" />
<span class="pcb-name">{p.name}</span>
</>
);
return (
<li class="pcb-chip" style={hueStyle(p.hue ?? HUES[i % 6], i)}>
{p.href ? (
<a class="pcb-chip-body pcb-chip-body--link" href={p.href}>
{inner}
</a>
) : (
<span class="pcb-chip-body">{inner}</span>
)}
</li>
);
})}
</ul>
)
}
</div>
</section>
</>
)}
<style>
.pcb {
--pcb-ink: var(--nv-ink, #08110f);
--pcb-lime: var(--nv-lime, #c8ff38);
--pcb-text: #f7fafc;
--pcb-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
--pcb-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
box-sizing: border-box;
overflow: hidden;
isolation: isolate;
padding: 64px 20px;
background: var(--pcb-ink);
color: var(--pcb-text);
font-family: var(--pcb-font);
}
.pcb *,
.pcb *::before,
.pcb *::after {
box-sizing: border-box;
}
:where(.pcb) :where(p, h2, h3, h4, ul) {
margin: 0;
}
.pcb-glow {
position: absolute;
z-index: -1;
border-radius: 50%;
pointer-events: none;
}
.pcb-glow--aqua {
top: -150px;
left: -150px;
width: 460px;
height: 460px;
background: radial-gradient(50% 50%, rgb(115 245 223 / 0.28) 0%, rgb(115 245 223 / 0) 70%);
}
.pcb-glow--violet {
top: 47px;
right: -150px;
width: 500px;
height: 500px;
background: radial-gradient(50% 50%, rgb(168 140 255 / 0.26) 0%, rgb(168 140 255 / 0) 70%);
}
.pcb-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
max-width: 1337px;
margin: 0 auto;
}
.pcb-head {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
max-width: 680px;
text-align: center;
}
.pcb-eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
color: rgb(247 250 252 / 0.72);
font: 700 11px/13.2px var(--pcb-font);
letter-spacing: 1.32px;
text-transform: uppercase;
}
.pcb-dot {
flex: none;
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--pcb-lime);
opacity: 0.64;
}
.pcb-heading {
color: var(--pcb-text);
font: 700 34px/36.72px var(--pcb-font);
letter-spacing: -1.2px;
}
.pcb-chips {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
padding: 0;
list-style: none;
}
.pcb-chip {
flex: 0 0 180px;
/* The appear pose lives on `translate`, the hover on `transform`, so
the two never fight over one property. */
transition:
opacity 0.6s var(--pcb-ease) calc(var(--pcb-i, 0) * 80ms),
translate 0.6s var(--pcb-ease) calc(var(--pcb-i, 0) * 80ms);
}
.pcb-chip-body {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
height: 100px;
padding: 0 18px;
border-radius: 18px;
background-color: var(--pcb-tint);
box-shadow: inset 0 0 0 1px var(--pcb-edge);
color: inherit;
text-decoration: none;
transition:
transform 0.3s var(--pcb-ease),
background-color 0.3s var(--pcb-ease);
}
.pcb-chip-body:hover {
transform: scale(1.02) translateY(-4px);
background-color: var(--pcb-tint-hover);
}
.pcb-chip-body--link:focus-visible {
outline: 2px solid var(--pcb-lime);
outline-offset: 3px;
background-color: var(--pcb-tint-hover);
}
.pcb-glyph {
display: grid;
flex: none;
place-items: center;
width: 20px;
height: 20px;
border-radius: 3px;
background: #f7f8f6;
color: var(--pcb-ink);
}
.pcb-glyph svg {
width: 16px;
height: 16px;
fill: currentColor;
}
.pcb-hue-dot {
flex: none;
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--pcb-hue);
}
.pcb-name {
color: var(--pcb-text);
font: 700 16px/16px var(--pcb-font);
letter-spacing: 0.64px;
white-space: nowrap;
}
/* Appear: only once the script is running and motion is allowed. */
.pcb[data-pcb-ready] .pcb-chip {
opacity: 0;
translate: 0 16px;
}
.pcb[data-pcb-ready][data-pcb-in] .pcb-chip {
opacity: 1;
translate: 0 0;
}
@media (prefers-reduced-motion: no-preference) {
.pcb-dot {
animation: pcb-pulse 2.4s ease-in-out infinite;
}
}
@keyframes pcb-pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.55;
transform: scale(1.36);
}
}
@media (prefers-reduced-motion: reduce) {
.pcb-chip-body:hover {
transform: none;
}
.pcb-chip {
transition: none;
}
}
/* Phone: the violet glow drops to the foot of the stacked column, as in
the reference. */
@media (max-width: 809.98px) {
.pcb-glow--violet {
top: auto;
right: auto;
bottom: -200px;
left: calc(50% - 280px);
}
}
@media (min-width: 810px) {
.pcb {
padding: 64px 44px;
}
}
</style>
<script>
/**
* PartnerChipBand controller: the staggered appear, once per instance.
*
* Sets data-pcb-ready (which applies the hidden pose) only when it can also
* reveal the chips, then data-pcb-in when the band enters the viewport.
*/
function initPartnerChipBand(root: HTMLElement): void {
if (root.dataset.pcbBooted === "") return;
if (root.dataset.pcbAppear !== "1") return;
if (!("IntersectionObserver" in window)) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
root.dataset.pcbBooted = "";
const controller = new AbortController();
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
root.dataset.pcbIn = "";
observer.disconnect();
}
},
{ threshold: 0.2 },
);
root.dataset.pcbReady = "";
observer.observe(root);
controller.signal.addEventListener("abort", () => observer.disconnect());
document.addEventListener(
"astro:before-swap",
() => {
controller.abort();
delete root.dataset.pcbBooted;
delete root.dataset.pcbReady;
delete root.dataset.pcbIn;
},
{ once: true, signal: controller.signal },
);
}
const boot = () =>
document.querySelectorAll<HTMLElement>("[data-pcb-root]").forEach(initPartnerChipBand);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, { once: true });
} else {
boot();
}
document.addEventListener("astro:page-load", boot);
</script>
===== END src/components/PartnerChipBand.astro =====
===== FILE (supporting): src/components/PartnerChipBand.md =====
# Partner chip band
A dark ink band of partner wordmarks: a lime-dot eyebrow, a centred heading
and a row of rounded chips, each tinted in its own hue with a small glyph, a
hue dot and a name. Two soft glows sit in the corners.
Catalog page: `/components/partner-chip-band`
## Purpose
The trust strip under a hero: names of partners or customers. It works when
you have text names and no logo files, because each chip carries its own
colour and glyph.
If you have real logo files and permission to use them, a logo cloud suits
better.
## Files required
**`PartnerChipBand.astro`**: one file, the whole component.
- No imports, no npm packages, no framework, no images.
- One small inline script for the appear-on-scroll. Everything else is CSS.
- Loads Manrope and Inter from Google Fonts unless `fontStylesheet=""`.
`PartnerChipBand.md`: this file. Documentation, not required to run.
## Basic usage
```astro
---
import PartnerChipBand from "../components/PartnerChipBand.astro";
---
<PartnerChipBand
heading="Chosen by the teams that keep shelves stocked."
partners={[
{ name: "CASPIAN", hue: "115 245 223", glyph: "peak" },
{ name: "KESTREL", hue: "200 255 56", glyph: "star", href: "/customers/kestrel" },
]}
/>
```
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `""` | Small caps line with a lime dot. Empty drops it. |
| `heading` | `string?` | `""` | Centred heading, max 680px. Empty drops it. |
| `headingLevel` | `"h2" \| "h3" \| "h4"` | `"h2"` | |
| `partners` | `PartnerChip[]?` | `[]` | See below. A chip without a `name` is dropped. With no partners the band (and its font link) renders nothing; eyebrow and heading never render on their own. |
| `listLabel` | `string?` | `"Partners"` | Accessible name of the chip list. |
| `glow` | `boolean?` | `true` | The aqua and violet corner glows. |
| `appear` | `boolean?` | `true` | The staggered fade-up on first view. |
| `fontStylesheet` | `string?` | Manrope + Inter | `""` to skip. |
| `id` | `string?` | none | Root id. |
`PartnerChip`: `{ name: string; hue?: string; glyph?: "peak" | "star" |
"bars" | "orbit" | "diamond" | "wave"; href?: string }`.
- `hue` as an `"r g b"` triplet gets the exact measured tints (0.08 at rest,
0.16 on hover, 0.16 edge). Any other CSS colour is mixed with `color-mix()`.
Defaults cycle aqua, lime, purple, orange, blue, pink.
- `href` turns the chip into a link. Without one the chip is not interactive.
## Behaviour
**Layout.** Padding 64px 44px; heading block, then chips 30px below. Chips
are 180 × 100, 18px radius, 14px apart, centred and wrapping. Six fit on one
row at 1280px and up; fewer per row below; one column on a phone.
**Hover** (0.3s ease-out):
| Element | Change |
| :--- | :--- |
| Chip | `scale(1.02) translateY(-4px)`, tint alpha 0.08 → 0.16 |
The transform is off under reduced motion. The tint change stays.
**Animation.**
- Appear: each chip goes from `opacity 0, translateY(16px)` to rest over 0.6s,
80ms apart, the first time the band is 20% in view. The pose uses the CSS
`translate` property and the hover uses `transform`, so they never cancel
each other.
- The eyebrow dot pulses (scale 1 → 1.36, opacity 1 → 0.55, 2.4s loop).
**Script.** One IntersectionObserver per instance. It sets `data-pcb-ready`
(which applies the hidden pose) and observes; on intersection it sets
`data-pcb-in` and disconnects. It does nothing when `appear` is false, under
reduced motion, or without IntersectionObserver, so in all of those cases, and
without JavaScript, the chips are visible from the first paint. It boots on
load and on `astro:page-load`, is idempotent per root, and tears down through
an AbortController on `astro:before-swap`.
## Accessibility
- Chips are a `<ul>` named by `listLabel`; each name is real text.
- Glyphs, hue dots, glows and the eyebrow dot are `aria-hidden`.
- Linked chips are 100px tall and show a lime `:focus-visible` ring.
- No focus movement and no scrolling on load.
## Integration notes
- Full bleed: paints its own ink surface. No wrapper needed.
- Renders a `<section>` with no accessible name. Add one if the page has
several landmarks.
- Theme with `--nv-ink`, `--nv-lime`, `--nv-font` on any ancestor.
## Images and credits
None. Glyphs are original inline SVG and the partner names are fictional text
wordmarks, so nothing needs a credit.
## Approximations
- **Chip edge.** The screenshot shows a faint outline the dump does not
report as a border. It is drawn as a 1px inset shadow in the chip's hue at
0.16 alpha.
- **Glyph tile.** The reference used small raster logos on white. These are
20px off-white rounded tiles with an original ink glyph.
- **Eyebrow dot pulse** is inferred from one mid-animation sample (opacity
0.64, scale about 1.36). The loop timing is a guess.
- **Stagger and duration** (80ms, 0.6s) are not measured. The 16px offset is.
- **Phone.** The reference crops its sixth chip inside a fixed 1000px band.
This version shows all six and lets the band grow. The violet glow moves to
the foot of the column, as in the phone capture.
- **Tablet.** No tablet capture exists. Chips wrap naturally (four then two at
1024px, for example).
===== END src/components/PartnerChipBand.md =====
--- 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.