Component library
Back to catalog

Rule title banner

An inner-page title band that needs no photo: an eyebrow against a hairline, a large serif page title and an optional short introduction beside it, resting on a heavy accent rule.

bannerpage-titleinner-pagetypographicno-photo

Open preview in a new tab

Use this component

Three ways to get Rule title banner into one of your own projects. The first two carry the component's files with them, so nothing has to fetch this page.

Start here

Placement prompt

The whole thing: what to do, where it goes, what must not change, and every one of the component's 1 file appended underneath with the path each belongs at. Paste it into Claude Code inside the destination project — it fetches nothing and needs nothing else.

Read it first — the files are added underneath when you copy
Add Rule title banner from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/rule-title-banner/

Place it in:
[ENTER PAGE, FILE, OR ROUTE]

Position:
[ENTER WHERE IT SHOULD APPEAR]

Every file you need is appended below, each fenced with the path it belongs at. Write them there, relative to the project root. Do not import anything from the deployed library at runtime, and do not fetch the library page — the files below are the source of truth.

Adapt its demo content, links, colors, typography, and images to this project through documented props where possible. Preserve its responsive behavior, accessibility, interaction logic, reduced-motion behavior, and support for multiple instances. Avoid changing unrelated project code.

Test it at desktop, tablet, and mobile widths. Run the project's available check and build commands. Report the files changed and any differences from the library version. Do not commit or deploy.

Install kit

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: "Rule title banner"
Library page: https://astro.baysixmedia.com/components/rule-title-banner/
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/RuleTitleBanner.astro =====
---
/**
 * RuleTitleBanner
 *
 * An inner-page title band that needs no photograph: a small eyebrow set
 * against a hairline, a large serif page title, and — when there is one — a
 * short introduction beside it on wide screens, all resting on a heavy rule
 * in the accent colour. It names the page; it does not pitch.
 *
 * The title is the page's heading (h1 by default). With no title the
 * banner renders nothing. Eyebrow and introduction render only when given.
 *
 * Reads --rtb-* custom properties from any ancestor, with fallbacks.
 * Step 39: the faces come through --rtb-display (title; Fraunces by default) and --rtb-family (eyebrow and
 * introduction; Archivo), and the column through --rtb-grid-max / --rtb-grid-gutter (desktop), so a site
 * can put the title on its page grid and set its own type — with no input the banner is exactly as it was.
 */

