component

Banner

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.

Basic

A minimal banner with only a heading.

Project settings

---
import Banner from "../Banner.astro";
---

<Banner heading="Project settings" />

Heading + Subheading

Add a subheading for a two-line title stack.

Project settings

Manage workspace access, billing, and integrations

---
import Banner from "../Banner.astro";
---

<Banner
  heading="Project settings"
  subheading="Manage workspace access, billing, and integrations"
/>

Eyebrow

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

Dec 14

Today

Project review

2:00 PM with the design team

Featured

New

Animation framework

Smoother, more dynamic transitions

Siri suggestions

Add Safina

@saffina28 on Instagram

Notification

Eyebrow flips to contrast

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>

Composing with Stamp

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.

Inbox

12 new notifications

Featured item

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>

Sizes

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

Extra small

size="xs"

Eyebrow

Small

size="sm"

Eyebrow

Medium

size="md"

Eyebrow

Large

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>

Text styles

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

R$ 12,840.55

Across 312 transactions

Revenue

R$ 48,200.00

Section title

---
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>

Heading level

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 (16) 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".

Slot layout

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

New

Trailing together

The eyebrow badge sits next to the label.

Featured

New

Trailing apart

The eyebrow badge flushes to the right edge.

Top-aligned leading icon

With root align start, the icon hugs the top of a multi-line text block rather than centering against it.

Per-slot self-alignment

root align start pins the trailing badge to the top, while leading align center keeps the icon centered on the text block.

New

Centered

Centered banner

The whole text block is centered horizontally.

Right aligned

Right-aligned banner

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>

Trailing justify

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.

Flush right

layout.trailing.justify: 'end' pins the path to the right edge — no passThrough needed.

…/Projects/pindoba

Left of its column

justify: 'start' left-aligns the same content within its trailing region.

…/Projects/pindoba
---
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>

Leading span

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

Design tokens, refreshed

June 3, 2026 · 4 min read

New

Changelog · v2.4

Design tokens, refreshed

New

June 3, 2026 · 4 min read

Changelog · v2.4

Design tokens, refreshed

June 3, 2026 · 4 min read

New
---
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>

Feedback Surface

Banner inherits Panel’s feedback surfaces, useful when the banner itself is the component (not wrapped by a card or dialog).

Primary

Inherits panel surface from `feedback`

Success

Inherits panel surface from `feedback`

Warning

Inherits panel surface from `feedback`

Danger

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>
props · 27 shown · 27 total
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"
default "transparent"

Panel surface fill. Only meaningful when `feedback="neutral"`.

border
"none""bold""default""muted""accent"
default "none"

Panel border ring strength. Only meaningful when `feedback="neutral"`.

children slot svelte
Snippet

No description yet.

element binding svelte
HTMLDivElementnull

No description yet.

emphasis
"primary""secondary""tertiary"

Panel emphasis variant. Under `"primary"`, panel-aware text colors flip to their contrast variant.

eyebrow slot svelte
stringSnippetTitleWithLeadingTrailing

No description yet.

eyebrowTextStyle
"eyebrow""control.xs""control.sm""control.md""control.lg""control.xl""heading.2xl""heading.xl""heading.lg""heading.md""heading.sm""heading.xs""heading.2xs""heading.3xs""body.lg""body.md""body.sm""lead""label.lg""label.md""label.sm""caption""numeric.sm""numeric.md""numeric.lg""code""kbd""quote"

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.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic surface color inherited from Panel. When set to anything other than `neutral`, banner owns its background via the feedback palette.

heading slot svelte
stringSnippetTitleWithLeadingTrailing

No description yet.

headingLevel
123456
default 3

Document 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.

headingTextStyle
"eyebrow""control.xs""control.sm""control.md""control.lg""control.xl""heading.2xl""heading.xl""heading.lg""heading.md""heading.sm""heading.xs""heading.2xs""heading.3xs""body.lg""body.md""body.sm""lead""label.lg""label.md""label.sm""caption""numeric.sm""numeric.md""numeric.lg""code""kbd""quote"

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.

headingWeight
"normal""semibold"
default semibold

Heading font weight. `"normal"` renders the heading as plain body text.

layout
BannerLayout

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"`).

leading slot svelte
stringSnippet

No description yet.

padding
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""7xl""8xl""4xs""3xs""2xs""9xl""10xl""11xl"
default "none"

Panel padding token. Defaults to none so banner reads flat inside cards/dialogs.

passThrough
BannerPassThrough<RootElementAttributes, SlotElementAttributes>

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.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"
default "none"

Panel radius token.

radiusBottom
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Panel radius token applied to the bottom corners only.

radiusLeft
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Panel radius token applied to the left corners only.

radiusRight
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Panel radius token applied to the right corners only.

radiusTop
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Panel radius token applied to the top corners only.

shadow
"sm""md""lg""xl""none""xs"

Panel drop-shadow token.

size
"sm""md""lg""xs"
default "md"

Controls the heading font size. Padding and radius come from Panel and can be set independently.

subheading slot svelte
stringSnippetTitleWithLeadingTrailing

No description yet.

subheadingTextStyle
"eyebrow""control.xs""control.sm""control.md""control.lg""control.xl""heading.2xl""heading.xl""heading.lg""heading.md""heading.sm""heading.xs""heading.2xs""heading.3xs""body.lg""body.md""body.sm""lead""label.lg""label.md""label.sm""caption""numeric.sm""numeric.md""numeric.lg""code""kbd""quote"

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.

trailing slot svelte
stringSnippet

No description yet.

translucent
boolean
default false

Frosted-glass backdrop blur. Only meaningful when `feedback="neutral"`.

Plus all standard <div> HTML attributes.

Type

  • Components
  • Blocks