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

Invoice 2041 is paid and the receipt is on its way.
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

orders-october.csv has 1,284 rows.

Import failed

Row 312 has no customer email.

Saved with gaps

Three products have no price yet.

Sync paused

Syncing resumes after the 02:00 maintenance window.
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 &ldquo;atlas-staging&rdquo; 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

Billing is visible to workspace owners. Ask Priya Raman for 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

PropTypeDefaultWhat it does
status"success" | "error" | "warning" | "info" | "404" | "403"noneWhat happened. Picks the glyph, its tone and the default title.
titleReactNodenoneThe outcome in a few words. Defaults to "Page not found" for 404, "No access" for 403 and a generic line otherwise.
headingLevel1 | 2 | 3 | 42The 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.
iconIconName | ReactNodenoneReplaces 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.
childrenReactNodenoneOne or two sentences, then an optional ResultActions.
...restComponentProps<"div">noneEvery native div attribute except title, which the part owns.

Anatomy

root.eq-resultStatus and size are on data-status and data-size.
glyph.eq-result-glyphA 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-titleAn h1 to h4.
body.eq-result-bodyThe explanation and the actions.
actions.eq-result-actionsResultActions: 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);
  }
}

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