Oct 2, 2026 · Component

TabularTimer

The same clock twice, in the same font. The top row uses the font's default proportional figures and changes width on every tick; the bottom row asks for tabular figures and holds perfectly still. One CSS property separates them.

Proportional0.00sQuerying warehouse
Tabular0.00sQuerying warehouse
Same clock, same font. Only one of them keeps its width.

Watch the dashed edge: one number breathes, the other holds.

What it shows, and why

Digits in most text fonts are drawn like letters: a 1 is narrower than a 0. Fine in prose, wrong in anything that updates in place. A live timer, a price that refreshes, a percentage in a dashboard: each tick redraws a number of a different width, and everything after it shifts. The reader's eye tracks the jitter instead of the value.

font-variant-numeric: tabular-nums asks the same font for its fixed-width digits, and the number stops moving. This site's own font, Geist, defaults to proportional figures, which is what the top row demonstrates: measured here in the browser, “11.11s” runs 102.6px where “00.00s” runs 164.5px, and in tabular both hold 151.6px. The clock itself reads performance.now() each frame, so the frame rate decides how often it looks, never what it says.

The place this bites hardest is the one this site just spent an essay on: an AI's thinking indicator, counting its elapsed seconds while you stare at it. That figure updates every frame in front of a reader with nothing else to do, which is why it is the worst place for a trembling number, and the whole subject of Designing the wait.

Use it

One file, Tailwind only, no other dependency. Everything is in the single block below: the usage sits at the top as a comment, the component follows, and Copy takes the whole thing.

The demo only proves its point if your font's default figures are proportional. If the number does not tremble, your font already ships tabular (or monospaced) digits, and the lesson is free.

tabular-timer.tsx
// Usage: copy this whole file, then
//
//   import { TabularTimer } from "@/components/tabular-timer";
//
//   <TabularTimer />
//
// No props: the figure is the argument.

"use client";

import { useCallback, useEffect, useRef, useState } from "react";

const ACCENT = "#8a3a5c";

const HAIRLINE = "border-black/10 dark:border-white/10";

function format(ms: number) {
  return `${(ms / 1000).toFixed(2)}s`;
}

function Row({
  label,
  numeric,
  value,
  good,
}: {
  label: string;
  numeric: "proportional-nums" | "tabular-nums";
  value: string;
  good?: boolean;
}) {
  return (
    <div
      className={`grid grid-cols-[1fr_28px] items-center gap-4 py-[22px] min-[521px]:grid-cols-[112px_1fr_28px] ${HAIRLINE}`}
    >
      <span className="text-[15px] opacity-70 max-[520px]:col-span-full">
        {label}
      </span>
      <span className="flex min-w-0 items-baseline gap-3.5 overflow-hidden whitespace-nowrap">
        <span
          className="rounded border border-dashed px-1 py-1 text-[clamp(32px,6vw,48px)] font-[450] leading-none tracking-[-0.02em]"
          style={{ fontVariantNumeric: numeric, borderColor: ACCENT }}
        >
          {value}
        </span>
        <span className="text-sm opacity-50 max-[520px]:hidden">
          Querying warehouse
        </span>
      </span>
      <span
        aria-label={good ? "Recommended" : "Not recommended"}
        className={`grid size-[26px] place-items-center rounded-full ${
          good ? "" : "bg-black/10 dark:bg-white/10"
        }`}
        style={good ? { backgroundColor: ACCENT } : undefined}
      >
        <svg
          viewBox="0 0 16 16"
          aria-hidden="true"
          className={`size-3.5 fill-none ${good ? "stroke-white" : "stroke-current opacity-70"}`}
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          {good ? (
            <path d="M4.5 8.5l2.2 2.2 4.8-5" />
          ) : (
            <path d="M5 5l6 6M11 5l-6 6" />
          )}
        </svg>
      </span>
    </div>
  );
}

export function TabularTimer() {
  const [elapsed, setElapsed] = useState(0);
  const [running, setRunning] = useState(true);

  const startRef = useRef<number | null>(null);
  const offsetRef = useRef(0);
  const frameRef = useRef<number | null>(null);

  const tick = useCallback(() => {
    if (startRef.current !== null) {
      setElapsed(offsetRef.current + performance.now() - startRef.current);
    }
    frameRef.current = requestAnimationFrame(tick);
  }, []);

  useEffect(() => {
    if (running) {
      startRef.current = performance.now();
      frameRef.current = requestAnimationFrame(tick);
    } else if (startRef.current !== null) {
      offsetRef.current += performance.now() - startRef.current;
      startRef.current = null;
    }
    return () => {
      if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
    };
  }, [running, tick]);

  const restart = () => {
    offsetRef.current = 0;
    startRef.current = performance.now();
    setElapsed(0);
    setRunning(true);
  };

  const value = format(elapsed);

  const btn =
    "cursor-pointer rounded-full px-3.5 py-[7px] text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#8a3a5c]";

  return (
    <figure className="m-0 w-full">
      <div
        className={`overflow-hidden rounded-3xl border bg-white shadow-[0_1px_3px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.05)] dark:bg-white/[0.035] dark:shadow-none ${HAIRLINE}`}
      >
                <div className="px-[clamp(20px,6vw,72px)] pb-8 pt-10 [&>div:first-child]:border-t-0 [&>div]:border-t">
          <Row label="Proportional" numeric="proportional-nums" value={value} />
          <Row label="Tabular" numeric="tabular-nums" value={value} good />
        </div>

        <div
          className={`flex flex-wrap items-center justify-end gap-3 border-t px-[clamp(16px,4vw,32px)] py-4 ${HAIRLINE}`}
        >
          <div className="inline-flex gap-2">
            <button
              type="button"
              className={`${btn} bg-transparent opacity-70`}
              onClick={() => setRunning((r) => !r)}
            >
              {running ? "Pause" : "Resume"}
            </button>
            <button
              type="button"
              className={`${btn} bg-black text-white dark:bg-white dark:text-black`}
              onClick={restart}
            >
              Restart
            </button>
          </div>
        </div>
      </div>

      <figcaption className="mt-3.5 text-center text-[13px] italic opacity-55">
        Same clock, same font. Only one of them keeps its width.
      </figcaption>
    </figure>
  );
}
The component, verbatim: this page renders this exact file.