Feedback

Alert

An inline message about the page or a task, in one of four tones, with an optional action.

Built on
ARIA live regions
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.5 kB
Dependencies
React

Card expires in 3 days

Update the Visa ending 4242 to keep your Pro plan.
Try its options

Loading its options.

Code
import { Alert } from "@/components/equip/alert";

export default function Demo() {
  return (
    <Alert tone="warn" title="Card expires in 3 days" style={{ width: 216 }}>
      Update the Visa ending 4242 to keep your Pro plan.
    </Alert>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:216px" class="eq-alert" data-tone="warn">
  <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <path d="M8 2.25l6.25 11H1.75z" />
    <path d="M8 6.75v3M8 11.5v.01" />
  </svg>
  <div class="eq-alert-body">
    <p class="eq-alert-title">Card expires in 3 days</p>
    <div class="eq-alert-text">Update the Visa ending 4242 to keep your Pro plan.</div>
  </div>
</div>

Examples

Tones

Each tone has its own icon shape, so the difference survives greyscale and colour blindness.

Maintenance on 18 Nov, 02:00 UTC

The dashboard is read-only for about 20 minutes.

Domain verified

app.example.com now serves production traffic.

You have used 92% of build minutes

New builds queue once you reach 3,000.

Deploy failed

next build exited with code 1 in src/app/layout.tsx.
Code
import { Alert } from "@/components/equip/alert";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 8, maxWidth: 420 }}>
      <Alert title="Maintenance on 18 Nov, 02:00 UTC">The dashboard is read-only for about 20 minutes.</Alert>
      <Alert tone="ok" title="Domain verified">app.example.com now serves production traffic.</Alert>
      <Alert tone="warn" title="You have used 92% of build minutes">New builds queue once you reach 3,000.</Alert>
      <Alert tone="danger" announce={false} title="Deploy failed">
        next build exited with code 1 in src/app/layout.tsx.
      </Alert>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;max-width:420px">
  <div class="eq-alert" data-tone="info">
    <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <circle cx="8" cy="8" r="6.25" />
      <path d="M8 7.25v3.5M8 5.25v.01" />
    </svg>
    <div class="eq-alert-body">
      <p class="eq-alert-title">Maintenance on 18 Nov, 02:00 UTC</p>
      <div class="eq-alert-text">The dashboard is read-only for about 20 minutes.</div>
    </div>
  </div>
  <div class="eq-alert" data-tone="ok">
    <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <circle cx="8" cy="8" r="6.25" />
      <path d="M5.5 8.25l1.75 1.75 3.25-3.5" />
    </svg>
    <div class="eq-alert-body">
      <p class="eq-alert-title">Domain verified</p>
      <div class="eq-alert-text">app.example.com now serves production traffic.</div>
    </div>
  </div>
  <div class="eq-alert" data-tone="warn">
    <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M8 2.25l6.25 11H1.75z" />
      <path d="M8 6.75v3M8 11.5v.01" />
    </svg>
    <div class="eq-alert-body">
      <p class="eq-alert-title">You have used 92% of build minutes</p>
      <div class="eq-alert-text">New builds queue once you reach 3,000.</div>
    </div>
  </div>
  <div class="eq-alert" data-tone="danger">
    <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M5.4 1.75h5.2l3.65 3.65v5.2l-3.65 3.65H5.4l-3.65-3.65V5.4z" />
      <path d="M6 6l4 4M10 6l-4 4" />
    </svg>
    <div class="eq-alert-body">
      <p class="eq-alert-title">Deploy failed</p>
      <div class="eq-alert-text">next build exited with code 1 in src/app/layout.tsx.</div>
    </div>
  </div>
</div>

With an action

The action sits at the end of the row, after the text, so it is reached after the message is read.

Payment failed

We could not charge $480.00 for invoice INV-2041.
Code
import { Button } from "@/components/equip/button";
import { Alert } from "@/components/equip/alert";

export default function Example() {
  return (
    <Alert
      tone="danger"
      announce={false}
      title="Payment failed"
      action={
        <Button size="s" variant="outline">
          Update card
        </Button>
      }
      style={{ maxWidth: 440 }}
    >
      We could not charge $480.00 for invoice INV-2041.
    </Alert>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="max-width:440px" class="eq-alert" data-tone="danger">
  <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <path d="M5.4 1.75h5.2l3.65 3.65v5.2l-3.65 3.65H5.4l-3.65-3.65V5.4z" />
    <path d="M6 6l4 4M10 6l-4 4" />
  </svg>
  <div class="eq-alert-body">
    <p class="eq-alert-title">Payment failed</p>
    <div class="eq-alert-text">We could not charge $480.00 for invoice INV-2041.</div>
  </div>
  <div class="eq-alert-action">
    <button type="button" class="eq-button" data-variant="outline" data-size="s">
      <span class="eq-button-label">Update card</span>
    </button>
  </div>
</div>

Title or body alone

Changes saved to eu-west-1

Invites expire after 7 days. Resend from the Members page.
Code
import { Alert } from "@/components/equip/alert";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 8, maxWidth: 420 }}>
      <Alert tone="ok" title="Changes saved to eu-west-1" />
      <Alert>Invites expire after 7 days. Resend from the Members page.</Alert>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;max-width:420px">
  <div class="eq-alert" data-tone="ok">
    <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <circle cx="8" cy="8" r="6.25" />
      <path d="M5.5 8.25l1.75 1.75 3.25-3.5" />
    </svg>
    <div class="eq-alert-body">
      <p class="eq-alert-title">Changes saved to eu-west-1</p>
    </div>
  </div>
  <div class="eq-alert" data-tone="info">
    <svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <circle cx="8" cy="8" r="6.25" />
      <path d="M8 7.25v3.5M8 5.25v.01" />
    </svg>
    <div class="eq-alert-body">
      <div class="eq-alert-text">Invites expire after 7 days. Resend from the Members page.</div>
    </div>
  </div>
