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: "Bordered logo grid"
Library page: https://astro.baysixmedia.com/components/bordered-logo-grid/
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/BorderedLogoGrid.astro =====
---
/**
* BorderedLogoGrid — an editorial proof wall.
*
* Client logos in a ruled wall: hairline-divided cells with no cards, no
* fills and no shadows, the label set as a real heading. In the default
* "staggered" layout the heading takes the wall's first cell and the logos
* run on after it, so the marks start one cell in; "grid" puts the heading
* above an unbroken wall.
*
* The wall is laid out for the number of logos it is given. The column count
* (and how many cells the heading spans) is chosen so rows come out full, and
* where no arrangement can avoid a short last row the remainder is closed
* with blank ruled cells rather than left ragged. Each mark is sized for
* equal visual weight from its aspect ratio (`width` at 40px tall), so a wide
* wordmark and a square mark read as the same size.
*
* No script. Type is inherited from the page; nothing is fetched unless a
* `fontStylesheet` is passed. Each cell is a link only when its logo has an
* `href`.
*/
interface BrandLogo {
/** The cell's caption, and its accessible name. */
name: string;
/** The mark. Unset renders a hatched 3:1 placeholder. */
src?: string;
/** The mark's width when drawn 40px tall — its aspect ratio, used for optical sizing and to reserve space. */
width?: number;
/** Empty by default: the caption already says who it is. */
alt?: string;
/** What the logo should be. Used by the placeholder only. */
hint?: string;
href?: string;
}
interface Props {
id?: string;
/** The heading over (or inside) the wall. Empty drops it. */
label?: string;
labelLevel?: "h2" | "h3" | "p";
logos?: BrandLogo[];
/** "staggered": the heading takes the wall's first cell. "grid": the heading sits above an unbroken wall. */
layout?: "staggered" | "grid";
/** A closing hairline under the section. */
divider?: boolean;
/** Optional stylesheet URL. None by default: the component inherits the page's type. */
fontStylesheet?: string;
}
const { id, label = "", labelLevel = "h2", logos: rawLogos = [], layout = "staggered", divider = true, fontStylesheet = "" } = Astro.props;
const Label = labelLevel;
/* A logo without a name has nothing to caption it: dropped. No logos, no section (a label alone is a claim). */
const logos = rawLogos.filter((logo) => logo?.name);
const n = logos.length;
const inWall = layout === "staggered" && Boolean(label);
/*
* Desktop arrangement. For each column count and heading span, the number of
* blank cells needed to close the last row; the least wins, then the column
* count nearest five, then the narrower heading. Deterministic from `n`.
*/
function arrange(count: number, heading: boolean) {
let best = { cols: Math.min(Math.max(count, 3), 5), span: heading ? 1 : 0, fill: 0, score: Infinity };
for (const cols of [3, 4, 5, 6]) {
for (const span of heading ? [1, 2, 3] : [0]) {
if (span >= cols) continue;
const cells = count + span;
if (cells < cols && cols > 3) continue; /* never a single ragged row wider than its content */
const fill = (cols - (cells % cols)) % cols;
const rows = Math.ceil(cells / cols);
const score = fill * 10 + Math.abs(cols - 5) + (rows > 3 ? 4 : 0) + (span === 3 ? 1 : 0) + (span >= 2 && cols < 5 ? 2 : 0);
if (score < best.score) best = { cols, span, fill, score };
}
}
return best;
}
const desk = arrange(n, inWall);
/* Tablet: three columns; the heading takes a cell or the whole first row, whichever closes the wall. */
const tabSpan = inWall ? ((3 - ((n + 1) % 3)) % 3 <= (3 - (n % 3)) % 3 ? 1 : 3) : 0;
const tabFill = (3 - ((n + (tabSpan === 1 ? 1 : 0)) % 3)) % 3;
/* Phone: two columns, the heading a full row. */
const phoneFill = n % 2;
const fillers = Math.max(desk.fill, tabFill, phoneFill);
/*
* Optical sizing: every mark gets about the area of a 3:1 wordmark drawn 37px
* tall (≈ 4100px²), so a wide wordmark and a square badge carry the same
* weight. Clamped so a tall mark never towers and a very wide one never
* shrinks below legibility.
*/
const markHeight = (width?: number) => {
if (!width || width <= 0) return undefined;
const ratio = width / 40;
return Math.round(Math.min(46, Math.max(22, Math.sqrt(4100 / ratio))));
};
const LOGO_SPEC = "3:1 · SVG";
const LOGO_HINT = "Client logo, single colour";
const style = `--blg-cols:${desk.cols};--blg-span:${desk.span};--blg-tab-span:${tabSpan}`;
---
{n > 0 && (
<section class="blg" id={id} data-layout={inWall ? "staggered" : "grid"} data-divider={divider ? "" : undefined} style={style}>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<div class="blg-container">
{label && !inWall && <Label class="blg-label">{label}</Label>}
<ul class="blg-wall" role="list">
{inWall && (
<li class="blg-head">
<Label class="blg-label">{label}</Label>
</li>
)}
{logos.map((logo) => {
const h = markHeight(logo.width);
const inner = (
<Fragment>
<span class="blg-stage">
{logo.src ? (
<img
class="blg-mark"
src={logo.src}
alt={logo.alt ?? ""}
width={logo.width}
height="40"
style={h ? `--blg-h:${h}px` : undefined}
loading="lazy"
decoding="async"
/>
) : (
<span class="blg-ph" role="img" aria-label={`Image placeholder: ${logo.hint ?? LOGO_HINT}`}>
<span class="blg-ph-spec">{LOGO_SPEC}</span>
</span>
)}
</span>
<span class:list={["blg-name", logo.src && "blg-name--sr"]}>{logo.name}</span>
</Fragment>
);
return (
<li class="blg-item">
{logo.href ? (
<a class="blg-cell" href={logo.href}>
{inner}
</a>
) : (
<div class="blg-cell">{inner}</div>
)}
</li>
);
})}
{Array.from({ length: fillers }, (_, i) => (
<li
class="blg-fill"
aria-hidden="true"
data-d={i < desk.fill ? "" : undefined}
data-t={i < tabFill ? "" : undefined}
data-m={i < phoneFill ? "" : undefined}
/>
))}
</ul>
</div>
</section>
)}
<style>
.blg {
--blg-ink: var(--lx-ink, #16181d);
--blg-muted: var(--lx-muted, #5d636e);
--blg-rule: var(--lx-rule, rgb(22 24 29 / 0.14));
--blg-surface: var(--lx-surface-page, #ffffff);
--blg-accent: var(--lx-accent, #16181d);
container: blg / inline-size;
box-sizing: border-box;
display: flex;
justify-content: center;
padding: clamp(56px, 8vw, 112px) clamp(20px, 4vw, 40px);
background: var(--blg-surface);
color: var(--blg-ink);
font-family: inherit;
}
.blg *,
.blg *::before,
.blg *::after {
box-sizing: border-box;
}
.blg-container {
width: 100%;
max-width: 1200px;
}
/* The closing hairline is the wall's own bottom edge; without the divider the wall ends open. */
.blg:not([data-divider]) .blg-wall {
border-block-end: 0;
}
.blg-label {
margin: 0;
max-width: 24ch;
font-size: clamp(1.375rem, 1.1rem + 1.1vw, 1.875rem);
font-weight: 600;
line-height: 1.12;
letter-spacing: -0.022em;
text-wrap: balance;
color: var(--blg-ink);
}
/* Heading above the wall (grid layout): a short rule, then the heading. */
.blg[data-layout="grid"] .blg-container > .blg-label {
margin-bottom: clamp(28px, 4vw, 44px);
}
.blg[data-layout="grid"] .blg-container > .blg-label::before,
.blg-head .blg-label::before {
content: "";
display: block;
width: 28px;
height: 2px;
margin-bottom: 18px;
background: var(--blg-accent);
}
/*
* The wall. The hairlines are the wall's own colour showing through a 1px
* gap between opaque cells, so every internal rule is exactly one pixel
* and meets its neighbours cleanly at any column count. Only the top and
* bottom edges carry a border; the sides stay open.
*/
.blg-wall {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
margin: 0;
padding: 0;
list-style: none;
background: var(--blg-rule);
border-block: 1px solid var(--blg-rule);
}
.blg-wall > li {
min-width: 0;
background: var(--blg-surface);
}
.blg-head {
grid-column: 1 / -1;
display: flex;
align-items: flex-end;
padding: 28px 14px 26px;
}
.blg-item {
display: flex;
}
.blg-cell {
position: relative;
display: grid;
grid-template-rows: 1fr auto;
align-items: center;
width: 100%;
min-height: 132px;
padding: 18px 14px 14px;
color: inherit;
text-decoration: none;
}
.blg-stage {
display: grid;
place-items: center;
min-height: 64px;
}
.blg-mark {
display: block;
width: auto;
height: var(--blg-h, 40px);
max-width: min(78%, 180px);
object-fit: contain;
opacity: 0.86;
transition: opacity 180ms ease;
}
/* A 3:1 plate at the size real marks are drawn, carrying the spec only; the hint is in its accessible name. */
.blg-ph {
display: grid;
place-items: center;
width: min(120px, 78%);
aspect-ratio: 3 / 1;
border: 1px dashed rgb(22 24 29 / 0.28);
background: repeating-linear-gradient(135deg, rgb(22 24 29 / 0.045) 0 6px, transparent 6px 12px);
color: var(--blg-muted);
}
.blg-ph-spec {
font-size: 0.6875rem;
letter-spacing: 0.04em;
white-space: nowrap;
}
/* A caption only where it carries information (a placeholder); a real mark's name stays in its accessible name. */
.blg-name--sr {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.blg-name {
justify-self: start;
max-width: 100%;
font-size: 0.75rem;
font-weight: 500;
line-height: 1.3;
letter-spacing: 0.01em;
color: var(--blg-muted);
overflow-wrap: anywhere;
transition: color 180ms ease;
}
/* Blank cells that close a short last row; each shows only at the width it is needed for. */
.blg-fill {
display: none;
}
.blg-fill[data-m] {
display: block;
}
a.blg-cell:hover .blg-mark {
opacity: 1;
}
a.blg-cell:hover .blg-name {
color: var(--blg-ink);
}
a.blg-cell:focus-visible {
outline: 2px solid var(--blg-accent);
outline-offset: -4px;
}
a.blg-cell:focus-visible .blg-name {
color: var(--blg-ink);
}
@container blg (min-width: 560px) {
.blg-wall {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.blg-head {
grid-column: span var(--blg-tab-span);
padding: 26px 22px 22px 18px;
}
.blg-cell {
min-height: 148px;
padding: 22px 18px 16px;
}
.blg-fill[data-m] {
display: none;
}
.blg-fill[data-t] {
display: block;
}
}
@container blg (min-width: 960px) {
.blg-wall {
grid-template-columns: repeat(var(--blg-cols), minmax(0, 1fr));
}
.blg-head {
grid-column: span var(--blg-span);
padding: 30px 32px 28px 22px;
}
/* The heading grows with the cells it spans, so a wide heading cell is filled by type, not left empty. */
.blg-head .blg-label {
max-width: none;
font-size: clamp(1.5rem, calc(0.55rem + var(--blg-span) * 0.95cqw), 2.625rem);
line-height: 1.06;
letter-spacing: -0.028em;
}
.blg-cell {
min-height: 168px;
padding: 26px 22px 18px;
}
.blg-stage {
min-height: 88px;
}
.blg-fill[data-t] {
display: none;
}
.blg-fill[data-d] {
display: block;
}
}
@media (prefers-reduced-motion: reduce) {
.blg-mark,
.blg-name {
transition: none;
}
}
@media (forced-colors: active) {
/* The hairlines are the wall's background showing through 1px gaps; keep that background, or the rules vanish. */
.blg-wall {
forced-color-adjust: none;
background: CanvasText;
}
.blg-wall > li {
background: Canvas;
}
a.blg-cell:focus-visible {
outline-color: Highlight;
}
}
</style>
===== END src/components/BorderedLogoGrid.astro =====
===== FILE (supporting): src/components/BorderedLogoGrid.md =====
# Bordered logo grid
An editorial proof wall: client logos in a ruled, hairline-divided wall, the
heading set in the wall's first cell. The wall is laid out for the number of
logos it gets, marks are sized for equal visual weight, and type is inherited
from the page.
Catalog page: `/components/bordered-logo-grid`
## Purpose
Social proof that reads as a considered brand wall rather than a row of
tiles: a real heading, sized to the cells it spans, and generous ruled cells
where the marks do the talking.
Use something else for more than about fifteen logos (a marquee reads better)
or for logos that need to animate.
## Files required
**`BorderedLogoGrid.astro`**: one file, no script.
- No imports, no npm packages, no webfont: type is inherited from the page.
- Colours read `--lx-*` custom properties with fallbacks.
`BorderedLogoGrid.md`: this file. Not needed to run.
No image files. A logo without `src` draws a labelled placeholder. No client
names ship as defaults: pass `logos` (and `label`) or nothing renders.
## Basic usage
```astro
---
import BorderedLogoGrid from "../components/BorderedLogoGrid.astro";
---
<BorderedLogoGrid
label="Trusted by homeowners and builders across the Peninsula"
logos={[
{ name: "Northgate", src: "/logos/northgate.svg", width: 180 },
{ name: "Kestrel Air", src: "/logos/kestrel.svg", width: 40, href: "/case-studies/kestrel" },
]}
/>
```
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `id` | `string?` | none | Root id. |
| `label` | `string?` | `""` | The wall's heading. Empty drops it. Shown only alongside logos. |
| `labelLevel` | `"h2" \| "h3" \| "p"?` | `"h2"` | So it fits your outline. |
| `logos` | `BrandLogo[]?` | `[]` | See below. A logo without a `name` is dropped. With no logos the component renders nothing. |
| `layout` | `"staggered" \| "grid"?` | `"staggered"` | `staggered`: the heading takes the wall's first cell. `grid`: the heading sits above an unbroken wall. |
| `divider` | `boolean?` | `true` | The wall's closing bottom rule. `false` leaves the wall open at the foot. |
| `fontStylesheet` | `string?` | `""` | Optional stylesheet URL. Nothing is fetched by default. |
```ts
interface BrandLogo {
name: string; // the cell's accessible name; shown as a caption only under a placeholder
src?: string; // the mark
width?: number; // its width at 40px tall — its aspect ratio, for optical sizing
alt?: string; // "" by default: the name already labels the cell
hint?: string; // placeholder only
href?: string; // makes the whole cell one link
}
```
## Layout for any count
The desktop wall picks 3–6 columns, and how many cells the heading spans, so
that rows come out full: three logos sit in one row beside a wide heading,
eight make two rows of five with the heading across two cells, twelve make
three rows of five. When no arrangement closes the last row it is finished
with blank ruled cells, which show only at the width that needs them. Tablets
use three columns, phones two (the heading a full row). The layout follows
the component's own width through container queries.
## Images
| Slot | Default hint | Ratio · size |
| :--- | :--- | :--- |
| `logos[].src` | Client logo, single colour | any ratio · SVG preferred |
With `width` supplied, each mark is drawn at about the area of a 3:1 wordmark
37px tall (≈ 4100px²), clamped between 22 and 46px tall — a wide wordmark and
a square badge carry the same weight and none dominates. Without `width` the
mark is drawn 40px tall. The placeholder is a hatched 3:1 plate showing the
spec; the hint is in its accessible name.
## Behaviour
No script. A cell with `href` is a link: hover brings the mark to full
strength. The heading grows with the cells it spans (up to 2.6rem across
three), so a wide heading cell is filled by type rather than left empty.
## Accessibility
- A real `<ul>` of cells (with `role="list"` so Safari keeps list semantics
under `list-style: none`); filler cells are `aria-hidden`.
- Logo images are `alt=""` by default; each cell carries the company name as
visually hidden text (a visible caption under a placeholder), so a linked
cell's accessible name is the company name.
- Linked cells get an inset `:focus-visible` ring that is never clipped by a
neighbouring cell.
- Under `forced-colors: active` the rules become `CanvasText` lines.
## Reduced motion
The only transitions are the hover colour changes; they are removed.
## Without JavaScript
Identical. There is no script.
## Responsive
| Component width | Wall | Heading |
| :--- | :--- | :--- |
| ≥ 960 | 3–6 columns chosen from the logo count | a cell spanning 1–3 columns (staggered) |
| 560–959 | 3 columns | one cell, or the first row when that closes the wall |
| < 560 | 2 columns | the first row |
===== END src/components/BorderedLogoGrid.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: not applicable — this component ships no script.
--- 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.