Banner
A message in the page flow: information, a neutral note, a warning or an error. Renders nothing while title and description are empty, so <Banner variant="error" description={error} /> needs no condition. It is a live region: role="alert" for errors, role="status" for the rest. Pass role to change it.
import { type CSSProperties } from "react";
import { Banner, type BannerVariant } from "@etched/reticle/banner";
const copy: Record<BannerVariant, [string, string]> = {
default: ["Firmware 2.4 is available", "Benches update at the end of the current run."],
alert: ["Bench B-14 is due for calibration", "Results after 18:00 may drift."],
error: ["Export failed", "The report service did not respond. Try again in a minute."],
secondary: ["Department routing is off", "This mapping applies when routing is turned on."],
};
const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };
export function Preview() {
return (
<div style={stack}>
<Banner title={copy.default[0]} />
<Banner variant="alert" title={copy.alert[0]} />
<Banner variant="error" title={copy.error[0]} />
</div>
);
}Installation
import { Banner } from "@etched/reticle/banner";Examples
Variants
import { type CSSProperties } from "react";
import { Banner, type BannerVariant } from "@etched/reticle/banner";
const variants: BannerVariant[] = ["default", "alert", "error", "secondary"];
const copy: Record<BannerVariant, [string, string]> = {
default: ["Firmware 2.4 is available", "Benches update at the end of the current run."],
alert: ["Bench B-14 is due for calibration", "Results after 18:00 may drift."],
error: ["Export failed", "The report service did not respond. Try again in a minute."],
secondary: ["Department routing is off", "This mapping applies when routing is turned on."],
};
const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };
export function Variants() {
return (
<div style={stack}>
{variants.map((v) => (
<Banner key={v} variant={v} title={copy[v][0]} description={copy[v][1]} />
))}
</div>
);
}Message only, custom icon, rich text
import { FlaskIcon } from "@phosphor-icons/react";
import { type CSSProperties } from "react";
import { Banner } from "@etched/reticle/banner";
const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };
export function MessageOnlyCustomIconRichText() {
return (
<div style={stack}>
<Banner variant="secondary" description="Slack delivery is paused. Your notification settings are saved." />
<Banner title="Profile fields are ready to edit." />
<Banner variant="secondary" icon={<FlaskIcon />} description="Experiments in this lab are visible to your team only." />
<Banner
description={
<>
<strong>Loading LinkedIn details…</strong> The candidate is saved. Details appear when the import ends.
</>
}
/>
</div>
);
}Actions and dismiss
import { XIcon } from "@phosphor-icons/react";
import { useState, type CSSProperties } from "react";
import { Button } from "@etched/reticle/button";
import { Banner } from "@etched/reticle/banner";
const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };
export function ActionsAndDismiss() {
const [dismissed, setDismissed] = useState(false);
return (
<div style={stack}>
{dismissed ? (
<div className="row">
{/* Focus moves here when the banner is dismissed, so keyboard users keep their place. */}
<Button autoFocus onClick={() => setDismissed(false)}>
Show the firmware banner
</Button>
</div>
) : (
<Banner
title="Firmware 2.4 is available"
description="Benches update at the end of the current run."
action={
<>
<Button size="sm" variant="primary">
Update now
</Button>
<Button size="sm" variant="ghost" shape="square" aria-label="Dismiss" icon={<XIcon />} onClick={() => setDismissed(true)} />
</>
}
/>
)}
<Banner variant="error" title="Export failed" description="The report service did not respond." action={<Button size="sm">Retry export</Button>} />
</div>
);
}Compact
import { type CSSProperties } from "react";
import { Button } from "@etched/reticle/button";
import { Banner, type BannerVariant } from "@etched/reticle/banner";
const variants: BannerVariant[] = ["default", "alert", "error", "secondary"];
const copy: Record<BannerVariant, [string, string]> = {
default: ["Firmware 2.4 is available", "Benches update at the end of the current run."],
alert: ["Bench B-14 is due for calibration", "Results after 18:00 may drift."],
error: ["Export failed", "The report service did not respond. Try again in a minute."],
secondary: ["Department routing is off", "This mapping applies when routing is turned on."],
};
const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };
export function Compact() {
return (
<div style={stack}>
{variants.map((v) => (
<Banner key={v} size="sm" variant={v} description={copy[v][1]} />
))}
<Banner size="sm" variant="alert" title="Calibration due." description="Results after 18:00 may drift." />
<Banner size="sm" variant="error" description="report.pdf is larger than 25 MB." action={<Button size="sm">Choose file</Button>} />
</div>
);
}Live error: renders nothing until there is a message
import { useRef, useState, type CSSProperties } from "react";
import { Button } from "@etched/reticle/button";
import { Banner } from "@etched/reticle/banner";
const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };
export function LiveErrorRendersNothingUntilThereIsAMessage() {
const [error, setError] = useState("");
const check = useRef<HTMLButtonElement>(null);
const clear = () => {
setError("");
check.current?.focus();
};
return (
<div style={stack}>
<div className="row">
<Button ref={check} onClick={() => setError("Lot L-7799 does not exist. Check the ID and try again.")}>
Check lot
</Button>
</div>
<Banner
size="sm"
variant="error"
description={error}
action={
<Button size="sm" onClick={clear}>
Clear
</Button>
}
/>
</div>
);
}