Feedback
Result
A full-area outcome after an action or request: success, error, warning, info, not found or no access.
- Built on
<h1> to <h4>currentColor- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 2.0 kB
- Built from
- Icon
Payment received
Try its options
Loading its options.
Code
import { Result } from "@/components/equip/result";
export default function Demo() {
return (
<Result status="success" size="s" title="Payment received" style={{ maxWidth: 208 }}>
Invoice 2041 is paid and the receipt is on its way.
</Result>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="max-width:208px" class="eq-result" data-status="success" data-size="s">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="20" style="--_size:20px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M8 12.5l2.75 2.75L16 9.5" />
</svg>
</div>
<h2 class="eq-result-title">Payment received</h2>
<div class="eq-result-body">Invoice 2041 is paid and the receipt is on its way.</div>
</div>Examples
The four outcomes
The glyph takes the status tone: ok, danger, warn, or neutral for info. The title still has to say the outcome on its own.
Export ready
Import failed
Saved with gaps
Sync paused
Code
import { Result } from "@/components/equip/result";
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 24, width: "100%", maxWidth: 680 }}>
<Result style={{ flex: "1 1 150px", minWidth: 0 }} status="success" size="s" headingLevel={3} title="Export ready">
orders-october.csv has 1,284 rows.
</Result>
<Result style={{ flex: "1 1 150px", minWidth: 0 }} status="error" size="s" headingLevel={3} title="Import failed">
Row 312 has no customer email.
</Result>
<Result style={{ flex: "1 1 150px", minWidth: 0 }} status="warning" size="s" headingLevel={3} title="Saved with gaps">
Three products have no price yet.
</Result>
<Result style={{ flex: "1 1 150px", minWidth: 0 }} status="info" size="s" headingLevel={3} title="Sync paused">
Syncing resumes after the 02:00 maintenance window.
</Result>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:24px;width:100%;max-width:680px">
<div style="flex:1 1 150px;min-width:0" class="eq-result" data-status="success" data-size="s">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="20" style="--_size:20px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M8 12.5l2.75 2.75L16 9.5" />
</svg>
</div>
<h3 class="eq-result-title">Export ready</h3>
<div class="eq-result-body">orders-october.csv has 1,284 rows.</div>
</div>
<div style="flex:1 1 150px;min-width:0" class="eq-result" data-status="error" data-size="s">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="20" style="--_size:20px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9.25 9.25l5.5 5.5M14.75 9.25l-5.5 5.5" />
</svg>
</div>
<h3 class="eq-result-title">Import failed</h3>
<div class="eq-result-body">Row 312 has no customer email.</div>
</div>
<div style="flex:1 1 150px;min-width:0" class="eq-result" data-status="warning" data-size="s">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="20" style="--_size:20px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 4 21 19.5H3z" />
<path d="M12 9.5v3.5M12 16.75v.01" />
</svg>
</div>
<h3 class="eq-result-title">Saved with gaps</h3>
<div class="eq-result-body">Three products have no price yet.</div>
</div>
<div style="flex:1 1 150px;min-width:0" class="eq-result" data-status="info" data-size="s">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="20" style="--_size:20px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 11.25v5.25" />
<path d="M12 7.5v.01" />
</svg>
</div>
<h3 class="eq-result-title">Sync paused</h3>
<div class="eq-result-body">Syncing resumes after the 02:00 maintenance window.</div>
</div>
</div>Page not found
As the whole page, the title is the h1. The code above it is hidden from assistive tech, since the title already says it.
Page not found
The project “atlas-staging” was renamed or deleted. Check the address or go back to your projects.
Code
import { Button } from "@/components/equip/button";
import { Result, ResultActions } from "@/components/equip/result";
export default function Example() {
return (
<Result status="404" headingLevel={1}>
<p>The project “atlas-staging” was renamed or deleted. Check the address or go back to your projects.</p>
<ResultActions>
<Button variant="accent">Go to projects</Button>
<Button variant="ghost">Contact support</Button>
</ResultActions>
</Result>
);
}HTMLworks in any framework, with the part's CSS
<div class="eq-result" data-status="404" data-size="m">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="28" style="--_size:28px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M15.5 8.5l-2 5-5 2 2-5z" />
</svg>
</div>
<span class="eq-result-code" aria-hidden="true">Error 404</span>
<h1 class="eq-result-title">Page not found</h1>
<div class="eq-result-body">
<p>The project “atlas-staging” was renamed or deleted. Check the address or go back to your projects.</p>
<div class="eq-result-actions">
<button type="button" class="eq-button" data-variant="accent" data-size="m">
<span class="eq-button-label">Go to projects</span>
</button>
<button type="button" class="eq-button" data-variant="ghost" data-size="m">
<span class="eq-button-label">Contact support</span>
</button>
</div>
</div>
</div>No access, in a panel
No access
Code
import { Button } from "@/components/equip/button";
import { Result, ResultActions } from "@/components/equip/result";
export default function Example() {
return (
<div style={{ width: "100%", maxWidth: 360, padding: 16, boxSizing: "border-box", border: "1px solid var(--eq-line)", borderRadius: "var(--eq-radius-l)" }}>
<Result status="403" size="s" headingLevel={3}>
Billing is visible to workspace owners. Ask Priya Raman for access.
<ResultActions>
<Button size="s" variant="outline">Request access</Button>
</ResultActions>
</Result>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="width:100%;max-width:360px;padding:16px;box-sizing:border-box;border:1px solid var(--eq-line);border-radius:var(--eq-radius-l)">
<div class="eq-result" data-status="403" data-size="s">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="20" style="--_size:20px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M6.5 10.5h11a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-11a2 2 0 0 1 -2 -2v-6a2 2 0 0 1 2 -2z" />
<path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
<path d="M12 14.5v2" />
</svg>
</div>
<span class="eq-result-code" aria-hidden="true">Error 403</span>
<h3 class="eq-result-title">No access</h3>
<div class="eq-result-body">Billing is visible to workspace owners. Ask Priya Raman for access.<div class="eq-result-actions"><button type="button" class="eq-button" data-variant="outline" data-size="s"><span class="eq-button-label">Request access</span></button></div></div>
</div>
</div>Whole screen, with your own glyph
size="l" gives a 64px disc and a title that grows with the space around it; icon swaps the glyph while the disc keeps the status tone.
Check your inbox
We sent a sign-in link to ines@marrow.co.uk. It works for 15 minutes.
Code
import { Button } from "@/components/equip/button";
import { Result, ResultActions } from "@/components/equip/result";
export default function Example() {
return (
<Result status="success" size="l" icon="mail" title="Check your inbox" style={{ width: "100%" }}>
<p>We sent a sign-in link to ines@marrow.co.uk. It works for 15 minutes.</p>
<ResultActions>
<Button variant="outline">Send it again</Button>
</ResultActions>
</Result>
);
}HTMLworks in any framework, with the part's CSS
<div style="width:100%" class="eq-result" data-status="success" data-size="l">
<div class="eq-result-glyph">
<svg aria-hidden="true" focusable="false" class="eq-icon" data-size="32" style="--_size:32px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 5h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2z" />
<path d="M4 7l8 5.5L20 7" />
</svg>
</div>
<h2 class="eq-result-title">Check your inbox</h2>
<div class="eq-result-body">
<p>We sent a sign-in link to ines@marrow.co.uk. It works for 15 minutes.</p>
<div class="eq-result-actions">
<button type="button" class="eq-button" data-variant="outline" data-size="m">
<span class="eq-button-label">Send it again</span>
</button>
</div>
</div>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
status | "success" | "error" | "warning" | "info" | "404" | "403" | none | What happened. Picks the glyph, its tone and the default title. |
title | ReactNode | none | The outcome in a few words. Defaults to "Page not found" for 404, "No access" for 403 and a generic line otherwise. |
headingLevel | 1 | 2 | 3 | 4 | 2 | The title's heading level. Use 1 when the result is the whole page. |
size | "s" | "m" | "l" | "m" | s for a card or a dialog, m for a page or a panel, l for a whole screen such as a 404: a 64px disc and a title from 30px to 48px. |
icon | IconName | ReactNode | none | Replaces the status glyph. A string is an Icon name, drawn at the right size for the result; your own svg or Icon element is sized to match. The disc keeps the status tone. |
children | ReactNode | none | One or two sentences, then an optional ResultActions. |
...rest | ComponentProps<"div"> | none | Every native div attribute except title, which the part owns. |
Anatomy
| root | .eq-result | Status and size are on data-status and data-size. |
|---|---|---|
| glyph | .eq-result-glyph | A disc washed in the status tone, holding an Icon sized 20, 28 or 32px, or your icon. |
| code | .eq-result-code | "Error 404" or "Error 403", hidden from assistive tech. |
| title | .eq-result-title | An h1 to h4. |
| body | .eq-result-body | The explanation and the actions. |
| actions | .eq-result-actions | ResultActions: buttons or links that move on. |
Notes
- Distinct from Empty state, which fills a list or table that has nothing in it yet and invites the first item; Result reports how something ended.
- The glyph takes the status tone (ok, danger, warn, or neutral for info, 404 and 403) and never the accent; it is hidden from assistive tech, so the title must say the outcome.
- Give at most one accent button in ResultActions, for the obvious next step; the rest are outline or ghost.
- It is not a live region; when it replaces a form after submitting, move focus to the title (give it tabIndex={-1}) or announce the outcome in a status message.
- Say what happened and what to do next: "Invoice 2041 is paid" beats "Success".
- Renders on the server with no client JavaScript.
- A size l title scales with the nearest size container, or the viewport when there is none.
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/result.tsx82 lines
import type { ComponentProps, ReactNode } from "react";
import { Icon, type IconName } from "./icon";
import "./result.css";
export type ResultStatus = "success" | "error" | "warning" | "info" | "404" | "403";
export type ResultProps = Omit<ComponentProps<"div">, "title"> & {
/** What happened. Picks the glyph, its tone and the default title. */
status: ResultStatus;
/** Says the outcome in a few words. Defaults to "Page not found" for 404 and "No access" for 403. */
title?: ReactNode;
/** The title's heading level. Use 1 when the result is the whole page, as a 404 usually is. */
headingLevel?: 1 | 2 | 3 | 4;
/** s for a card or a dialog, m for a page or a panel, l for a whole screen. */
size?: "s" | "m" | "l";
/** Replaces the status glyph: an Icon name, or your own svg or node. The disc keeps the status tone. */
icon?: IconName | ReactNode;
};
/* The glyph's size in pixels at each result size. */
const glyphSizes = { s: 20, m: 28, l: 32 } as const;
const glyphs: Record<ResultStatus, IconName> = {
success: "check-circle",
error: "close-circle",
warning: "warning",
info: "info",
"404": "compass",
"403": "lock",
};
const titles: Record<ResultStatus, string> = {
success: "Done",
error: "Something went wrong",
warning: "Check before you go on",
info: "Good to know",
"404": "Page not found",
"403": "No access",
};
export function Result({
status,
title,
headingLevel = 2,
size = "m",
icon,
className,
children,
...rest
}: ResultProps) {
const Heading = `h${headingLevel}` as const;
return (
<div
{...rest}
className={["eq-result", className].filter(Boolean).join(" ")}
data-status={status}
data-size={size}
>
<div className="eq-result-glyph">
{icon == null || icon === false ? (
<Icon name={glyphs[status]} size={glyphSizes[size]} />
) : typeof icon === "string" ? (
<Icon name={icon as IconName} size={glyphSizes[size]} />
) : (
icon
)}
</div>
{(status === "404" || status === "403") && (
<span className="eq-result-code" aria-hidden="true">
Error {status}
</span>
)}
<Heading className="eq-result-title">{title ?? titles[status]}</Heading>
{children != null && <div className="eq-result-body">{children}</div>}
</div>
);
}
/** The buttons or links that move on from the result, placed last inside Result. One accent at most. */
export function ResultActions({ className, ...rest }: ComponentProps<"div">) {
return <div {...rest} className={["eq-result-actions", className].filter(Boolean).join(" ")} />;
}components/equip/result.css115 lines
@layer equip {
.eq-result {
--_tone: var(--eq-ink-2);
--_disc: 56px;
--_glyph: 28px;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--eq-space-6) var(--eq-space-4);
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-m) / 1.45 var(--eq-font);
text-align: center;
overflow-wrap: anywhere;
}
.eq-result[data-size="s"] {
--_disc: 36px;
--_glyph: 20px;
padding: 0;
font-size: var(--eq-text-s);
}
/* Large: a whole screen. The title follows the nearest container's width, or the viewport. */
.eq-result[data-size="l"] {
--_disc: 64px;
--_glyph: 32px;
}
.eq-result[data-status="success"] {
--_tone: var(--eq-ok);
}
.eq-result[data-status="error"] {
--_tone: var(--eq-danger);
}
.eq-result[data-status="warning"] {
--_tone: var(--eq-warn);
}
/* A filled disc in a wash of the tone: an outcome, where the empty state's dashed tile is an absence. */
.eq-result-glyph {
display: grid;
place-items: center;
width: var(--_disc);
height: var(--_disc);
margin-bottom: var(--eq-space-3);
border-radius: var(--eq-radius-full);
background: color-mix(in srgb, var(--_tone) 14%, transparent);
color: var(--_tone);
}
.eq-result[data-size="s"] .eq-result-glyph {
margin-bottom: var(--eq-space-2);
}
/* Sizes your own svg passed as icon. The built-in glyph's numeric Icon size outranks this. */
.eq-result-glyph > svg {
width: var(--_glyph);
height: var(--_glyph);
}
.eq-result-code {
margin-bottom: var(--eq-space-1);
color: var(--eq-ink-3);
font-family: var(--eq-font-mono);
font-variation-settings: var(--eq-font-mono-settings);
font-size: var(--eq-text-xs);
font-variant-numeric: tabular-nums;
}
.eq-result-title {
margin: 0;
font-size: calc(var(--eq-text-l) * 1.25);
font-weight: var(--eq-weight-strong);
line-height: 1.25;
letter-spacing: -0.01em;
text-wrap: balance;
}
.eq-result[data-size="s"] .eq-result-title {
font-size: var(--eq-text-m);
letter-spacing: -0.005em;
}
.eq-result[data-size="l"] .eq-result-title {
font-size: clamp(calc(var(--eq-text-l) * 1.875), 7cqi, calc(var(--eq-text-l) * 3));
line-height: 1.04;
letter-spacing: -0.03em;
}
.eq-result-body {
max-width: 48ch;
margin-top: var(--eq-space-1);
color: var(--eq-ink-2);
text-wrap: pretty;
}
.eq-result-body > p {
margin: 0;
}
.eq-result-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--eq-space-2);
margin-top: var(--eq-space-5);
}
.eq-result[data-size="s"] .eq-result-actions {
margin-top: var(--eq-space-3);
}
}