Commerce

Stock indicator

A small inline stock status: in stock, low, out or on backorder, with a glyph and words for each.

Built on
<time>Intl.DateTimeFormat
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.2 kB
Built from
Icon
In stockOnly 3 leftBack in stock Ships from
Try its options

Loading its options.

Code
import { StockIndicator } from "@/components/equip/stock-indicator";

export default function Demo() {
  return (
    <div style={{ display: "grid", gap: 6, justifyItems: "start" }}>
      <StockIndicator status="in-stock" />
      <StockIndicator status="low" count={3} />
      <StockIndicator status="out" date="2026-12-04" />
      <StockIndicator status="backorder" date="2027-01-12" />
    </div>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:6px;justify-items:start">
  <span class="eq-stock-indicator" data-status="in-stock" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
    <span class="eq-stock-indicator-label">In stock</span>
  </span>
  <span class="eq-stock-indicator" data-status="low" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
    <span class="eq-stock-indicator-label">Only 3 left</span>
  </span>
  <span class="eq-stock-indicator" data-status="out" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
    <span class="eq-stock-indicator-label">Back in stock <time dateTime="2026-12-04">December 4</time></span>
  </span>
  <span class="eq-stock-indicator" data-status="backorder" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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 7v5l3.5 2.5" />
    </svg>
    <span class="eq-stock-indicator-label">Ships from <time dateTime="2027-01-12">January 12</time></span>
  </span>
</div>

Examples

On a product page

Sits under the price and beside the quantity, where the decision to buy is made.

Ceramic pour-over set
Quantity: 1Only 3 left
Order by 2 pm for next-day delivery
Code
import { StockIndicator } from "@/components/equip/stock-indicator";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 8, maxWidth: 360 }}>
      <div style={{ fontWeight: 650 }}>Ceramic pour-over set</div>
      <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 12 }}>
        <span style={{ color: "var(--eq-ink-2)", fontSize: 13 }}>Quantity: 1</span>
        <StockIndicator status="low" count={3} />
      </div>
      <StockIndicator status="in-stock" size="s">
        Order by 2 pm for next-day delivery
      </StockIndicator>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;max-width:360px">
  <div style="font-weight:650">Ceramic pour-over set</div>
  <div style="display:flex;flex-wrap:wrap;align-items:center;gap:12px">
    <span style="color:var(--eq-ink-2);font-size:13px">Quantity: 1</span>
    <span class="eq-stock-indicator" data-status="low" data-size="m">
      <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
      <span class="eq-stock-indicator-label">Only 3 left</span>
    </span>
  </div>
  <span class="eq-stock-indicator" data-status="in-stock" data-size="s">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="14" style="--_size:14px" 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>
    <span class="eq-stock-indicator-label">Order by 2 pm for next-day delivery</span>
  </span>
</div>

Own wording and locale

children replaces the text and keeps the glyph; locale formats the date in a backorder or restock line.

Ready to collect from Leeds todayMade to order, ships in 3 weeksBack in stock Ausverkauft
Code
import { StockIndicator } from "@/components/equip/stock-indicator";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 8, justifyItems: "start" }}>
      <StockIndicator status="in-stock">Ready to collect from Leeds today</StockIndicator>
      <StockIndicator status="backorder">Made to order, ships in 3 weeks</StockIndicator>
      <StockIndicator status="out" date="2026-12-04" locale="en-GB" />
      <StockIndicator status="out" lang="de">
        Ausverkauft
      </StockIndicator>
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;justify-items:start">
  <span class="eq-stock-indicator" data-status="in-stock" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
    <span class="eq-stock-indicator-label">Ready to collect from Leeds today</span>
  </span>
  <span class="eq-stock-indicator" data-status="backorder" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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 7v5l3.5 2.5" />
    </svg>
    <span class="eq-stock-indicator-label">Made to order, ships in 3 weeks</span>
  </span>
  <span class="eq-stock-indicator" data-status="out" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
    <span class="eq-stock-indicator-label">Back in stock <time dateTime="2026-12-04">4 December</time></span>
  </span>
  <span lang="de" class="eq-stock-indicator" data-status="out" data-size="m">
    <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="16" style="--_size:16px" 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>
    <span class="eq-stock-indicator-label">Ausverkauft</span>
  </span>