export interface RuleTitleBannerProps {
	/** A short label above the title. */
	eyebrow?: string;
	/** The page title. */
	title?: string;
	/** One or two sentences introducing the page. */
	intro?: string;
	/** h1 when it carries the page title (the default). */
	headingLevel?: "h1" | "h2";
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends RuleTitleBannerProps {}

const {
	eyebrow = "",
	title = "",
	intro = "",
	headingLevel = "h1",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Archivo:wght@500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const hasTitle = title.trim() !== "";
---

{hasTitle && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{
	hasTitle && (
		<section class:list={["rtb", className]} id={id} data-has-intro={intro.trim() ? "" : undefined}>
			<div class="rtb-inner">
				{eyebrow.trim() && (
					<p class="rtb-eyebrow">
						<span>{eyebrow}</span>
					</p>
				)}
				<div class="rtb-body">
					<Heading class="rtb-title">{title}</Heading>
					{intro.trim() && <p class="rtb-intro">{intro}</p>}
				</div>
			</div>
		</section>
	)
}

<style>
	.rtb {
		--rtb-bg: var(--rtb-surface, #f5f1ea);
		--rtb-ink-color: var(--rtb-ink, #1b2420);
		--rtb-muted-color: var(--rtb-muted, #55605a);
		--rtb-accent-color: var(--rtb-accent, #2f5d4a);
		--rtb-hair: rgb(27 36 32 / 0.18);
		background: var(--rtb-bg);
		color: var(--rtb-ink-color);
		border-bottom: 6px solid var(--rtb-accent-color);
	}

	.rtb-inner {
		max-width: var(--rtb-grid-max, 1200px);
		margin-inline: auto;
		padding: clamp(40px, 7vw, 96px) clamp(20px, 5vw, 48px) clamp(36px, 5vw, 64px);
	}

	.rtb-eyebrow {
		display: flex;
		align-items: center;
		gap: 16px;
		margin: 0 0 clamp(20px, 3vw, 32px);
		font: 600 0.8125rem/1.3 var(--rtb-family, "Archivo", ui-sans-serif, system-ui, sans-serif);
		letter-spacing: 0.14em;
		text-transform: uppercase;
		color: var(--rtb-accent-color);
	}

	/* The hairline runs from the label to the edge of the column. */
	.rtb-eyebrow::after {
		content: "";
		flex: 1;
		height: 1px;
		background: var(--rtb-hair);
	}

	.rtb-body {
		display: grid;
		gap: clamp(16px, 3vw, 28px);
	}

	.rtb-title {
		margin: 0;
		max-width: 18ch;
		font-family: var(--rtb-display, "Fraunces", ui-serif, Georgia, "Times New Roman", serif);
		font-weight: 500;
		font-size: clamp(2.25rem, 1.4rem + 4.2vw, 4.75rem);
		line-height: 1.02;
		letter-spacing: -0.02em;
		overflow-wrap: break-word;
		hyphens: auto;
	}

	.rtb-intro {
		margin: 0;
		max-width: 46ch;
		font: 500 clamp(1rem, 0.95rem + 0.3vw, 1.1875rem) / 1.55 var(--rtb-family, "Archivo", ui-sans-serif, system-ui, sans-serif);
		color: var(--rtb-muted-color);
	}

	/* The site gutter applies on desktop only; at 960px and up the default clamp is already its 48px maximum. */
	@media (min-width: 960px) {
		.rtb-inner {
			padding-inline: var(--rtb-grid-gutter, clamp(20px, 5vw, 48px));
		}
	}

	@media (min-width: 900px) {
		/* With an introduction the title and the intro share the row, the intro settling on the baseline. */
		.rtb[data-has-intro] .rtb-body {
			grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
			align-items: end;
			column-gap: clamp(32px, 5vw, 72px);
		}
	}
</style>

===== END src/components/RuleTitleBanner.astro =====

===== FILE (supporting): src/components/RuleTitleBanner.md =====
# Rule title banner

An inner-page title band that needs no photograph: an eyebrow against a
hairline, a large serif page title, an optional short introduction beside it
on wide screens, all resting on a heavy accent rule. It names the page; it
does not pitch.

Catalog page: `/components/rule-title-banner`

## Files required

- **`RuleTitleBanner.astro`** — the whole component. No script, no images.
- `RuleTitleBanner.md` — this file.

## Props

| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `title` | `string?` | `""` | The page heading. With no title the banner renders nothing. |
| `eyebrow` | `string?` | `""` | Label above the title. Empty drops it and its hairline. |
| `intro` | `string?` | `""` | One or two sentences; beside the title from 900px. |
| `headingLevel` | `"h1" \| "h2"` | `"h1"` | |
| `fontStylesheet`, `class`, `id` | | | Loads Fraunces and Archivo by default. |

Theme with `--rtb-surface`, `--rtb-ink`, `--rtb-muted`, `--rtb-accent`.

===== END src/components/RuleTitleBanner.md =====

--- VERIFIED IN THE COMPONENT LIBRARY ---
Production-ready in the library.
Verified 2026-09-27 at 1440, 1280, 1024, 768, 640, 390, 360, 320px.
Passed: full-page context, keyboard, no javascript, reduced motion, two instances, no overflow, declarations corroborated, lifecycle.
Library source fingerprint: sha256:331de22333f67416e9dbee694cc4064f331d2e4b70306630059e279743fa20ec — 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.

Component reference

A fragment for a request you write yourself, e.g. “I want to add [paste] to Section 3 of my homepage.” It names the component, links its page, and fixes what must not change — the design build and its effects — while leaving branding and content open. It does not carry the code.

Read it first
“Rule title banner” from my Astro Component Library:
https://astro.baysixmedia.com/components/rule-title-banner/

Use the existing library component as the implementation source. Preserve its design structure, layout, proportions, spacing system, responsive behavior, visual effects, animations, transitions, hover states, interactions, and accessibility.

The copy, colors, typography, images, and other content may be adapted to match the destination website. Do not redesign, simplify, or reinterpret the component’s underlying design build or effects.

Component readiness

Judged on recorded evidence, not on how it looks in the frame above. A description of a component is not a result.

Interactivity
Static
Profile
core only
Installation files
1 required + 1 supporting
Verified
2026-09-27
Client navigation
Not applicable — ships no script
Source fingerprint
Matches the verified source
Production ready
Yes

Production-ready component

Every required check passed against the code below, and the recorded fingerprint still matches it.

Verified 2026-09-27 at 1440, 1280, 1024, 768, 640, 390, 360, 320px

  • Full-page contextpass
  • Keyboard — declared to contain nothing focusablenot-applicable
  • No JavaScriptpass
  • Reduced motion — declared to have no motionnot-applicable
  • Two instancespass
  • No overflowpass
  • Declarations corroboratedpass
  • Lifecycle — declared to ship no scriptnot-applicable
9 verification notes
  • 2026-09-27 (Step 39, quality/improvement/39/rule-title-banner.json): --rtb-grid-max / --rtb-grid-gutter (desktop) and --rtb-display / --rtb-family face hooks added; every fallback is the previous value and the default render is pixel-identical at 1440/768/390. Re-run in Chromium (tools/cdp.ts) on the built fixture at every width listed, with and without JavaScript: scrollWidth === clientWidth, no duplicate ids, two independent instances (one h1, one h2), no script, 0 focusable elements, 0 transitions or animations. The long-title cases were not re-run: the title's sizing, measure and wrapping rules are unchanged (only its font-family now reads --rtb-display first).
  • Run 2026-09-25 in Chromium against the dev server's generic verification fixture (/integration-test/component/<slug>), two instances per page. Chromium only; no WebKit or Gecko claim is made.
  • Overflow: scrollWidth === clientWidth at 320, 360, 390, 640, 768, 1024, 1280 and 1440.
  • Two instances: independent ids (seeded from the fixture's id), no duplicate ids on the page.
  • No JavaScript: identical render, because the component ships none (no script inside either root).
  • Content safety: omitted props render nothing, checked by src/lib/content-safety.test.ts.
  • Heading: the first instance renders exactly one h1 (the second is headingLevel h2); with no eyebrow and no intro the second instance renders neither and the title spans the row.
  • Long titles: an 81-character title wraps to 8 lines at 320px and 5 at 1440px without overflow; a 7-character title stays on one line.
  • Keyboard not-applicable, licensed by hasFocusableContent: false (0 focusable elements observed). Reduced motion not-applicable, licensed by hasMotion: false (0 transitions and 0 animations observed).

Open the verification fixture

src/components/library/RuleTitleBanner.astro

one fileNo imports, no companion files, no packages. Copy RuleTitleBanner.astro into any Astro project's components folder and render it.

---
/**
 * RuleTitleBanner
 *
 * An inner-page title band that needs no photograph: a small eyebrow set
 * against a hairline, a large serif page title, and — when there is one — a
 * short introduction beside it on wide screens, all resting on a heavy rule
 * in the accent colour. It names the page; it does not pitch.
 *
 * The title is the page's heading (h1 by default). With no title the
 * banner renders nothing. Eyebrow and introduction render only when given.
 *
 * Reads --rtb-* custom properties from any ancestor, with fallbacks.
 * Step 39: the faces come through --rtb-display (title; Fraunces by default) and --rtb-family (eyebrow and
 * introduction; Archivo), and the column through --rtb-grid-max / --rtb-grid-gutter (desktop), so a site
 * can put the title on its page grid and set its own type — with no input the banner is exactly as it was.
 */

export interface RuleTitleBannerProps {
	/** A short label above the title. */
	eyebrow?: string;
	/** The page title. */
	title?: string;
	/** One or two sentences introducing the page. */
	intro?: string;
	/** h1 when it carries the page title (the default). */
	headingLevel?: "h1" | "h2";
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends RuleTitleBannerProps {}

const {
	eyebrow = "",
	title = "",
	intro = "",
	headingLevel = "h1",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Archivo:wght@500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const hasTitle = title.trim() !== "";
---

{hasTitle && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{
	hasTitle && (
		<section class:list={["rtb", className]} id={id} data-has-intro={intro.trim() ? "" : undefined}>
			<div class="rtb-inner">
				{eyebrow.trim() && (
					<p class="rtb-eyebrow">
						<span>{eyebrow}</span>
					</p>
				)}
				<div class="rtb-body">
					<Heading class="rtb-title">{title}</Heading>
					{intro.trim() && <p class="rtb-intro">{intro}</p>}
				</div>
			</div>
		</section>
	)
}

<style>
	.rtb {
		--rtb-bg: var(--rtb-surface, #f5f1ea);
		--rtb-ink-color: var(--rtb-ink, #1b2420);
		--rtb-muted-color: var(--rtb-muted, #55605a);
		--rtb-accent-color: var(--rtb-accent, #2f5d4a);
		--rtb-hair: rgb(27 36 32 / 0.18);
		background: var(--rtb-bg);
		color: var(--rtb-ink-color);
		border-bottom: 6px solid var(--rtb-accent-color);
	}

	.rtb-inner {
		max-width: var(--rtb-grid-max, 1200px);
		margin-inline: auto;
		padding: clamp(40px, 7vw, 96px) clamp(20px, 5vw, 48px) clamp(36px, 5vw, 64px);
	}

	.rtb-eyebrow {
		display: flex;
		align-items: center;
		gap: 16px;
		margin: 0 0 clamp(20px, 3vw, 32px);
		font: 600 0.8125rem/1.3 var(--rtb-family, "Archivo", ui-sans-serif, system-ui, sans-serif);
		letter-spacing: 0.14em;
		text-transform: uppercase;
		color: var(--rtb-accent-color);
	}

	/* The hairline runs from the label to the edge of the column. */
	.rtb-eyebrow::after {
		content: "";
		flex: 1;
		height: 1px;
		background: var(--rtb-hair);
	}

	.rtb-body {
		display: grid;
		gap: clamp(16px, 3vw, 28px);
	}

	.rtb-title {
		margin: 0;
		max-width: 18ch;
		font-family: var(--rtb-display, "Fraunces", ui-serif, Georgia, "Times New Roman", serif);
		font-weight: 500;
		font-size: clamp(2.25rem, 1.4rem + 4.2vw, 4.75rem);
		line-height: 1.02;
		letter-spacing: -0.02em;
		overflow-wrap: break-word;
		hyphens: auto;
	}

	.rtb-intro {
		margin: 0;
		max-width: 46ch;
		font: 500 clamp(1rem, 0.95rem + 0.3vw, 1.1875rem) / 1.55 var(--rtb-family, "Archivo", ui-sans-serif, system-ui, sans-serif);
		color: var(--rtb-muted-color);
	}

	/* The site gutter applies on desktop only; at 960px and up the default clamp is already its 48px maximum. */
	@media (min-width: 960px) {
		.rtb-inner {
			padding-inline: var(--rtb-grid-gutter, clamp(20px, 5vw, 48px));
		}
	}

	@media (min-width: 900px) {
		/* With an introduction the title and the intro share the row, the intro settling on the baseline. */
		.rtb[data-has-intro] .rtb-body {
			grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
			align-items: end;
			column-gap: clamp(32px, 5vw, 72px);
		}
	}
</style>