The 2 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: "Tag title banner"
Library page: https://astro.baysixmedia.com/components/tag-title-banner/
Files: 2 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/TagTitleBanner.astro =====
---
/**
* TagTitleBanner
*
* An inner-page banner built as a split: the breadcrumb and a heavy
* uppercase title with a stamped tag on a navy field, and — when a photo is
* given — a full-height photo panel meeting it at an accent seam. Below
* 700px of its own width the photo becomes a strip that closes the banner.
* Leaves room at the top for a fixed pill header.
*
* Sizes follow the component's own width (container queries), so it holds
* the same composition in a sidebar layout or a narrow column. The title
* rises in on load; the breadcrumb follows. Both end at rest and are off
* under reduced motion.
*
* Reads the --tw-* tokens from any ancestor, with fallbacks.
*/
export interface TagTitleCrumb {
label: string;
/** Omit on the last crumb, the current page. */
href?: string;
}
export interface TagTitleBannerProps {
title?: string;
/** The orange box after the title. `""` drops it. */
tag?: string;
breadcrumbs?: TagTitleCrumb[];
/** Background photo. `src: ""` renders the navy band alone. */
background?: { src?: string };
animate?: boolean;
fontStylesheet?: string;
class?: string;
id?: string;
}
interface Props extends TagTitleBannerProps {}
const {
title = "",
tag = "",
breadcrumbs: rawCrumbs = [],
background: bgProp,
animate = true,
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;
/* No photo by default: the navy band renders alone. No title, no banner. */
const background = { src: "", ...(bgProp ?? {}) };
const breadcrumbs = rawCrumbs.filter((c) => c?.label);
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
---
{title && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["ttb", className]} id={id} data-animate={animate ? "" : undefined} data-media={background.src ? "" : undefined}>
<div class="ttb-grid">
<div class="ttb-inner">
{
breadcrumbs.length > 0 && (
<nav class="ttb-crumbs" aria-label="Breadcrumb">
<ol>
{breadcrumbs.map((c, i) => (
<li>
{c.href && i < breadcrumbs.length - 1 ? (
<a href={safeHref(c.href)}>{c.label}</a>
) : (
<span aria-current={i === breadcrumbs.length - 1 ? "page" : undefined}>{c.label}</span>
)}
</li>
))}
</ol>
</nav>
)
}
<h1 class="ttb-title">
<span class="ttb-word">{title}</span>
{tag && <span class="ttb-tag">{tag}</span>}
</h1>
</div>
{
background.src && (
<div class="ttb-media" aria-hidden="true">
<img src={background.src} alt="" width="1920" height="600" fetchpriority="high" decoding="async" />
</div>
)
}
</div>
</section>
</>
)}
<style>
.ttb {
--ttb-navy: var(--tw-navy, #0b4654);
--ttb-accent: var(--tw-accent, #ff6d45);
--ttb-display: var(--tw-display, Outfit, ui-sans-serif, system-ui, sans-serif);
--ttb-body: var(--tw-body, "Noto Sans", ui-sans-serif, system-ui, sans-serif);
--ttb-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
--ttb-seam: 6px;
container-type: inline-size;
position: relative;
isolation: isolate;
overflow: hidden;
background: var(--ttb-navy);
color: #fff;
font-family: var(--ttb-body);
/* The closing edge: the banner ends on a line, never on open colour. */
border-bottom: var(--ttb-seam) solid var(--ttb-accent);
}
.ttb *,
.ttb *::before,
.ttb *::after {
box-sizing: border-box;
}
.ttb-grid {
display: grid;
}
/* Step 39.2: --tw-grid-max / --tw-grid-gutter put the title on a site grid; the fallbacks are the original 1440 / 64. */
.ttb-inner {
--ttb-max: var(--tw-grid-max, 1440px);
--ttb-gutter: var(--tw-grid-gutter, 64px);
}
.ttb-inner {
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 18px;
min-width: 0;
padding: 124px 20px 40px;
}
/* ---- breadcrumb: wayfinding first, above the title ---- */
.ttb-crumbs ol {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 2px;
margin: 0;
padding: 0;
list-style: none;
font: 500 15px/1.4 var(--ttb-body);
}
.ttb-crumbs li {
display: flex;
align-items: center;
min-width: 0;
}
.ttb-crumbs li + li::before {
content: "/";
margin: 0 8px 0 6px;
color: rgb(255 255 255 / 0.4);
}
.ttb-crumbs a,
.ttb-crumbs span {
display: inline-block;
padding: 5px 0;
overflow-wrap: anywhere;
}
.ttb-crumbs a {
color: rgb(255 255 255 / 0.74);
text-decoration: underline;
text-decoration-color: rgb(255 255 255 / 0);
text-underline-offset: 4px;
transition: color 0.2s var(--ttb-ease), text-decoration-color 0.2s var(--ttb-ease);
}
.ttb-crumbs a:hover {
color: #fff;
text-decoration-color: rgb(255 255 255 / 0.6);
}
.ttb-crumbs a:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
border-radius: 2px;
}
.ttb-crumbs [aria-current] {
color: #fff;
font-weight: 600;
}
/* ---- title: one lockup, the tag stamped onto the end of it ---- */
.ttb-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.14em 0.2em;
margin: 0;
max-width: 16ch;
font: 700 clamp(40px, 11.5cqi, 60px) / 0.98 var(--ttb-display);
letter-spacing: -0.035em;
text-transform: uppercase;
text-wrap: balance;
overflow-wrap: anywhere;
}
.ttb-tag {
display: inline-block;
padding: 0.1em 0.26em 0.06em;
border: 2px solid transparent;
border-radius: 4px;
background: var(--ttb-accent);
color: var(--ttb-navy);
font-size: 0.62em;
line-height: 1;
letter-spacing: -0.02em;
white-space: nowrap;
}
/* ---- photo ---- */
.ttb-media {
position: relative;
min-height: 168px;
border-top: var(--ttb-seam) solid var(--ttb-accent);
}
.ttb-media img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: 70% center;
}
/* A light navy grade so a busy photo sits in the palette instead of fighting it. */
.ttb-media::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgb(11 70 84 / 0.45), rgb(11 70 84 / 0.12) 55%, rgb(11 70 84 / 0.05));
}
/* ---- tablet and up: side by side, the seam runs top to bottom ---- */
@container (min-width: 700px) {
.ttb[data-media] .ttb-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr);
}
.ttb-inner {
gap: 22px;
padding: 150px 40px 56px;
}
.ttb-title {
max-width: 14ch;
font-size: clamp(52px, 8.4cqi, 104px);
}
.ttb:not([data-media]) .ttb-title {
max-width: 20ch;
}
.ttb-media {
min-height: 0;
border-top: 0;
border-left: var(--ttb-seam) solid var(--ttb-accent);
}
}
@container (min-width: 1100px) {
.ttb-inner {
padding: 196px max(var(--ttb-gutter), calc((100cqi - var(--ttb-max)) / 2 + var(--ttb-gutter))) 72px;
}
.ttb[data-media] .ttb-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
}
}
/* ---- motion: rises in, ends at rest ---- */
@keyframes ttb-rise {
from {
opacity: 0;
transform: translateY(var(--ttb-from, 18px));
}
}
.ttb[data-animate] .ttb-title {
animation: ttb-rise 0.7s var(--ttb-ease) both;
}
.ttb[data-animate] .ttb-crumbs {
--ttb-from: 10px;
animation: ttb-rise 0.6s var(--ttb-ease) 0.12s both;
}
@media (prefers-reduced-motion: reduce) {
.ttb[data-animate] .ttb-title,
.ttb[data-animate] .ttb-crumbs {
animation: none;
}
.ttb-crumbs a {
transition: none;
}
}
@media (forced-colors: active) {
.ttb {
border-bottom-color: CanvasText;
}
.ttb-media,
.ttb-tag {
border-color: CanvasText;
}
}
</style>
===== END src/components/TagTitleBanner.astro =====
===== FILE (supporting): src/components/TagTitleBanner.md =====
# Tag title banner
An inner-page banner built as a split: the breadcrumb and a heavy uppercase
title with a stamped tag on a navy field, and — when a photo is given — a
full-height photo panel meeting it at an orange seam. Below 700px of its own
width the photo becomes a strip that closes the banner. An orange rule always
closes the bottom edge.
The same component serves every inner page — change `title`, `tag` and
`breadcrumbs`.
Catalog page: `/components/tag-title-banner`
## Files required
- **`TagTitleBanner.astro`** — the whole component. No script.
- `public/tag-title-banner/port.webp` — optional; the catalog demo passes it
as `background`.
- `TagTitleBanner.md` — this file.
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `title` | `string?` | `""` | Required in practice: with no title the banner renders nothing. |
| `tag` | `string?` | `""` | The stamped orange tag after the title (navy text). Empty drops it. |
| `breadcrumbs` | `{ label, href? }[]` | `[]` | Last crumb is the current page. Empty drops the trail; a crumb without `label` is dropped. |
| `background` | `{ src? }?` | none | The photo panel. No `src` → the navy field alone, one column. |
| `animate` | `boolean?` | `true` | |
| `fontStylesheet`, `class`, `id` | | | |
## Behaviour (measured)
- Layout follows the banner's own width (container queries): split from
700px, photo strip below it.
- Breadcrumb above the title, sentence case, "/" separators; links 74% white
→ white with an underline on hover, each at least 24px tall. The current
crumb is white and bold (orange on navy failed contrast).
- Title rises 18px and fades in on load; the breadcrumb follows 0.12s later.
CSS animations; off under reduced motion.
- Forced colours: the seam, closing rule and tag keep a visible border.
## Images and credits
| File | Size | Credit |
| :--- | :--- | :--- |
| `port.webp` | 1920 × 600 | Unsplash QzqpPPpWP6I, Unsplash License |
## Approximations
The reference banner photo carries the reference company's name on a
container; the stock port photo replaces it.
===== END src/components/TagTitleBanner.md =====
===== FILE (required): public/tag-title-banner/port.webp =====
[binary asset — download https://astro.baysixmedia.com/tag-title-banner/port.webp and save it to public/tag-title-banner/port.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: 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.