Monet
@etched/monet 0.0.1

Banner

Ported

A message in the page flow: information, success, a neutral note, a warning or an error. It is a live region: role="alert" for errors, role="status" for the rest. Pass role to change it. While title and description are empty, only the region is there, empty and hidden, so a message that comes later is announced, and <Banner variant="error" description={error} /> needs no condition.

Firmware 2.4 is available
Slack is connected
import { type CSSProperties } from "react";
import { Banner, type BannerVariant } from "@etched/monet/banner";

const copy: Record<BannerVariant, [string, string]> = {
  info: ["Firmware 2.4 is available", "Benches update at the end of the current run."],
  success: ["Slack is connected", "Alerts for your chips now go to #bench-alerts."],
  warning: ["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.info[0]} />
      <Banner variant="success" title={copy.success[0]} />
      <Banner variant="error" title={copy.error[0]} />
    </div>
  );
}

Installation

import { Banner, BannerAction } from "@etched/monet/banner";

Examples

Variants

Firmware 2.4 is available
Benches update at the end of the current run.
Slack is connected
Alerts for your chips now go to #bench-alerts.
Bench B-14 is due for calibration
Results after 18:00 may drift.
Department routing is off
This mapping applies when routing is turned on.
import { type CSSProperties } from "react";
import { Banner, type BannerVariant } from "@etched/monet/banner";

const variants: BannerVariant[] = ["info", "success", "warning", "error", "secondary"];

const copy: Record<BannerVariant, [string, string]> = {
  info: ["Firmware 2.4 is available", "Benches update at the end of the current run."],
  success: ["Slack is connected", "Alerts for your chips now go to #bench-alerts."],
  warning: ["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

Slack delivery is paused. Your notification settings are saved.
Profile fields are ready to edit.
Experiments in this lab are visible to your team only.
Loading LinkedIn details… The candidate is saved. Details appear when the import ends.
import { FlaskIcon } from "@phosphor-icons/react";
import { type CSSProperties } from "react";
import { Banner } from "@etched/monet/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

BannerAction takes the banner's color: primary for the main action, secondary and ghost for the rest. onDismiss adds a close button.

Firmware 2.4 is available
Benches update at the end of the current run.
Slack is connected
Alerts for your chips now go to #bench-alerts.
Bench B-15 is due for calibration
import { ArrowsClockwiseIcon } from "@phosphor-icons/react";
import { useState, type CSSProperties } from "react";
import { Button } from "@etched/monet/button";
import { Banner, BannerAction } from "@etched/monet/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={<BannerAction>Update now</BannerAction>}
          onDismiss={() => setDismissed(true)}
        />
      )}
      <Banner
        variant="success"
        title="Slack is connected"
        description="Alerts for your chips now go to #bench-alerts."
        action={<BannerAction variant="secondary">Send a test alert</BannerAction>}
      />
      <Banner
        variant="warning"
        title="Bench B-15 is due for calibration"
        action={
          <>
            <BannerAction variant="ghost">Remind me</BannerAction>
            <BannerAction>Calibrate</BannerAction>
          </>
        }
      />
      <Banner
        variant="error"
        title="Export failed"
        description="The report service did not respond."
        action={<BannerAction icon={<ArrowsClockwiseIcon />}>Retry export</BannerAction>}
      />
    </div>
  );
}

Compact

Benches update at the end of the current run.
Alerts for your chips now go to #bench-alerts.
Results after 18:00 may drift.
This mapping applies when routing is turned on.
Calibration due.
Results after 18:00 may drift.
import { type CSSProperties } from "react";
import { Banner, BannerAction, type BannerVariant } from "@etched/monet/banner";

const variants: BannerVariant[] = ["info", "success", "warning", "error", "secondary"];

const copy: Record<BannerVariant, [string, string]> = {
  info: ["Firmware 2.4 is available", "Benches update at the end of the current run."],
  success: ["Slack is connected", "Alerts for your chips now go to #bench-alerts."],
  warning: ["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="warning" title="Calibration due." description="Results after 18:00 may drift." />
      <Banner size="sm" variant="error" description="report.pdf is larger than 25 MB." action={<BannerAction variant="secondary">Choose file</BannerAction>} />
    </div>
  );
}

Live error: hidden until there is a message

The empty banner keeps its live region on the page, so screen readers announce the message when it comes.

import { useRef, useState, type CSSProperties } from "react";
import { Button } from "@etched/monet/button";
import { Banner } from "@etched/monet/banner";

const stack: CSSProperties = { display: "grid", gap: "var(--et-space-3)" };

export function LiveErrorHiddenUntilThereIsAMessage() {
  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} onDismiss={clear} closeLabel="Clear" />
    </div>
  );
}

Accessibility

  • role="alert" for error, role="status" for the other variants; pass role to change it. The icon is hidden; the close button is named by closeLabel ("Close").
  • Screen readers announce the text when it comes in: an alert at once, a status after what they are reading.
  • With no title or description, the empty region stays in the page, hidden, so a message set later is announced.
  • Keep the banner rendered and set description when the message comes, not the banner. In onDismiss, move focus if the close button had it.

API reference

BannerAction

PropTypeDefaultDescription
variant"primary" | "secondary" | "ghost""primary"primary: filled in the banner's color, for the main action. secondary: an edge in its color. ghost: text only.