Reticle@etched/reticle 0.0.0

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.

Kumo source
Firmware 2.4 is available
Bench B-14 is due for calibration
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

Firmware 2.4 is available
Benches update at the end of the current run.
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/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

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

Firmware 2.4 is available
Benches update at the end of the current run.
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

Benches update at the end of the current run.
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 { 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>
  );
}

API reference