</div>

Stock by size

In a list of variants the small size keeps every row to one line.

  • XSOut of stock
  • SOnly 2 left
  • MIn stock
  • LIn stock
  • XLShips from
Code
import { StockIndicator } from "@/components/equip/stock-indicator";

const sizes = [
  { size: "XS", status: "out" },
  { size: "S", status: "low", count: 2 },
  { size: "M", status: "in-stock" },
  { size: "L", status: "in-stock" },
  { size: "XL", status: "backorder", date: "2026-12-18" },
] as const;

export default function Example() {
  return (
    <ul style={{ display: "grid", gap: 6, margin: 0, padding: 0, listStyle: "none", maxWidth: 280 }}>
      {sizes.map((row) => (
        <li
          key={row.size}
          style={{
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            gap: 12,
            paddingBottom: 6,
            borderBottom: "1px solid var(--eq-line)",
            fontSize: 13,
          }}
        >
          <span style={{ fontWeight: 650 }}>{row.size}</span>
          <StockIndicator
            size="s"
            status={row.status}
            count={"count" in row ? row.count : undefined}
            date={"date" in row ? row.date : undefined}
          />
        </li>
      ))}
    </ul>
  );
}
HTMLworks in any framework, with the part's CSS
<ul style="display:grid;gap:6px;margin:0;padding:0;list-style:none;max-width:280px">
  <li style="display:flex;justify-content:space-between;align-items:center;gap:12px;padding-bottom:6px;border-bottom:1px solid var(--eq-line);font-size:13px">
    <span style="font-weight:650">XS</span>
    <span class="eq-stock-indicator" data-status="out" data-size="s">
      <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="14" style="--_size:14px" 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>
      <span class="eq-stock-indicator-label">Out of stock</span>
    </span>
  </li>
  <li style="display:flex;justify-content:space-between;align-items:center;gap:12px;padding-bottom:6px;border-bottom:1px solid var(--eq-line);font-size:13px">
    <span style="font-weight:650">S</span>
    <span class="eq-stock-indicator" data-status="low" data-size="s">
      <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="14" style="--_size:14px" 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>
      <span class="eq-stock-indicator-label">Only 2 left</span>
    </span>
  </li>
  <li style="display:flex;justify-content:space-between;align-items:center;gap:12px;padding-bottom:6px;border-bottom:1px solid var(--eq-line);font-size:13px">
    <span style="font-weight:650">M</span>
    <span class="eq-stock-indicator" data-status="in-stock" data-size="s">
      <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="14" style="--_size:14px" 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>
      <span class="eq-stock-indicator-label">In stock</span>
    </span>
  </li>
  <li style="display:flex;justify-content:space-between;align-items:center;gap:12px;padding-bottom:6px;border-bottom:1px solid var(--eq-line);font-size:13px">
    <span style="font-weight:650">L</span>
    <span class="eq-stock-indicator" data-status="in-stock" data-size="s">
      <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="14" style="--_size:14px" 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>
      <span class="eq-stock-indicator-label">In stock</span>
    </span>
  </li>
  <li style="display:flex;justify-content:space-between;align-items:center;gap:12px;padding-bottom:6px;border-bottom:1px solid var(--eq-line);font-size:13px">
    <span style="font-weight:650">XL</span>
    <span class="eq-stock-indicator" data-status="backorder" data-size="s">
      <svg aria-hidden="true" focusable="false" class="eq-icon eq-stock-indicator-glyph" data-size="14" style="--_size:14px" 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 7v5l3.5 2.5" />
      </svg>
      <span class="eq-stock-indicator-label">Ships from <time dateTime="2026-12-18">December 18</time></span>
    </span>
  </li>
</ul>

Props

