Reticle@etched/reticle 0.0.0

Stat tile

A label, a large value and an optional detail line, on a card. The app lays tiles out in its own grid. With onClick the tile is a button; with render, a link. Then the whole tile takes the hover fill and the focus ring. Its accessible name is its text, in order. Set aria-label for a shorter one.

Profiles saved1,284of 1,402 profiles
PendingDid not load
import { type CSSProperties } from "react";
import { StatTile } from "@etched/reticle/stat-tile";

/** The app's grid: as many 12 rem columns as fit. Reticle does not lay tiles out. */
const tiles: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(12rem, 1fr))", gap: "var(--et-space-4)" };

export function Preview() {
  return (
    <div style={{ ...tiles, gridTemplateColumns: "repeat(2, minmax(0, 1fr))", inlineSize: "100%" }}>
      <StatTile label="Profiles saved" value="1,284" detail="of 1,402 profiles" />
      <StatTile label="Pending" value={null} detail="Did not load" />
    </div>
  );
}

Installation

import { StatTile } from "@etched/reticle/stat-tile";

Examples

In a grid

Orbit's profile enrichment. The app sets the grid; tiles in a row take the same height.

Profiles saved1,2841,402 unique LinkedIn profiles
Pending37Waiting or collecting
Needs attention8152 failed · 21 uncertain · 8 cancelled
Awaiting profile resolution0Missing or unresolved LinkedIn identity
import { type CSSProperties } from "react";
import { StatTile } from "@etched/reticle/stat-tile";

/** The app's grid: as many 12 rem columns as fit. Reticle does not lay tiles out. */
const tiles: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(12rem, 1fr))", gap: "var(--et-space-4)" };

export function InAGrid() {
  return (
    <div style={tiles}>
      <StatTile label="Profiles saved" value="1,284" detail="1,402 unique LinkedIn profiles" />
      <StatTile label="Pending" value="37" detail="Waiting or collecting" />
      <StatTile label="Needs attention" value="81" detail="52 failed · 21 uncertain · 8 cancelled" />
      <StatTile label="Awaiting profile resolution" value="0" detail="Missing or unresolved LinkedIn identity" />
    </div>
  );
}

Not available

With no value, the tile shows a dash, and screen readers read "Not available". A count of 0 still shows 0.

Lots testedThe count did not load
Chips binned0This week
import { type CSSProperties } from "react";
import { StatTile } from "@etched/reticle/stat-tile";

/** The app's grid: as many 12 rem columns as fit. Reticle does not lay tiles out. */
const tiles: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(12rem, 1fr))", gap: "var(--et-space-4)" };

export function NotAvailable() {
  return (
    <div style={tiles}>
      <StatTile label="Lots tested" value={null} detail="The count did not load" />
      <StatTile label="Chips binned" value={0} detail="This week" />
    </div>
  );
}

Buttons

With onClick, the whole tile is a button, for example a filter for the table below it. Orbit's referral approvals.

Showing all referrals

import { useState, type CSSProperties } from "react";
import { Text } from "@etched/reticle/text";
import { StatTile } from "@etched/reticle/stat-tile";

/** The app's grid: as many 12 rem columns as fit. Reticle does not lay tiles out. */
const tiles: CSSProperties = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(12rem, 1fr))", gap: "var(--et-space-4)" };

const decisions = [
  { key: "accepted", label: "Accepted", count: 412 },
  { key: "passed", label: "Passed", count: 388 },
  { key: "waiting", label: "Not yet approved", count: 204 },
];

const total = decisions.reduce((sum, { count }) => sum + count, 0);

export function Buttons() {
  const [filter, setFilter] = useState("");

  return (
    <div style={{ display: "grid", gap: "var(--et-space-3)" }}>
      <div style={tiles}>
        {decisions.map(({ key, label, count }) => (
          <StatTile
            key={key}
            label={label}
            value={count.toLocaleString()}
            detail={`${Math.round((count / total) * 100)}% of ${total.toLocaleString()} referrals`}
            onClick={() => setFilter(label)}
          />
        ))}
      </div>
      <Text variant="secondary" role="status">
        {filter ? `Showing: ${filter}` : "Showing all referrals"}
      </Text>
    </div>
  );
}

API reference

PropTypeDefaultDescription
label *ReactNode–What the value counts, for example "Profiles saved".
value *ReactNode–The value, formatted by the app, for example count.toLocaleString(). Figures are tabular. null, undefined or "" shows "—", which screen readers read as "Not available". 0 shows 0.
detailReactNode–A line under the value, such as a breakdown or a share of the total.
onClickMouseEventHandler<HTMLElement>–Makes the whole tile a button that calls this, for example to filter the table below.
renderReactElement<{ className?: string }>–Makes the whole tile another element with the tile's look, such as a link: render={<a href="/candidates?status=offer" />} or a router link.