component
A reusable, slot-rich title bar with flanking leading/trailing snippets and an optional nested heading + subheading, each with their own leading/trailing. Banner is built on top of Panel (surfaces, borders, padding, radius) so it can be used standalone as a section header, embedded inside a card or dialog, or dropped anywhere a “title with actions” pattern is needed.
A minimal banner with only a heading.
---
import Banner from "../Banner.astro";
---
<Banner heading="Project settings" />Add a subheading for a two-line title stack.
Manage workspace access, billing, and integrations
---
import Banner from "../Banner.astro";
---
<Banner
heading="Project settings"
subheading="Manage workspace access, billing, and integrations"
/>eyebrow adds a small kicker line above the heading — case-preserving and muted by default. It inherits panel-aware text color, so under emphasis="primary" it automatically flips to a contrast variant. Accepts the same string | { leading, content, trailing } shape as heading.
Whether the eyebrow reads as a quiet meta line, a label-style kicker, or an uppercase overline is a styling decision the consumer owns. Reach for passThrough.eyebrow.style (or write the text uppercased yourself) when you want a tighter, ALL-CAPS look:
<Banner
eyebrow="Wednesday"
heading="Dec 14"
passThrough={{
eyebrow: {
style: { textTransform: "uppercase", letterSpacing: "0.05em" },
},
}}
/>
Wednesday
Today
2:00 PM with the design team
Featured
Smoother, more dynamic transitions
Siri suggestions
@saffina28 on Instagram
Notification
Same auto-contrast cascade as heading and subheading.
---
import Banner from "../Banner.astro";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Bell, Calendar, Sparkles } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
<Banner eyebrow="Wednesday" heading="Dec 14" />
<Banner
eyebrow="Today"
heading="Project review"
subheading="2:00 PM with the design team"
>
<Stamp
slot="eyebrow-leading"
size="2xs"
shape="square"
feedback="neutral"
border="none"
>
<Calendar />
</Stamp>
</Banner>
<Banner
eyebrow="Featured"
heading="Animation framework"
subheading="Smoother, more dynamic transitions"
>
<span slot="eyebrow-trailing">
<Badge size="xs" feedback="primary">New</Badge>
</span>
</Banner>
<Banner
eyebrow="Siri suggestions"
heading="Add Safina"
subheading="@saffina28 on Instagram"
>
<Stamp
slot="eyebrow-leading"
size="2xs"
shape="square"
feedback="neutral"
border="none"
>
<Sparkles />
</Stamp>
</Banner>
<Banner
feedback="primary"
emphasis="primary"
background="surface.peak"
padding="md"
radius="md"
eyebrow="Notification"
heading="Eyebrow flips to contrast"
subheading="Same auto-contrast cascade as heading and subheading."
>
<Stamp
slot="eyebrow-leading"
size="2xs"
shape="square"
emphasis="adaptive"
border="none"
>
<Bell />
</Stamp>
</Banner>
</div>Banner’s slots compose cleanly with Stamp — use them at the top level (leading, trailing) or inside the heading itself via the { leading, content, trailing } shape.
12 new notifications
Stamps composed inline with the heading
---
import Banner from "../Banner.astro";
import Stamp from "@pindoba/astro-stamp";
import { stack } from "@pindoba/styled-system/patterns";
import { Bell, Check, Inbox, Star } from "@lucide/astro";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<Banner heading="Inbox" subheading="12 new notifications">
<Stamp slot="leading" background="surface.ground" shape="square" size="lg">
<Inbox />
</Stamp>
<Stamp
slot="trailing"
feedback="success"
emphasis="secondary"
shape="circle"
>
<Check />
</Stamp>
</Banner>
<Banner
heading="Featured item"
subheading="Stamps composed inline with the heading"
>
<Stamp
slot="heading-leading"
size="sm"
background="surface.ground"
feedback="warning"
>
<Star />
</Stamp>
<Stamp slot="heading-trailing" size="sm" feedback="danger" shape="circle">
<Bell />
</Stamp>
</Banner>
</div>Four sizes scale all three text rows together — the heading and subheading each step through a distinct token per size, and the eyebrow nudges up at lg. Padding/radius come from Panel and can be set independently.
Eyebrow
size="xs"
Eyebrow
size="sm"
Eyebrow
size="md"
Eyebrow
size="lg"
---
import Banner from "../Banner.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "lg", direction: "column" })}>
<Banner
size="xs"
eyebrow="Eyebrow"
heading="Extra small"
subheading='size="xs"'
/>
<Banner size="sm" eyebrow="Eyebrow" heading="Small" subheading='size="sm"' />
<Banner size="md" eyebrow="Eyebrow" heading="Medium" subheading='size="md"' />
<Banner size="lg" eyebrow="Eyebrow" heading="Large" subheading='size="lg"' />
</div>While size scales all three text rows together, headingTextStyle, eyebrowTextStyle, and subheadingTextStyle let each slot adopt a design-system textStyle token directly — e.g. a big tabular numeric.lg heading over a mono uppercase eyebrow label for KPI/stat cards. A token fully owns its slot’s typography (overriding the size-derived sizing and, for the heading, headingWeight); passThrough.<slot>.style still layers on top. Because these live on the shared base props, any component that extends Banner (such as Card’s header) inherits them.
Expenses · June 2026
Across 312 transactions
Revenue
---
import Banner from "../Banner.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
<!-- KPI / metric card header: a big tabular number for the heading and the
mono uppercase label for the eyebrow, straight from the design tokens. -->
<Banner
eyebrow="Expenses · June 2026"
eyebrowTextStyle="eyebrow"
heading="R$ 12,840.55"
headingTextStyle="numeric.lg"
subheading="Across 312 transactions"
subheadingTextStyle="body.sm"
/>
<!-- Tokens own typography independently of `size`: this heading stays
`numeric.md` regardless of the size variant driving padding/eyebrow. -->
<Banner
size="sm"
eyebrow="Revenue"
eyebrowTextStyle="eyebrow"
heading="R$ 48,200.00"
headingTextStyle="numeric.md"
/>
<!-- A heading-token swap with no eyebrow/subheading override. -->
<Banner heading="Section title" headingTextStyle="heading.lg" />
</div>The heading row renders as an <h3> by default — the right level for a card or
dialog title. When a banner is the page or section heading, set
headingLevel (1–6) to place it correctly in the document outline;
headingLevel={1} renders an <h1>. It is purely semantic: typography still
comes from size / headingWeight / headingTextStyle, so raising the level
never changes how the banner looks. The docs site’s own page masthead is a
Banner with headingLevel={1} and headingTextStyle="heading.2xl".
All layout lives under a single layout prop, keyed per slot (the same shape as passThrough), so you don’t reach for raw passThrough CSS just to nudge alignment or placement. layout.eyebrow, layout.heading, and layout.subheading each take an align (cross-axis alignment of that row’s items) and a trailing placement — "apart" (default) flushes the trailing element to the row’s right edge, while "together" lets it sit next to the content (useful for a small badge after a label, which otherwise drifts to the far edge). layout.content.align aligns the whole text block horizontally — "center" and "end" give you a centered or right-aligned banner. layout.root.align aligns the top-level leading/trailing slots against the whole text block — e.g. "start" top-aligns a leading icon over a multi-line body — and layout.leading.align / layout.trailing.align override it for a single slot (center a leading avatar while the root stays top-aligned). Per-row overrides stay independent, and passThrough.<slot>.style still layers on top for anything outside these axes.
Featured
The eyebrow badge sits next to the label.
Featured
The eyebrow badge flushes to the right edge.
With root align start, the icon hugs the top of a multi-line text block rather than centering against it.
root align start pins the trailing badge to the top, while leading align center keeps the icon centered on the text block.
Centered
The whole text block is centered horizontally.
Right aligned
The whole text block hugs the right edge.
---
import Banner from "../Banner.astro";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Info } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
{
/* eyebrow.trailing: "together" → the badge sits right next to the eyebrow text. */
}
<Banner
layout={{ eyebrow: { trailing: "together" } }}
eyebrow="Featured"
heading="Trailing together"
subheading="The eyebrow badge sits next to the label."
>
<span slot="eyebrow-trailing">
<Badge size="xs" feedback="primary">New</Badge>
</span>
</Banner>
{
/* eyebrow.trailing: "apart" (default) → the badge flushes to the right edge. */
}
<Banner
layout={{ eyebrow: { trailing: "apart" } }}
eyebrow="Featured"
heading="Trailing apart"
subheading="The eyebrow badge flushes to the right edge."
>
<span slot="eyebrow-trailing">
<Badge size="xs" feedback="primary">New</Badge>
</span>
</Banner>
{/* root.align="start" → the leading icon top-aligns to the text block. */}
<Banner
layout={{ root: { align: "start" } }}
heading="Top-aligned leading icon"
subheading="With root align start, the icon hugs the top of a multi-line text block rather than centering against it."
>
<Stamp
slot="leading"
size="md"
shape="square"
feedback="primary"
border="none"
>
<Info />
</Stamp>
</Banner>
{
/* leading/trailing align override root.align for a single slot: here
the root is top-aligned but the leading icon re-centers on its own. */
}
<Banner
layout={{ root: { align: "start" }, leading: { align: "center" } }}
heading="Per-slot self-alignment"
subheading="root align start pins the trailing badge to the top, while leading align center keeps the icon centered on the text block."
>
<Stamp
slot="leading"
size="md"
shape="square"
feedback="primary"
border="none"
>
<Info />
</Stamp>
<span slot="trailing">
<Badge size="xs" feedback="primary">New</Badge>
</span>
</Banner>
{/* content.align="center" → the whole text block is centered. */}
<Banner
layout={{ content: { align: "center" } }}
eyebrow="Centered"
heading="Centered banner"
subheading="The whole text block is centered horizontally."
/>
{/* content.align="end" → the whole text block hugs the right edge. */}
<Banner
layout={{ content: { align: "end" } }}
eyebrow="Right aligned"
heading="Right-aligned banner"
subheading="The whole text block hugs the right edge."
/>
</div>The top-level trailing slot always sits at the right edge (the text block takes the remaining width), and its content is centered inside the slot by default. When the trailing region is wider than its content — a fixed-width metadata column, or a truncating path — layout.trailing.justify positions that content along the slot’s main axis: "end" flushes it to the card’s right edge, "start" hugs the text block, "center" (default) centers it. This replaces reaching for passThrough.trailing.style.justifyContent. It composes with layout.trailing.align (the vertical/cross-axis knob), so { trailing: { align: "start", justify: "end" } } pins content to the top-right corner.
layout.trailing.justify: 'end' pins the path to the right edge — no passThrough needed.
justify: 'start' left-aligns the same content within its trailing region.
---
import Banner from "../Banner.astro";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
// A fixed-width metadata column that's wider than its text, so the
// `layout.trailing.justify` knob has room to position the content. Card's
// worktree rows use this to flush a truncating path to the right edge.
const metaClass = css({
width: "12rem",
fontFamily: "mono",
fontSize: "xs",
color: "panel.text.muted",
whiteSpace: "nowrap",
});
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
{/* justify "end" flushes the trailing content to the right edge. */}
<Banner
layout={{ trailing: { align: "start", justify: "end" } }}
heading="Flush right"
subheading="layout.trailing.justify: 'end' pins the path to the right edge — no passThrough needed."
>
<span slot="trailing" class={metaClass}>…/Projects/pindoba</span>
</Banner>
{/* justify "start" keeps it hugging the text block instead. */}
<Banner
layout={{ trailing: { align: "start", justify: "start" } }}
heading="Left of its column"
subheading="justify: 'start' left-aligns the same content within its trailing region."
>
<span slot="trailing" class={metaClass}>…/Projects/pindoba</span>
</Banner>
</div>By default the leading element (an icon or Stamp) is centered against the whole text block. layout.leading.span lets it flank just a pair of rows instead, which reads more balanced for changelog-style cards. The heading is always flanked; span controls which neighbour rows the element also reaches. "above" flanks the heading + the eyebrow above it and drops the subheading to a full-width row below; "below" flanks the heading + the subheading below it and lifts the eyebrow to a full-width row above. The trailing element rides the flanked row — pair it with layout.trailing.align: "start" to pin it top-right. layout.leading.span has no effect without a leading element.
Changelog · v2.4
June 3, 2026 · 4 min read
Changelog · v2.4
June 3, 2026 · 4 min read
Changelog · v2.4
June 3, 2026 · 4 min read
---
import Banner from "../Banner.astro";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Sparkles } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column", width: "100%" })}>
{/* all (default): the stamp flanks the whole text block. */}
<Banner
layout={{ leading: { span: "all" }, trailing: { align: "start" } }}
eyebrow="Changelog · v2.4"
heading="Design tokens, refreshed"
subheading="June 3, 2026 · 4 min read"
>
<Stamp slot="leading" size="lg" shape="square" feedback="primary">
<Sparkles />
</Stamp>
<Badge slot="trailing" size="xs" feedback="success">New</Badge>
</Banner>
{
/* above: stamp flanks the heading + eyebrow above it; subheading drops below. */
}
<Banner
layout={{
leading: { span: "above" },
trailing: { align: "start" },
}}
eyebrow="Changelog · v2.4"
heading="Design tokens, refreshed"
subheading="June 3, 2026 · 4 min read"
>
<Stamp slot="leading" size="lg" shape="square" feedback="primary">
<Sparkles />
</Stamp>
<Badge slot="trailing" size="xs" feedback="success">New</Badge>
</Banner>
{
/* below: stamp flanks the heading + subheading below it; eyebrow lifts on top. */
}
<Banner
layout={{
leading: { span: "below" },
trailing: { align: "start" },
}}
eyebrow="Changelog · v2.4"
heading="Design tokens, refreshed"
subheading="June 3, 2026 · 4 min read"
>
<Stamp slot="leading" size="lg" shape="square" feedback="primary">
<Sparkles />
</Stamp>
<Badge slot="trailing" size="xs" feedback="success">New</Badge>
</Banner>
</div>Banner inherits Panel’s feedback surfaces, useful when the banner itself is the component (not wrapped by a card or dialog).
Inherits panel surface from `feedback`
Inherits panel surface from `feedback`
Inherits panel surface from `feedback`
Inherits panel surface from `feedback`
---
import Banner from "../Banner.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<Banner
feedback="primary"
padding="md"
radius="lg"
heading="Primary"
subheading="Inherits panel surface from `feedback`"
/>
<Banner
feedback="success"
padding="md"
radius="lg"
heading="Success"
subheading="Inherits panel surface from `feedback`"
/>
<Banner
feedback="warning"
padding="md"
radius="lg"
heading="Warning"
subheading="Inherits panel surface from `feedback`"
/>
<Banner
feedback="danger"
padding="md"
radius="lg"
heading="Danger"
subheading="Inherits panel surface from `feedback`"
/>
</div>"transparent"Panel surface fill. Only meaningful when `feedback="neutral"`.
"none"Panel border ring strength. Only meaningful when `feedback="neutral"`.
No description yet.
No description yet.
Panel emphasis variant. Under `"primary"`, panel-aware text colors flip to their contrast variant.
No description yet.
Text style token applied to the eyebrow slot (e.g. `"eyebrow"`, `"label.sm"`). Overrides the `size`-derived typography for the eyebrow; `passThrough.eyebrow.style` still layers on top.
"neutral"Semantic surface color inherited from Panel. When set to anything other than `neutral`, banner owns its background via the feedback palette.
No description yet.
3Document outline level of the heading row — picks the rendered tag (`1` → `<h1>` … `6` → `<h6>`). Purely semantic: typography still comes from `size` / `headingWeight` / `headingTextStyle`, so raising a banner to the page's `<h1>` never changes how it looks. Set it whenever the banner is a real section or page heading rather than a card title.
Text style token applied to the heading slot (e.g. `"numeric.md"`, `"heading.lg"`). Overrides the `size`-derived typography for the heading (including `headingWeight`); only `passThrough.heading.style` layers on top.
semiboldHeading font weight. `"normal"` renders the heading as plain body text.
All layout configuration, keyed per slot (mirrors `passThrough`). Each slot owns its own object: - `root: { align }` — cross-axis alignment of the root flex (leading ↔ text block ↔ trailing). Default `"stretch"`. - `content: { align }` — horizontal alignment of the whole text block. Default `"start"`. - `leading: { align, span }` — leading element's cross-axis self-alignment (overrides `root.align`) and flank `span` (`"all"` | `"above"` | `"below"`, default `"all"`). - `trailing: { align, justify }` — trailing element's cross-axis self-alignment (overrides `root.align`) and horizontal `justify` of its own content (`"end"` flushes constrained content to the right edge; default `"center"`). - `eyebrow` / `heading` / `subheading: { align, trailing }` — per-row cross-axis `align` (default `"center"`) and trailing-element placement (`"apart"` at the right edge or `"together"` next to the text, default `"apart"`).
No description yet.
"none"Panel padding token. Defaults to none so banner reads flat inside cards/dialogs.
Per-slot style/attribute override bag. Each slot accepts a `style` (Panda style object) merged into its base styles and a `props` bag of extra HTML attributes.
"none"Panel radius token.
Panel radius token applied to the bottom corners only.
Panel radius token applied to the left corners only.
Panel radius token applied to the right corners only.
Panel radius token applied to the top corners only.
Panel drop-shadow token.
"md"Controls the heading font size. Padding and radius come from Panel and can be set independently.
No description yet.
Text style token applied to the subheading slot (e.g. `"body.sm"`, `"caption"`). Overrides the `size`-derived typography for the subheading; `passThrough.subheading.style` still layers on top.
No description yet.
falseFrosted-glass backdrop blur. Only meaningful when `feedback="neutral"`.
Plus all standard <div> HTML
attributes.