← All components

Alert

An inline notice whose variants are weights, not colours.

Billing moves to the 1st

From November every plan renews on the first of the month. Nothing changes until then.

Read-only until 14:00

The workspace is moving to a new region. Edits made now would be lost.

The idea

There is no red, amber or green: the words already say whether something failed, and a colour asks the reader to have learnt a legend. What a notice can honestly vary is how hard it asks for attention, so `default` is a hairline like any other grouped thing and `strong` adds the one solid ink on it — the mark on an ink tile, or a rule down the left edge. It sits on the page, so it is not glass.

Composes Button.

Install

terminal1 line
npm i mihirmodi-ui motion

The tokens, the theme script and the routing provider are a one-time job — see Setup. Or skip the package entirely and take the source at the foot of this page.

Usage

Alert — usage18 lines
import { Alert, Button } from "mihirmodi-ui";

// Weight, not colour: the words say what happened.
<Alert icon={<InfoIcon />} title="Billing moves to the 1st">
  From November every plan renews on the first of the month.
</Alert>

// The one notice that must be read first. role="alert" interrupts —
// keep it for something that stops the reader.
<Alert
  variant="strong"
  role="alert"
  icon={<CardIcon />}
  title="Your card was declined"
  action={<Button size="sm">Update</Button>}
>
  We'll try again on 18 October.
</Alert>

Props

PropNotes
variant"default" | "strong"Strong puts the mark on an ink tile, or draws an ink rule when there is no mark.
icon / title / childrenReactNodeThe mark, the headline, and one or two sentences of body.
actionReactNodeA button or link on the right.
role"status" | "alert"Polite or interrupting. Interrupt only for something that stops the reader.

Source

Exactly what the package ships — this is read out of the source at build time, not a copy. Take it and adapt it if you would rather not add a dependency.

packages/ui/src/components/layout/Alert.tsx71 lines
import type { ReactNode } from "react";

export type AlertVariant = "default" | "strong";

export type AlertProps = {
  /**
   * By weight, not colour. `default` is a hairline note. `strong` puts the
   * mark on an ink tile — or, with no mark, an ink rule down the left edge —
   * for the one notice on a screen that must be read before the rest.
   */
  variant?: AlertVariant;
  /** A small glyph before the title. */
  icon?: ReactNode;
  title?: ReactNode;
  /** The body: one or two sentences that say what happened and what to do. */
  children?: ReactNode;
  /** A control on the right — a button, a link. */
  action?: ReactNode;
  /**
   * `status` is announced when the reader is free; `alert` interrupts them.
   * Interrupt only for something that stops them — a failed save, not a tip.
   */
  role?: "status" | "alert";
  className?: string;
};

/**
 * An inline notice: something the page needs to tell you, in the flow of it.
 *
 * There is no red, amber or green. A coloured alert asks the reader to have
 * learnt which colour means what, and to see colour at all; the words already
 * say whether something failed. What a notice can honestly vary is how hard
 * it asks for attention, so the variants are weights. `default` is a hairline,
 * like any other grouped thing on the page. `strong` adds the one solid ink on
 * the notice — the mark on an ink tile, or a rule down the left edge — which
 * is enough to make it the first thing read without making it shout.
 *
 * It sits on the page, so it is not glass. The live region is a prop because
 * only the caller knows how urgent the news is. Either way a notice that was
 * on the page when the reader arrived is not read out — a live region speaks
 * when its content changes — so render it up front and swap its words.
 */
export function Alert({ variant = "default", icon, title, children, action, role = "status", className = "" }: AlertProps) {
  const strong = variant === "strong";
  const rule = strong && !icon;
  return (
    <div
      role={role}
      className={`relative flex items-start gap-3 rounded-[14px] border-[0.5px] border-gray-alpha-400 bg-background-100 py-3.5 pr-3.5 ${
        rule ? "pl-5" : "pl-4"
      } ${className}`}
    >
      {rule && <span aria-hidden className="absolute bottom-3 left-2 top-3 w-[3px] rounded-full bg-ink" />}
      {icon && (
        <span
          aria-hidden
          className={`flex shrink-0 items-center justify-center ${
            strong ? "h-6 w-6 rounded-[8px] bg-ink text-background-100" : "mt-[3px] h-4 w-4 text-muted"
          }`}
        >
          {icon}
        </span>
      )}
      <div className={`flex min-w-0 flex-1 flex-col gap-0.5 ${strong && icon ? "pt-[3px]" : ""}`}>
        {title && <p className="text-[13px] font-medium leading-[1.4] text-gray-1000">{title}</p>}
        {children && <div className="text-[13px] leading-[1.55] text-muted">{children}</div>}
      </div>
      {action && <div className="-my-0.5 flex shrink-0 items-center self-center">{action}</div>}
    </div>
  );
}