PropTypeDefaultWhat it does
status"in-stock" | "low" | "out" | "backorder"noneWhich state to show. Sets the glyph, its colour and the default wording.
countnumbernoneUnits left. Low stock then reads "Only 3 left".
datestringnoneISO date. Out of stock reads "Back in stock" and backorder "Ships from" this date, in a time element.
localestring"en-US"Formats the date, in UTC so server and browser agree.
size"s" | "m""m"s for dense lists of variants.
childrenReactNodenoneReplaces the default wording. The glyph for the status stays.
...restComponentProps<"span">noneEvery native span attribute, including lang and id.

Anatomy

root.eq-stock-indicatorAn inline span; data-status picks the tone.
glyph.eq-stock-indicator-glyphA tick, a warning sign, a crossed circle or a clock, decorative.
label.eq-stock-indicator-label

Notes

  • Each status has its own glyph shape and its own words, so colour is never the only signal.
  • The glyph is hidden from assistive tech; the text carries the meaning.
  • Static text, not a live region: when stock changes after a choice of variant, announce it from the place that changed it.
  • The default wording is English. For another language pass children and set lang on the root.
  • Renders on the server with no client JavaScript.

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/stock-indicator.tsx62 lines
import type { ComponentProps, ReactNode } from "react";
import { Icon } from "./icon";
import "./stock-indicator.css";

export type StockStatus = "in-stock" | "low" | "out" | "backorder";

export type StockIndicatorProps = ComponentProps<"span"> & {
  status: StockStatus;
  /** Units left. Shown for low stock as "Only 3 left". */
  count?: number;
  /** ISO date. Out of stock reads "Back in stock" and backorder "Ships from" this date. */
  date?: string;
  /** Formats the date. The default wording is English; pass children to reword it. */
  locale?: string;
  size?: "s" | "m";
  /** Replaces the default wording. The glyph for the status stays. */
  children?: ReactNode;
};

const glyph = {
  "in-stock": "check-circle",
  low: "warning",
  out: "close-circle",
  backorder: "clock",
} as const;

export function StockIndicator({
  status,
  count,
  date,
  locale = "en-US",
  size = "m",
  className,
  children,
  ...rest
}: StockIndicatorProps) {
  const when =
    date === undefined ? null : (
      <time dateTime={date}>
        {new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", timeZone: "UTC" }).format(new Date(date))}
      </time>
    );

  let text: ReactNode;
  if (children != null) text = children;
  else if (status === "in-stock") text = "In stock";
  else if (status === "low") text = count === undefined ? "Low stock" : `Only ${count} left`;
  else if (status === "out") text = when ? <>Back in stock {when}</> : "Out of stock";
  else text = when ? <>Ships from {when}</> : "On backorder";

  return (
    <span
      {...rest}
      className={["eq-stock-indicator", className].filter(Boolean).join(" ")}
      data-status={status}
      data-size={size}
    >
      <Icon name={glyph[status]} size={size === "s" ? 14 : 16} className="eq-stock-indicator-glyph" />
      <span className="eq-stock-indicator-label">{text}</span>
    </span>
  );
}
components/equip/stock-indicator.css43 lines
@layer equip {
  .eq-stock-indicator {
    --_tone: var(--eq-ok);

    display: inline-flex;
    align-items: center;
    gap: var(--eq-space-1);
    color: var(--eq-ink);
    font: var(--eq-weight) var(--eq-text-s) / 1.3 var(--eq-font);
    font-variant-numeric: tabular-nums;
    vertical-align: middle;
  }

  .eq-stock-indicator[data-size="s"] {
    font-size: var(--eq-text-xs);
  }

  .eq-stock-indicator[data-status="low"] {
    --_tone: var(--eq-warn);
  }

  .eq-stock-indicator[data-status="out"] {
    --_tone: var(--eq-danger);
  }

  .eq-stock-indicator[data-status="backorder"] {
    --_tone: var(--eq-ink-2);
  }

  /* Each status has its own glyph shape as well as its colour, so neither carries it alone. */
  .eq-stock-indicator-glyph {
    color: var(--_tone);
  }

  .eq-stock-indicator[data-status="low"] .eq-stock-indicator-label,
  .eq-stock-indicator[data-status="out"] .eq-stock-indicator-label {
    font-weight: var(--eq-weight-strong);
  }

  .eq-stock-indicator-label time {
    white-space: nowrap;
  }
}

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