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: "Centered logo footer"
Library page: https://astro.baysixmedia.com/components/centered-logo-footer/
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/CenteredLogoFooter.astro =====
---
/**
* Centered logo footer
*
* A site footer that stacks a brand mark, a single row of links, a hairline
* rule and a bottom row carrying the copyright and a set of small icon links.
* Everything above the rule is centred; the row below it sits shoulder to
* shoulder on desktop and stacks on a phone.
*
* Static: no script at all. Every interactive part is a real link, so the
* whole thing works with JavaScript switched off and there is nothing for a
* client-side router to tear down or re-initialise.
*
* Self-contained: no imports, no packages, no global stylesheet, no webfont.
* Every colour, size and duration is a `--clf-*` custom property on the root.
*/
export interface FooterLink {
/** The visible word. */
label: string;
/** Where it goes. Anything that is not a navigable scheme is dropped. */
href: string;
}
export interface FooterSocialLink {
/**
* The accessible name — "Email", "RSS". Not shown, and it is the ONLY
* thing a screen reader gets for an icon link, so it has to stand alone.
*/
label: string;
href: string;
/** Which glyph. See GLYPHS below; add to that map to extend the set. */
icon: "mail" | "link" | "rss" | "chat" | "phone" | "pin";
}
interface Props {
/** The wordmark beside the brand mark. `""` renders the mark alone. */
brand?: string;
/** Where the brand mark links. */
brandHref?: string;
/**
* An image to use instead of the built-in mark. Square; anything else is
* letterboxed into the same box. Left unset the component draws its own,
* so the footer renders correctly with no assets to install.
*/
logoSrc?: string;
/** The row of links. */
links?: FooterLink[];
/** Accessible name for the link row, since a page may have several navs. */
navLabel?: string;
/** The line under the rule. No default: omitted, the line is not drawn. */
copyright?: string;
/** Small icon links on the other end of the bottom row. */
social?: FooterSocialLink[];
/** Page surface the footer paints, edge to edge. */
background?: string;
/** The wordmark and the brand mark. */
ink?: string;
/** The link row at rest. */
linkColor?: string;
/** The link row under a pointer or focus. */
linkHoverColor?: string;
/** Copyright and icon links. */
mutedColor?: string;
/** The hairline across the top of the footer. */
borderColor?: string;
/** The dashed rule between the two halves. */
ruleColor?: string;
/** Focus ring. */
accent?: string;
/** Content width before the gutters. */
maxWidth?: string;
/** Added to the root. */
class?: string;
/** Root element id. */
id?: string;
}
/*
* Nothing about the business is assumed: brand, links, copyright and icon
* links all default to nothing, and a half with nothing in it is not drawn.
* Pass nothing at all and the footer renders nothing. The copyright is never
* generated — pass the line you want.
*/
const {
brand,
brandHref = "/",
logoSrc,
links = [],
navLabel = "Footer",
copyright,
social = [],
background = "#ffffff",
ink = "#0a0a0a",
linkColor = "#525252",
linkHoverColor = "#171717",
mutedColor = "#737373",
borderColor = "#f5f5f5",
ruleColor = "rgb(0 0 0 / 0.2)",
accent = "#0a0a0a",
maxWidth = "1280px",
class: className,
id,
} = Astro.props;
/*
* Only schemes that navigate. A footer is copied into other people's projects
* where the hrefs often come from a CMS record rather than from the person
* writing the template.
*/
const SAFE_SCHEME = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (value?: string) =>
value && SAFE_SCHEME.test(value.trim()) ? value.trim() : undefined;
const navLinks = links
.map((link) => ({ ...link, href: safeHref(link.href) }))
.filter((link): link is FooterLink => Boolean(link.href && link.label));
const socialLinks = social
.map((item) => ({ ...item, href: safeHref(item.href) }))
.filter((item): item is FooterSocialLink => Boolean(item.href && item.label));
const brandLink = safeHref(brandHref);
const logo = safeHref(logoSrc);
/*
* The brand block renders only when asked for: a brand string (`""` is the
* documented way to show the generic built-in mark alone) or a logo. Omitted,
* there is no brand block at all and no name is invented.
*/
const hasBrand = typeof brand === "string" || Boolean(logo);
const hasTop = hasBrand || navLinks.length > 0;
const hasBottom = Boolean(copyright || socialLinks.length > 0);
/*
* Inlined rather than fetched, so the footer makes no request of its own.
* Drawn on a 24 grid at stroke 2, and deliberately generic: this library does
* not ship other companies' trademarks, so there are no network logos here.
* Copy this file into your project and add to the map to use your own.
*/
const GLYPHS: Record<string, string[]> = {
mail: [
"M3 7.5A2.5 2.5 0 0 1 5.5 5h13A2.5 2.5 0 0 1 21 7.5v9a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 16.5z",
"m3.5 7 8.5 6 8.5-6",
],
link: [
"M10.5 13.5a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1.5 1.5",
"M13.5 10.5a4 4 0 0 0-5.66 0l-3 3a4 4 0 1 0 5.66 5.66l1.5-1.5",
],
rss: ["M5 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2z", "M5 12a7 7 0 0 1 7 7", "M5 5a14 14 0 0 1 14 14"],
chat: ["M21 11.5a8 8 0 0 1-8 8H8l-5 3 1.2-4.3A8 8 0 0 1 13 3.5a8 8 0 0 1 8 8z"],
phone: [
"M6.5 3h3l1.5 4-2 1.5a12 12 0 0 0 5.5 5.5L16 12l4 1.5v3a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 4.5 5.2 2 2 0 0 1 6.5 3z",
],
pin: ["M19 10.5c0 5-7 11-7 11s-7-6-7-11a7 7 0 0 1 14 0z", "M12 13a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"],
};
const style = [
`--clf-bg:${background}`,
`--clf-ink:${ink}`,
`--clf-link:${linkColor}`,
`--clf-link-hover:${linkHoverColor}`,
`--clf-muted:${mutedColor}`,
`--clf-border:${borderColor}`,
`--clf-rule:${ruleColor}`,
`--clf-accent:${accent}`,
`--clf-max:${maxWidth}`,
].join(";");
---
{(hasTop || hasBottom) && (
<footer
class:list={["centered-logo-footer", className]}
id={id}
style={style}
data-centered-logo-footer
>
<div class="clf-inner">
{hasTop && (
<div class="clf-top">
{hasBrand && (
<a class="clf-brand" href={brandLink}>
{logo ? (
<img class="clf-mark" src={logo} alt="" width="60" height="60" loading="lazy" decoding="async" />
) : (
/*
An original mark, drawn here rather than shipped as a file,
so the component installs as one .astro and still renders.
*/
<svg class="clf-mark" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<circle cx="16" cy="16" r="13" stroke="currentColor" stroke-width="3"></circle>
<circle cx="16" cy="16" r="4.5" fill="currentColor"></circle>
</svg>
)}
{brand && <span class="clf-wordmark">{brand}</span>}
</a>
)}
{navLinks.length > 0 && (
<nav class="clf-nav" aria-label={navLabel}>
<ul class="clf-links">
{navLinks.map((link) => (
<li>
<a class="clf-link" href={link.href}>{link.label}</a>
</li>
))}
</ul>
</nav>
)}
</div>
)}
{/*
The rule. Dashes come from a repeating gradient rather than
`border-style: dashed`, because a dashed border cannot control dash
length, and the reference's is 2.5px on, 2.5px off. The fade at both
ends is one mask; the reference stacks three and composites them with
`exclude`, which is the same picture through more machinery.
*/}
{hasTop && hasBottom && <div class="clf-rule" aria-hidden="true"></div>}
{hasBottom && (
<div class="clf-bottom">
{copyright && <p class="clf-copy">{copyright}</p>}
{socialLinks.length > 0 && (
<ul class="clf-social">
{socialLinks.map((item) => (
<li>
<a class="clf-social-link" href={item.href}>
<svg
class="clf-social-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
{(GLYPHS[item.icon] ?? GLYPHS.link).map((d) => <path d={d}></path>)}
</svg>
{/* The whole accessible name. An icon link with no text is
an unnamed link, which is what the reference ships. */}
<span class="clf-a11y">{item.label}</span>
</a>
</li>
))}
</ul>
)}
</div>
)}
</div>
</footer>
)}
<style>
.centered-logo-footer {
--clf-pad-block: 80px;
--clf-pad-inline: 32px;
--clf-gutter: 32px;
/* Matches the gutter wherever the gutter exists, so the rule always
reaches the container edge and never past it. */
--clf-rule-bleed: 0px;
--clf-gap: 16px;
--clf-mark: 30px;
--clf-icon: 24px;
--clf-text: 0.875rem;
--clf-line: 1.4286;
--clf-duration: 150ms;
--clf-ease: cubic-bezier(0.4, 0, 0.2, 1);
box-sizing: border-box;
display: block;
inline-size: 100%;
/* The rule is masked and can reach past the content box; nothing should
ever be able to widen the page from in here. */
overflow: hidden;
padding: var(--clf-pad-block) var(--clf-pad-inline);
border-block-start: 1px solid var(--clf-border);
background: var(--clf-bg);
color: var(--clf-ink);
/* A system stack on purpose: a component in this library fetches
nothing, so it can never be waiting on a webfont. */
font-family:
system-ui,
-apple-system,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif;
}
.centered-logo-footer *,
.centered-logo-footer *::before,
.centered-logo-footer *::after {
box-sizing: border-box;
}
/*
* A scoped reset, and it earns its place. Everything below sets type on
* classes, which beats inheritance — but it does not beat a host rule
* aimed at the bare element, and a footer lands at the bottom of pages
* that style `ul`, `li`, `p` and `a` globally more often than not.
*
* `:where()` keeps this at the specificity of the root alone, so the rules
* underneath still win on source order while a host's element selector
* loses. The `:global()` is load-bearing: Astro cannot put its scope
* attribute inside `:where()`, so without it the compiler emits a second
* attribute beside it and the whole thing outranks the class rules.
*/
.centered-logo-footer :global(:where(p, ul, li, a, span, nav, img, svg)) {
margin: 0;
padding: 0;
border: 0;
font: inherit;
color: inherit;
text-decoration: none;
list-style: none;
}
.clf-inner {
max-inline-size: var(--clf-max);
margin-inline: auto;
}
/* ---- the centred half -------------------------------------------- */
.clf-top {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.clf-brand {
display: inline-flex;
align-items: center;
gap: 8px;
margin-block-end: var(--clf-gap);
padding: 4px 8px;
border-radius: 8px;
color: var(--clf-ink);
}
.clf-mark {
flex: 0 0 auto;
display: block;
inline-size: var(--clf-mark);
block-size: var(--clf-mark);
object-fit: contain;
}
.clf-wordmark {
font-size: var(--clf-text);
line-height: var(--clf-line);
font-weight: 500;
color: var(--clf-ink);
}
.clf-links {
display: flex;
/*
* Mobile first, and that is not a style preference — it is what makes
* the breakpoint land where the reference's lands. Written the other
* way round as `max-width: 639px`, the query misses a viewport that
* reports 639.2px, where the reference's `min-width: 640px` correctly
* does not apply: the row stayed horizontal for that fractional pixel
* while the reference had already stacked.
*/
flex-direction: column;
flex-wrap: wrap;
align-items: center;
justify-content: center;
text-align: center;
gap: var(--clf-gap);
}
/*
* The <li> is a flex container so the link is a block inside it. As an
* inline-block it sits on a baseline, and the line box reserves descender
* space under it — the row measured 21.6px against the reference's 20,
* and the footer carried the extra 1.6px all the way to its full height.
*/
.clf-links > li {
display: flex;
}
.clf-link {
display: block;
/*
* Padding for the hit area, negative margin of exactly the same size
* to give it back. The margin box is therefore the text box: the row
* measures what the reference's measures, gap to gap, while the thing
* you can actually hit is 12px taller and 16px wider than the word.
* Without the second half the row grew 4px and the footer with it.
*/
padding: 6px 8px;
margin: -6px -8px;
border-radius: 6px;
font-size: var(--clf-text);
line-height: var(--clf-line);
color: var(--clf-link);
transition: color var(--clf-duration) var(--clf-ease);
}
/*
* The reference has no hover at all. Its class is `hover:text-text-
* neutral-800` — a doubled `text-` that Tailwind never generates — so the
* rule is simply absent and the links sit at one colour in every state.
* Measured rest and hover on the live page: both oklch(0.439 0 0).
*/
.clf-link:hover {
color: var(--clf-link-hover);
}
/* ---- the rule ------------------------------------------------------ */
.clf-rule {
block-size: 1px;
/*
* Out to the container edges, past the inner gutter, which is where
* the reference's sits: it escapes its own padding with a calc width
* and is then capped by the same max-width as everything else. Here
* it is a negative margin of one gutter, which needs no cap because
* it never exceeds the container in the first place.
*/
margin-inline: calc(var(--clf-rule-bleed) * -1);
inline-size: calc(100% + var(--clf-rule-bleed) * 2);
margin-block-start: 32px;
background-image: repeating-linear-gradient(
to right,
var(--clf-rule) 0 2.5px,
transparent 2.5px 5px
);
/* Fades out before either end rather than stopping dead. */
-webkit-mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
mask-image: linear-gradient(to right, transparent, #000 10%, #000 90%, transparent);
}
/* ---- the bottom row ------------------------------------------------ */
.clf-bottom {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
/*
* No gap while stacked: the copyright below carries its own 32px, and
* a flex gap on top of it put 48px between the two halves where the
* reference has 32 — 16px that the footer then carried as extra
* height at every width under the breakpoint.
*/
margin-block-start: 32px;
}
.clf-copy {
/* Stacked, the copyright needs air under it before the icon row. */
margin-block-end: 32px;
}
.clf-copy {
font-size: var(--clf-text);
line-height: var(--clf-line);
color: var(--clf-muted);
}
.clf-social {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--clf-gap);
}
.clf-social-link {
position: relative;
display: grid;
place-items: center;
/* 24px of glyph in a 40px hit area. The reference's icon links are
exactly 24px square, which is under every touch-target minimum
there is; the box grows, the glyph does not. */
inline-size: 40px;
block-size: 40px;
margin: -8px;
border-radius: 8px;
color: var(--clf-muted);
transition: color var(--clf-duration) var(--clf-ease);
}
.clf-social-link:hover {
color: var(--clf-link-hover);
}
.clf-social-icon {
inline-size: var(--clf-icon);
block-size: var(--clf-icon);
}
/* ---- focus --------------------------------------------------------- */
.clf-brand:focus-visible,
.clf-link:focus-visible,
.clf-social-link:focus-visible {
outline: 2px solid var(--clf-accent);
outline-offset: 2px;
}
/* Read, not seen. */
.clf-a11y {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* ---- responsive ---------------------------------------------------- */
/*
* The reference adds its inner gutter at 768 and stacks at 640. Both are
* reproduced; the widths between them are where a seven-link row is
* tightest, and it wraps rather than overflowing.
*/
@media (min-width: 768px) {
.centered-logo-footer {
--clf-rule-bleed: var(--clf-gutter);
}
.clf-inner {
padding-inline: var(--clf-gutter);
}
}
/*
* The reference's `sm:` — exactly, so the two change shape at the same
* pixel. Below it the links stack and the bottom row stacks with them.
*
* One divergence inside the stacked layout, and it is deliberate: the
* links are centred here. The reference's column has no cross-axis
* alignment, so its list box shrinks to its widest item and every shorter
* link left-aligns inside it — a ragged column sitting under a centred
* logo and above a centred copyright line.
*/
@media (min-width: 640px) {
.clf-links {
flex-direction: row;
}
.clf-bottom {
flex-direction: row;
/* Side by side the margin is gone, so a gap is what keeps a long
copyright off the icons. Invisible until they would collide. */
gap: var(--clf-gap);
}
.clf-copy {
margin-block-end: 0;
}
}
/* ---- preferences --------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
/* One token, because every transition in the file resolves through it.
Nothing rests at zero opacity or zero size, so removing the motion
leaves the footer correct rather than blank. */
.centered-logo-footer {
--clf-duration: 0.01ms;
}
}
@media (forced-colors: active) {
/* The rule is a background image, which does not survive here, and the
top hairline is a near-white that may not either. */
.centered-logo-footer {
border-block-start-color: CanvasText;
}
.clf-rule {
background-image: none;
border-block-start: 1px dashed CanvasText;
}
}
</style>
===== END src/components/CenteredLogoFooter.astro =====
===== FILE (supporting): src/components/CenteredLogoFooter.md =====
# Centered logo footer
A site footer that stacks a brand mark over one row of links, a dashed
hairline, and a bottom row carrying the copyright opposite a set of small
icon links.
Catalog page: `/components/centered-logo-footer`
## Purpose
The end of a marketing page, where the job is to be quietly complete rather
than to be looked at: the brand once more, the handful of links people
actually go looking for in a footer, and the legal line.
Use something else if the footer has to carry four columns of sitemap. This
one is a single row by design — past nine or ten links it wraps into a block
of text and stops reading as navigation.
## Files required
**`CenteredLogoFooter.astro`** — one file, the whole component.
- No imports at all, not even a type.
- No npm packages, no icon library, no React, no Tailwind, no CSS reset.
- No global stylesheet. Every colour, size and duration is declared on the
component's own root element as a `--clf-*` custom property.
- No webfonts, and **no images**. The brand mark and every icon are inline
SVG, so the footer makes **no network request of its own** — there is
nothing to copy into `public/` for it to render.
- The only assumption about the host is that Astro compiles `.astro` files.
`CenteredLogoFooter.md` — this file. Documentation, not required to run.
## Basic usage
```astro
---
import CenteredLogoFooter from "../components/CenteredLogoFooter.astro";
---
<CenteredLogoFooter
brand="Fieldnote"
brandHref="/"
links={[
{ label: "Work", href: "/work" },
{ label: "Studio", href: "/studio" },
{ label: "Journal", href: "/journal" },
{ label: "Privacy", href: "/privacy" },
]}
copyright="© 2026 Fieldnote Studio"
social={[
{ label: "Email", href: "mailto:studio@example.com", icon: "mail" },
{ label: "Journal feed", href: "/rss.xml", icon: "rss" },
]}
/>
```
No client directive, no wrapper, no init call — the component ships **no
script at all**.
It goes at the end of the layout, after `<slot />` and outside `<main>`. It
paints its own surface edge to edge, so the page behind it does not need to
match.
## Props
Nothing about the business is assumed. Brand, links, copyright and icon links
default to empty, and a part with nothing in it is not drawn: no brand means
no brand block, no copyright means no copyright line, and the rule appears
only when there is something both above and below it. Pass nothing at all and
the component renders nothing. The "Fieldnote" studio in the catalog preview
is demo data passed from the catalog registry; it does not travel with the
file.
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `brand` | `string?` | — | The wordmark beside the mark. Omitted or `""` with a `logoSrc` renders the image alone; with neither, the brand block is not drawn. |
| `brandHref` | `string?` | `"/"` | Where the brand links. |
| `logoSrc` | `string?` | — | An image to use instead of the built-in mark. Square; anything else is letterboxed into the same box. |
| `links` | `FooterLink[]?` | `[]` | The row of links. |
| `navLabel` | `string?` | `"Footer"` | Accessible name for the row, since a page may have several navs. |
| `copyright` | `string?` | — | The line under the rule. Never generated; omitted hides it. |
| `social` | `FooterSocialLink[]?` | `[]` | Icon links at the other end of the bottom row. |
| `background` | `string?` | `#ffffff` | The surface it paints, edge to edge. |
| `ink` | `string?` | `#0a0a0a` | Wordmark and brand mark. |
| `linkColor` | `string?` | `#525252` | The link row at rest. |
| `linkHoverColor` | `string?` | `#171717` | The link row under a pointer or focus. |
| `mutedColor` | `string?` | `#737373` | Copyright and icon links. |
| `borderColor` | `string?` | `#f5f5f5` | The hairline across the top. |
| `ruleColor` | `string?` | `rgb(0 0 0 / 0.2)` | The dashed rule between the halves. |
| `accent` | `string?` | `#0a0a0a` | Focus ring. |
| `maxWidth` | `string?` | `1280px` | Content width before the gutters. |
| `class` | `string?` | — | Extra class on the root. |
| `id` | `string?` | — | Element id. |
### One link
| Field | Type | What it does |
| :--- | :--- | :--- |
| `label` | `string` | The visible word. |
| `href` | `string` | Where it goes. Anything that is not `http:`, `https:`, `mailto:`, `tel:` or a relative path is **dropped**, not rendered. |
### One icon link
| Field | Type | What it does |
| :--- | :--- | :--- |
| `label` | `string` | The accessible name. Not shown, and it is the **only** thing a screen reader gets, so it has to stand alone — `"Email"`, not `"icon"`. |
| `href` | `string` | Same scheme rules as above. |
| `icon` | `"mail" \| "link" \| "rss" \| "chat" \| "phone" \| "pin"` | Which glyph. |
```ts
import type { FooterLink, FooterSocialLink } from "../components/CenteredLogoFooter.astro";
```
### A dark footer
Every colour is a prop, so the dark version is seven of them:
```astro
<CenteredLogoFooter
background="#0b0d10"
ink="#ffffff"
linkColor="#a1a1aa"
linkHoverColor="#ffffff"
mutedColor="#8b8f98"
borderColor="rgb(255 255 255 / 0.1)"
ruleColor="rgb(255 255 255 / 0.25)"
accent="#ffffff"
/>
```
There is no `prefers-color-scheme` rule in the file, on purpose: no component
in this library ships one, because a component that picks its own theme
fights the page it lands in. Drive the props from whatever the host already
uses to decide.
## Why there are no network logos
The icon set is `mail`, `link`, `rss`, `chat`, `phone` and `pin` — all
generic. There is no Twitter bird, no GitHub cat, no Instagram glyph, and
that is deliberate rather than an omission: those are trademarks belonging to
other companies, and this library does not redistribute them.
The glyphs live in a `GLYPHS` map near the top of the file. Once the
component is in your project it is your file — add the paths you need:
```ts
const GLYPHS: Record<string, string[]> = {
mail: [ /* ... */ ],
yourNetwork: ["M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z"],
};
```
and widen the `icon` union to match.
## Behaviour
There is none to speak of, and that is the point.
| Input | What happens |
| :--- | :--- |
| Click anything | It is a real link. It navigates. |
| <kbd>Tab</kbd> | Walks the brand, then each link in order, then each icon link. |
| Hover or focus a link | Colour moves from `linkColor` to `linkHoverColor` over 150ms. |
### Hit areas
Every link is padded well past its text and then given back the same amount
in negative margin, so the thing you can hit is larger than the word while
the row still measures exactly what it looks like it measures. The icon links
are 40×40 around a 24px glyph by the same trick.
## Accessibility
- It renders a real `<footer>`. That is a `contentinfo` landmark **only when
it is not nested inside another sectioning element** — keep it at the top
level of the body, not inside `<main>`.
- The link row is a `<nav>` with an accessible name, so a screen reader
listing the page's navigations can tell it from the header's.
- **Every icon link has a name.** The label is in the markup, visually
hidden. An icon link without one is an unnamed link, which is what the
reference ships: five `<a>` elements whose entire content is an
`aria-hidden` SVG.
- The brand mark is decorative — the wordmark beside it is the accessible
name. With a `logoSrc` and no `brand` the link has no text name, so give
it a wordmark or wrap it yourself.
- Focus ring is the accent at 2px with 2px of offset, on all three kinds of
link.
- Under `forced-colors: active` the dashed rule is redrawn as a real dashed
border, because it is a background image and those do not survive.
## Reduced motion
Under `prefers-reduced-motion: reduce` the one duration token collapses to
`0.01ms`, so the colour changes become cuts. Nothing rests at zero opacity or
zero size, so the footer is correct rather than blank.
## Without JavaScript
Identical. The component ships no script, so there is nothing to degrade —
every link works, the layout is CSS, and the rendered HTML is the whole
component.
## Content length
- **Brand** — up to about 14 characters before it crowds the mark.
- **Links** — four to nine. The row wraps rather than overflowing, but a
wrapped footer row reads as a paragraph.
- **Copyright** — one line. At desktop it shares the row with the icons.
- **Icon links** — up to six. Past that the bottom row gets busier than the
thing it sits under.
## Sizing
Full width, painting its own background edge to edge, with content capped at
`maxWidth` and centred. `80px` of block padding, `32px` of inline padding,
plus another `32px` of inner gutter at 768px and up.
Retune from CSS without touching the file:
```css
.my-page .centered-logo-footer {
--clf-pad-block: 56px;
--clf-gap: 24px;
--clf-duration: 0ms;
}
```
## Differences from the reference
Rebuilt from a live page, measured rather than copied. The rendered box
matches at every width tested: 323.8px tall above the breakpoint and 591.8px
below it, flipping at the same pixel, with the same 80/32 padding, the same
1280px cap, the same 1px rule at 2.5px on and 2.5px off, and the same 14px
type at 16px gaps. What differs:
- **The copy, the brand and the mark are original.** Nothing of the
reference's wording or artwork is reproduced.
- **No third-party logos**, for the reason given above.
- **The links have a hover state.** The reference's do not: its class is
`hover:text-text-neutral-800`, a doubled `text-` that Tailwind never
generates, so the rule is simply absent. Measured on the live page — rest
and hover are both `oklch(0.439 0 0)`.
- **The icon links have accessible names.** The reference's have none.
- **The icon links are 40px targets**, not 24px.
- **The stacked link column is centred.** The reference's column has no
cross-axis alignment, so its list box shrinks to its widest item and every
shorter link left-aligns inside it — a ragged column under a centred logo.
- **The rule is one mask, not three.** The reference stacks three gradient
masks and composites them with `exclude` to fade both ends; one
`linear-gradient` does the same thing.
- **The breakpoint is written mobile-first**, which is not a style
preference: as `max-width: 639px` it missed a viewport reporting 639.2px,
where the reference's `min-width: 640px` correctly did not apply.
- The reference sets its text in Inter, a webfont. This uses the system
stack, because a component in this library fetches nothing.
===== END src/components/CenteredLogoFooter.md =====
--- VERIFIED IN THE COMPONENT LIBRARY ---
Production-ready in the library.
Verified 2026-09-19 at 1440, 1280, 1024, 834, 768, 640, 639, 480, 390, 320px.
Passed: full-page context, keyboard, no javascript, reduced motion, two instances, no overflow, declarations corroborated, lifecycle.
Library source fingerprint: sha256:a6b34539068d9f136ac551d1c7d2c9397bcec36b35fa7de472a4d4add34a863a — MATCHES 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.