</div>

Props

PropTypeDefaultWhat it does
tone"info" | "ok" | "warn" | "danger""info"Sets the icon shape and the colour of the icon and border.
titleReactNodenoneOne line that says what happened.
actionReactNodenonePlaced at the end of the row, usually a small Button.
announcebooleantone === "danger"Makes it a live region: role="alert" for danger, role="status" for every other tone. False sets no role.
...restComponentProps<"div">noneEvery native div attribute except title and role, which the part owns.

Anatomy

root.eq-alertTone is on data-tone.
icon.eq-alert-iconA different shape per tone.
body.eq-alert-body
title.eq-alert-title
text.eq-alert-text
action.eq-alert-action

Notes

  • Renders on the server with no client JavaScript.
  • A live region is announced when its content changes, not when it is already on the page at load; render it empty first, or mount it when the event happens.
  • Only danger interrupts with role="alert". Set announce={false} for a danger message that is part of the page rather than news.
  • The icon is hidden from assistive tech, so write a title that states the tone in words, such as "Deploy failed".

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/alert.tsx84 lines
import type { ComponentProps, ReactNode } from "react";
import "./alert.css";

type Tone = "info" | "ok" | "warn" | "danger";

export type AlertProps = Omit<ComponentProps<"div">, "title" | "role"> & {
  /** Carried by the icon shape and a tone-coloured border, never by colour alone. */
  tone?: Tone;
  /** One line that says what happened. */
  title?: ReactNode;
  /** Usually a small Button, placed at the end. */
  action?: ReactNode;
  /**
   * Makes the alert a live region so screen readers announce it when it appears:
   * role="alert" for danger, role="status" for every other tone.
   * Defaults to true for danger and false otherwise.
   */
  announce?: boolean;
};

/* One shape per tone, 16px, drawn in the tone colour. */
const icons: Record<Tone, ReactNode> = {
  info: (
    <>
      <circle cx="8" cy="8" r="6.25" />
      <path d="M8 7.25v3.5M8 5.25v.01" />
    </>
  ),
  ok: (
    <>
      <circle cx="8" cy="8" r="6.25" />
      <path d="M5.5 8.25l1.75 1.75 3.25-3.5" />
    </>
  ),
  warn: (
    <>
      <path d="M8 2.25l6.25 11H1.75z" />
      <path d="M8 6.75v3M8 11.5v.01" />
    </>
  ),
  danger: (
    <>
      <path d="M5.4 1.75h5.2l3.65 3.65v5.2l-3.65 3.65H5.4l-3.65-3.65V5.4z" />
      <path d="M6 6l4 4M10 6l-4 4" />
    </>
  ),
};

export function Alert({
  tone = "info",
  title,
  action,
  announce = tone === "danger",
  className,
  children,
  ...rest
}: AlertProps) {
  return (
    <div
      {...rest}
      className={["eq-alert", className].filter(Boolean).join(" ")}
      data-tone={tone}
      role={announce ? (tone === "danger" ? "alert" : "status") : undefined}
    >
      <svg
        className="eq-alert-icon"
        viewBox="0 0 16 16"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        aria-hidden="true"
      >
        {icons[tone]}
      </svg>
      <div className="eq-alert-body">
        {title != null && <p className="eq-alert-title">{title}</p>}
        {children != null && <div className="eq-alert-text">{children}</div>}
      </div>
      {action != null && <div className="eq-alert-action">{action}</div>}
    </div>
  );
}
components/equip/alert.css67 lines
@layer equip {
  .eq-alert {
    --_tone: var(--eq-ink-2);

    display: grid;
    /* Two tracks; the action adds a third only when it is there, so no empty gutter. */
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: var(--eq-space-2) var(--eq-space-3);
    padding: var(--eq-space-3);
    border: 1px solid color-mix(in srgb, var(--_tone) 55%, transparent);
    border-radius: var(--eq-radius-m);
    background: var(--eq-tint);
    color: var(--eq-ink);
    font: var(--eq-weight) var(--eq-text-s) / 1.45 var(--eq-font);
  }

  .eq-alert[data-tone="ok"] {
    --_tone: var(--eq-ok);
  }

  .eq-alert[data-tone="warn"] {
    --_tone: var(--eq-warn);
  }

  .eq-alert[data-tone="danger"] {
    --_tone: var(--eq-danger);
  }

  /* Nudged down so its centre sits on the first line of text. */
  .eq-alert-icon {
    width: 16px;
    height: 16px;
    margin-top: 1px;
    color: var(--_tone);
  }

  .eq-alert-body {
    display: grid;
    gap: 2px;
    min-width: 0;
  }

  .eq-alert-title {
    margin: 0;
    font-weight: var(--eq-weight-strong);
  }

  .eq-alert-text {
    color: var(--eq-ink-2);
  }

  .eq-alert-text > :first-child {
    margin-top: 0;
  }

  .eq-alert-text > :last-child {
    margin-bottom: 0;
  }

  .eq-alert-action {
    grid-column: 3;
    display: flex;
    gap: var(--eq-space-2);
    align-self: center;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index