Alert
An inline notice whose variants are weights, not colours.
Billing moves to the 1st
Your card was declined
Read-only until 14:00
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
npm i mihirmodi-ui motionThe 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
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
| Prop | Type | Notes |
|---|---|---|
| variant"default" | "strong" | "default" | "strong"= "default" | Strong puts the mark on an ink tile, or draws an ink rule when there is no mark. |
| icon / title / childrenReactNode | ReactNode | The mark, the headline, and one or two sentences of body. |
| actionReactNode | ReactNode | A button or link on the right. |
| role"status" | "alert" | "status" | "alert"= "status" | 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.
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>
);
}