Skip to main content

ns-ui

Glaze Crawl Heal

A live status badge drawn as a glaze surface mid-firing: gas bubbles burst through the molten glaze and the melt either flows back over the crater (heals) or, when surface tension has failed, pulls away into a ragged bare patch that never closes (crawling). Three real ceramics behaviours standing in for operational, degraded and down.

Use when Pick glaze-crawl-heal for a small, always-on system status dot/badge whose three states must read from behaviour and value alone, with no hue — its motion is continuous bubble burst-and-heal, never a static glyph. status-sphere-dots and status-glyph-cadence are the better pick for a single in-progress process (an assistant's own thinking/searching/done cadence) rather than a persisted up/degraded/down system state; craze-rule is unrelated — a one-shot section divider, not a status control.

Install

npx shadcn add https://design.helpmarq.com/r/glaze-crawl-heal.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Source
registry/core/glaze-crawl-heal/component.tsx
"use client";

import { useLayoutEffect, useRef } from "react";

// ---------------------------------------------------------------------------
// GlazeCrawlHeal — a live status badge drawn as a glaze surface mid-firing,
// not three arbitrary colour chips. In a kiln the glaze goes molten and gas
// bubbles rise through the melt and burst at the surface, leaving craters;
// whether the melt's viscosity and surface tension let it FLOW BACK over
// that crater (heals smooth) or not (crawling — the glaze pulls away from
// the body into a bare patch that never closes) is a real, named ceramics
// defect, not an invented metaphor. That gives the badge one physical state
// axis instead of a colour code: healthy = bubbles burst and heal in well
// under a second; degraded = the melt is thicker, craters linger for
// seconds before closing; down = surface tension has failed outright —
// bubbles still burst (gas still escapes) but the melt never flows back, so
// ragged, beaded-rim bare patches accumulate and persist. Unlike craze-rule
// (a static fracture-crack divider that draws in once and idles), this has
// no crack geometry at all — it is continuous bubble nucleation, bursting
// and healing/failing-to-heal, forever, because the glaze stays molten for
// as long as the system is up. The canvas is pure decoration on top of a
// real, always-present text status (STATUS_LABEL) inside a role="status"
// aria-live region — colour never carries the message alone. Ink is read
// once via getComputedStyle(canvas).color (the canvas carries text-
// foreground) in useLayoutEffect, before first paint, and re-read on a
// documentElement class MutationObserver; craters/patches are punched with
// globalCompositeOperation="destination-out" rather than painted in a
// second token, so a "hole" always reveals whatever sits behind the badge —
// dark-on-light in light theme, light-on-dark in dark, for free. Zero deps,
// one DPR-capped canvas, one rAF loop paused on prefers-reduced-motion,
// tab-hidden and unmount.
// ---------------------------------------------------------------------------

export type GlazeStatus = "healthy" | "degraded" | "down";

export interface GlazeCrawlHealProps {
  /** the kiln state to render. default "healthy". */
  status?: GlazeStatus;
  /**
   * accessible text announced via the component's own role="status" region
   * on every change. Defaults to the plain status name ("Operational").
   */
  label?: string;
  /** render `label` as a visible text node beside the badge (still announced either way). default true — a status control's text is not optional. */
  showLabel?: boolean;
  /** badge diameter in px. Reads down to ~14; below that the mechanic itself stops resolving and only the text still carries the state — see component header. default 16. */
  size?: number;
  /** extra classes merged onto the rendered root element */
  className?: string;
}

const STATUS_LABEL: Record<GlazeStatus, string> = {
  healthy: "Operational",
  degraded: "Degraded",
  down: "Down",
};

const TAU = Math.PI * 2;
const JAG_POINTS = 11; // ragged-edge vertex count for a crawled (bare) patch

// Per-status kiln parameters. healMs is the viscosity/surface-tension read:
// short = fluid enough to close fast, long = it only just manages to close.
// A "down" patch never gets a healMs of its own — it doesn't heal — until
// the badge recovers to a status that has one (see the status-change effect
// below), at which point the melt finally flows back over it.
const BUBBLE_CONFIG: Record<
  "healthy" | "degraded",
  {
    spawnMs: [number, number];
    openMs: number;
    healMs: [number, number];
    maxRFrac: number;
    maxConcurrent: number;
  }
> = {
  healthy: { spawnMs: [650, 1500], openMs: 150, healMs: [350, 550], maxRFrac: 0.3, maxConcurrent: 1 },
  degraded: { spawnMs: [1100, 2200], openMs: 220, healMs: [1700, 3100], maxRFrac: 0.42, maxConcurrent: 2 },
};
const PATCH_CONFIG = {
  spawnMs: [900, 1800] as [number, number],
  creepTauMs: 1300,
  maxRFrac: 0.52,
  maxConcurrent: 3,
};

function easeOutCubic(t: number) {
  const p = 1 - t;
  return 1 - p * p * p;
}
function easeInOutCubic(t: number) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

/** mulberry32 — small, fast, deterministic given a seed */
function mulberry32(seed: number) {
  let s = seed | 0;
  return () => {
    s = (s + 0x6d2b79f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/** per-vertex radius multipliers (0.72..1.06) for a crawled patch's ragged rim */
function makeJag(seed: number): number[] {
  const rng = mulberry32(seed);
  const out: number[] = [];
  for (let i = 0; i < JAG_POINTS; i++) out.push(0.72 + rng() * 0.34);
  return out;
}

// fixed seeds -> deterministic, byte-stable jag shapes for the
// prefers-reduced-motion freeze frame (no Math.random there, ever).
const DOWN_JAG_A = makeJag(101);
const DOWN_JAG_B = makeJag(202);

type Feature = {
  kind: "bubble" | "patch";
  x: number;
  y: number; // offset from disc centre, CSS px
  maxR: number; // CSS px
  bornAt: number;
  openMs: number;
  healMs: number; // set once healing begins; Infinity/unused until then
  healingSince: number | null;
  healStartR: number;
  creepTau: number;
  wobblePhase: number;
  jag: number[] | null; // null = perfect circle (bubble); set = ragged patch
};

type DrawSpec = {
  x: number;
  y: number;
  r: number;
  jag: number[] | null;
  rimAlpha: number;
  flash: number;
};

function drawFeature(
  ctx: CanvasRenderingContext2D,
  ink: string,
  cx: number,
  cy: number,
  R0: number,
  f: DrawSpec,
) {
  if (f.r <= 0.3) return;
  ctx.save();
  ctx.translate(cx + f.x, cy + f.y);

  ctx.globalCompositeOperation = "destination-out";
  ctx.beginPath();
  if (f.jag) {
    for (let i = 0; i < JAG_POINTS; i++) {
      const a = (i / JAG_POINTS) * TAU;
      const rad = f.r * f.jag[i]!;
      const px = Math.cos(a) * rad;
      const py = Math.sin(a) * rad;
      if (i === 0) ctx.moveTo(px, py);
      else ctx.lineTo(px, py);
    }
    ctx.closePath();
  } else {
    ctx.arc(0, 0, f.r, 0, TAU);
  }
  ctx.fill();

  ctx.globalCompositeOperation = "source-over";
  if (f.jag) {
    // beaded rim: glaze that pulled away balls up at the receding edge
    // instead of flowing back — the visible signature of crawling.
    ctx.fillStyle = ink;
    for (let i = 0; i < JAG_POINTS; i++) {
      const a = (i / JAG_POINTS) * TAU;
      const rad = f.r * f.jag[i]!;
      const px = Math.cos(a) * rad;
      const py = Math.sin(a) * rad;
      ctx.globalAlpha = 0.5 + 0.35 * ((i * 5) % 4) / 4;
      ctx.beginPath();
      ctx.arc(px, py, Math.max(0.55, R0 * 0.045), 0, TAU);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  } else if (f.rimAlpha > 0) {
    // healing crater's raised lip
    ctx.strokeStyle = ink;
    ctx.globalAlpha = f.rimAlpha;
    ctx.lineWidth = Math.max(0.6, R0 * 0.05);
    ctx.beginPath();
    ctx.arc(0, 0, f.r + R0 * 0.02, 0, TAU);
    ctx.stroke();
    ctx.globalAlpha = 1;
  }
  if (f.flash > 0) {
    ctx.strokeStyle = ink;
    ctx.globalAlpha = f.flash;
    ctx.lineWidth = Math.max(0.8, R0 * 0.09);
    ctx.beginPath();
    ctx.arc(0, 0, f.r * 1.02, 0, TAU);
    ctx.stroke();
    ctx.globalAlpha = 1;
  }
  ctx.restore();
}

function renderFrame(
  ctx: CanvasRenderingContext2D,
  ink: string,
  w: number,
  h: number,
  cx: number,
  cy: number,
  R0: number,
  sheenAngle: number,
  specs: DrawSpec[],
) {
  ctx.clearRect(0, 0, w, h);
  ctx.globalCompositeOperation = "source-over";
  ctx.fillStyle = ink;
  ctx.globalAlpha = 1;
  ctx.beginPath();
  ctx.arc(cx, cy, R0, 0, TAU);
  ctx.fill();

  // a slow-drifting specular dim patch — the melt still reads as liquid in
  // the gaps between bubble events, never a flat dead disc.
  ctx.globalCompositeOperation = "destination-out";
  const sx = cx + Math.cos(sheenAngle) * R0 * 0.5;
  const sy = cy + Math.sin(sheenAngle) * R0 * 0.5;
  ctx.globalAlpha = 0.08;
  ctx.beginPath();
  ctx.ellipse(sx, sy, R0 * 0.42, R0 * 0.3, sheenAngle, 0, TAU);
  ctx.fill();
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = "source-over";

  for (const f of specs) drawFeature(ctx, ink, cx, cy, R0, f);
}

/** deterministic freeze frame per status — craters/patches at varied stages of healing, never mid-t0 */
function staticSpecs(status: GlazeStatus, R0: number): DrawSpec[] {
  if (status === "healthy") {
    const maxR = R0 * BUBBLE_CONFIG.healthy.maxRFrac;
    return [
      { x: -R0 * 0.3, y: -R0 * 0.12, r: maxR * 0.55, jag: null, rimAlpha: 0.35, flash: 0 },
      { x: R0 * 0.34, y: R0 * 0.28, r: maxR * 0.15, jag: null, rimAlpha: 0.45, flash: 0 },
    ];
  }
  if (status === "degraded") {
    const maxR = R0 * BUBBLE_CONFIG.degraded.maxRFrac;
    return [
      { x: -R0 * 0.3, y: -R0 * 0.22, r: maxR * 0.92, jag: null, rimAlpha: 0.5, flash: 0 },
      { x: R0 * 0.28, y: R0 * 0.1, r: maxR * 0.55, jag: null, rimAlpha: 0.4, flash: 0 },
      { x: 0, y: R0 * 0.36, r: maxR * 0.2, jag: null, rimAlpha: 0.45, flash: 0 },
    ];
  }
  const maxR = R0 * PATCH_CONFIG.maxRFrac;
  return [
    { x: -R0 * 0.24, y: -R0 * 0.18, r: maxR * 0.9, jag: DOWN_JAG_A, rimAlpha: 0, flash: 0 },
    { x: R0 * 0.3, y: R0 * 0.26, r: maxR * 0.4, jag: DOWN_JAG_B, rimAlpha: 0, flash: 0 },
  ];
}

export function GlazeCrawlHeal({
  status = "healthy",
  label,
  showLabel = true,
  size = 16,
  className = "",
}: GlazeCrawlHealProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const engineRef = useRef<{ setStatus: (s: GlazeStatus) => void } | null>(null);

  // -- token derivation happens before first paint, so nothing ever paints
  // with a default/unset ink colour --
  useLayoutEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    let ink = "";
    let w = 0;
    let h = 0;
    let cx = 0;
    let cy = 0;
    let R0 = 0;
    let raf = 0;
    let nextSpawnAt = 0;
    let statusCur: GlazeStatus = status;
    const features: Feature[] = [];

    const readInk = () => {
      ink = getComputedStyle(canvas).color;
    };

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      w = size;
      h = size;
      canvas.width = Math.max(1, Math.round(w * dpr));
      canvas.height = Math.max(1, Math.round(h * dpr));
      canvas.style.width = `${w}px`;
      canvas.style.height = `${h}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      cx = w / 2;
      cy = h / 2;
      R0 = (Math.min(w, h) / 2) * 0.86;
    };

    const drawStatic = () => {
      renderFrame(ctx, ink, w, h, cx, cy, R0, -0.8, staticSpecs(statusCur, R0));
    };

    const maxRFor = (kind: "healthy" | "degraded" | "down") =>
      R0 * (kind === "down" ? PATCH_CONFIG.maxRFrac : BUBBLE_CONFIG[kind].maxRFrac);

    let featureSeed = 1;

    const maybeSpawn = (now: number) => {
      if (now < nextSpawnAt) return;
      if (statusCur === "down") {
        if (features.length < PATCH_CONFIG.maxConcurrent) {
          const margin = Math.max(0, R0 * 0.9 - maxRFor("down"));
          const ang = Math.random() * TAU;
          const dist = Math.random() * margin;
          features.push({
            kind: "patch",
            x: Math.cos(ang) * dist,
            y: Math.sin(ang) * dist,
            maxR: maxRFor("down") * (0.6 + Math.random() * 0.4),
            bornAt: now,
            openMs: 0,
            healMs: 0,
            healingSince: null,
            healStartR: 0,
            creepTau: PATCH_CONFIG.creepTauMs * (0.8 + Math.random() * 0.5),
            wobblePhase: Math.random() * TAU,
            jag: makeJag(featureSeed++),
          });
        }
        const [a, b] = PATCH_CONFIG.spawnMs;
        nextSpawnAt = now + a + Math.random() * (b - a);
        return;
      }
      const cfg = BUBBLE_CONFIG[statusCur];
      if (features.length < cfg.maxConcurrent) {
        const margin = Math.max(0, R0 * 0.9 - maxRFor(statusCur));
        const ang = Math.random() * TAU;
        const dist = Math.random() * margin;
        const [hMin, hMax] = cfg.healMs;
        features.push({
          kind: "bubble",
          x: Math.cos(ang) * dist,
          y: Math.sin(ang) * dist,
          maxR: R0 * cfg.maxRFrac * (0.7 + Math.random() * 0.5),
          bornAt: now,
          openMs: cfg.openMs,
          healMs: hMin + Math.random() * (hMax - hMin),
          healingSince: null,
          healStartR: 0,
          creepTau: 0,
          wobblePhase: 0,
          jag: null,
        });
      }
      const [a, b] = cfg.spawnMs;
      nextSpawnAt = now + a + Math.random() * (b - a);
    };

    const tick = (now: number) => {
      raf = requestAnimationFrame(tick);
      if (document.hidden) return;

      maybeSpawn(now);

      const specs: DrawSpec[] = [];
      for (let i = features.length - 1; i >= 0; i--) {
        const f = features[i]!;
        const age = now - f.bornAt;
        let r = 0;
        let rimAlpha = 0;
        let flash = 0;
        let dead = false;

        if (f.kind === "bubble") {
          if (f.healingSince == null) {
            if (age < f.openMs) {
              r = f.maxR * easeOutCubic(f.openMs > 0 ? age / f.openMs : 1);
            } else {
              f.healingSince = f.bornAt + f.openMs;
            }
          }
          if (f.healingSince != null) {
            const hAge = now - f.healingSince;
            if (hAge >= f.healMs) {
              dead = true;
            } else {
              r = f.maxR * (1 - easeInOutCubic(hAge / f.healMs));
              rimAlpha = 0.15 + 0.35 * (1 - hAge / f.healMs);
            }
          }
          const peak = f.bornAt + f.openMs;
          const df = Math.abs(now - peak);
          if (df < 120) flash = 1 - df / 120;
        } else {
          // patch — burst once (gas still escapes) then either creeps
          // (still failing to close) or, post-recovery, heals like a bubble
          if (f.healingSince == null) {
            r = f.maxR * (1 - Math.exp(-age / f.creepTau));
            r *= 1 + 0.03 * Math.sin(age * 0.0025 + f.wobblePhase);
            if (age < 130) flash = 1 - age / 130;
          } else {
            const hAge = now - f.healingSince;
            if (hAge >= f.healMs) {
              dead = true;
            } else {
              r = f.healStartR * (1 - easeInOutCubic(hAge / f.healMs));
            }
          }
        }

        if (dead) {
          features.splice(i, 1);
          continue;
        }
        specs.push({ x: f.x, y: f.y, r, jag: f.jag, rimAlpha, flash });
      }

      const sheenAngle = now * 0.00015;
      renderFrame(ctx, ink, w, h, cx, cy, R0, sheenAngle, specs);
    };

    const start = () => {
      if (reduced || raf) return;
      raf = requestAnimationFrame(tick);
    };
    const stop = () => {
      cancelAnimationFrame(raf);
      raf = 0;
    };

    engineRef.current = {
      setStatus: (next) => {
        const prev = statusCur;
        statusCur = next;
        if (prev === "down" && next !== "down") {
          // conditions recovered — the melt can finally flow back over
          // whatever crawled while it was down.
          const now = performance.now();
          const cfg = BUBBLE_CONFIG[next];
          const [hMin, hMax] = cfg.healMs;
          for (const f of features) {
            if (f.kind === "patch" && f.healingSince == null) {
              const age = now - f.bornAt;
              const r =
                f.maxR *
                (1 - Math.exp(-age / f.creepTau)) *
                (1 + 0.03 * Math.sin(age * 0.0025 + f.wobblePhase));
              f.healStartR = r;
              f.healingSince = now;
              f.healMs = hMin + Math.random() * (hMax - hMin);
            }
          }
        }
        if (reduced) drawStatic();
        else start();
      },
    };

    const onVis = () => {
      if (!document.hidden) start();
    };
    document.addEventListener("visibilitychange", onVis);

    const mo = new MutationObserver(() => {
      readInk();
      if (reduced) drawStatic();
    });
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });

    readInk();
    resize();
    if (reduced) drawStatic();
    else start();

    return () => {
      engineRef.current = null;
      stop();
      mo.disconnect();
      document.removeEventListener("visibilitychange", onVis);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [size]);

  useLayoutEffect(() => {
    engineRef.current?.setStatus(status);
  }, [status]);

  const text = label ?? STATUS_LABEL[status];

  return (
    <span
      role="status"
      aria-live="polite"
      data-glaze-status={status}
      className={`inline-flex shrink-0 items-center gap-2 ${className}`}
    >
      <span
        aria-hidden
        className="inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border"
        style={{ width: size, height: size }}
      >
        <canvas ref={canvasRef} className="block text-foreground" />
      </span>
      {showLabel ? (
        <span className="font-mono text-xs text-foreground">{text}</span>
      ) : (
        <span className="sr-only">{text}</span>
      )}
    </span>
  );
}
Build spec

Build <GlazeCrawlHeal status? label? showLabel? size? className?> as a live status badge sourced from real glaze-firing behaviour, not an invented colour code. GLAZE PHYSICS: in a kiln the glaze goes molten and gas bubbles rise through the melt and burst at the surface, leaving craters; whether the melt's viscosity and surface tension let it flow back over the crater (heal) or not (crawling — the glaze pulls away from the clay body into a bare patch that never closes) is a documented ceramics defect, and it is the component's entire state axis: healthy = bubbles burst and heal in well under a second (short healMs, thin/fluid melt), degraded = the melt is thicker so craters linger for one to three seconds before closing (long healMs, same mechanic, different viscosity), down = surface tension has failed outright — bubbles still burst (gas still escapes the melt) but the melt never flows back, so ragged, beaded-rim bare patches accumulate and persist until conditions recover. RENDERING: one DPR-capped <canvas> circle inset inside a border-border ring. Every frame redraws a solid disc filled with the canvas's own colour (getComputedStyle(canvas).color, the canvas carries text-foreground), then punches each live bubble/patch into that disc via globalCompositeOperation='destination-out' at its current radius — a perfect circle for a bubble, an 11-vertex ragged polygon (per-vertex radius 0.72-1.06x nominal, from a seeded mulberry32 walk) for a crawled patch. Punching with destination-out rather than painting a second token means a 'hole' always reveals whatever is genuinely behind the badge, so molten ink is dark-on-light in light theme and light-on-dark in dark theme for free, with no theme branch in the draw code. A healing crater gets a thin foreground rim stroke (its raised lip) that fades as it closes; a crawled patch gets small foreground beads at its ragged vertices instead (glaze balled up at the receding edge rather than flowing back) and never gets a rim, because it isn't healing. A slow-drifting, low-alpha destination-out ellipse (0.08 alpha, ~0.00015 rad/ms) sweeps across the disc as a specular sheen so the surface still reads as liquid in the seconds between bubble events. LIFECYCLE: a bubble opens 0->maxR over openMs (ease-out-cubic, the pop) then closes maxR->0 over a per-status randomised healMs (ease-in-out-cubic) and is removed; a momentary full-strength rim flash brackets the peak, selling the burst. A down-state patch bursts open the same way but then creeps 0->maxR on an exponential approach (r = maxR*(1-e^(-age/tau)), tau ~1.3s) with a small ambient wobble, asymptotically approaching but never reaching maxR — it simply never gets a healMs, so it never closes, which is what makes 'never heals' true rather than merely slow. Concurrency and nucleation cadence differ per status (healthy: 1 bubble, 650-1500ms between spawns; degraded: up to 2, 1100-2200ms; down: up to 3 patches, 900-1800ms) so the badge is genuinely alive at rest at every status, with no settled end state — screenshots taken a mount, +2.5s and +5s all differ. RECOVERY: when status transitions away from 'down', every live patch is handed a real healMs drawn from the new status's own healthy/degraded range and its current creeping radius becomes its heal-start radius, so it finally flows back closed on the same ease-in-out curve a bubble uses — the melt genuinely repairs itself once conditions are fixed, it does not just vanish. A11Y: the root is a <span role='status' aria-live='polite'> so every status change is announced by assistive tech; the canvas is aria-hidden and is decoration only — the accessible name and the announced text are one real string (STATUS_LABEL: 'Operational' / 'Degraded' / 'Down', or a caller-supplied label), rendered as a visible text node by default (showLabel) or as a visually-hidden span when showLabel is false, so a colour-blind or screen-reader user always gets the same information a sighted user gets from the canvas. No throttling logic is needed beyond that — status is caller-driven, discrete state, not a per-frame value. TOKENS: ink is read once via getComputedStyle(canvas).color in useLayoutEffect, before first paint, and re-read on a documentElement class MutationObserver; every stroke/fill on the canvas is that one ink value composited with alpha, so nothing is a second colour — --ns-accent never appears, because this is ambient state, not interaction chrome, and the badge's own outline ring is the one legitimate --border use. SCALE: default size is 16px, a realistic inline status-dot scale, and the mechanic (a bursting/healing/crawling disc) is proportioned to read down to about 14px; below roughly 10-12px the ragged-patch texture and rim detail stop resolving to the eye and the text label becomes the only reliable channel — stated plainly rather than hidden, which is exactly why the text is never optional to the accessibility tree. REDUCED MOTION: window.matchMedia('(prefers-reduced-motion: reduce)') freezes the canvas on one deterministic, non-t0 still per status built from fixed offsets/radii (and, for 'down', two fixed-seed ragged shapes) rather than any live simulation state — healthy shows one crater mid-heal and one nearly closed, degraded shows three craters at clearly different open/closing stages, down shows one large stalled patch and one smaller forming one — so the mechanic is legible in a single frame and the frame is byte-stable across repeated captures; status text and the aria-live region stay fully live regardless of the motion preference. Props: status ('healthy' | 'degraded' | 'down', default 'healthy'), label (accessible override), showLabel (default true), size (px, default 16), className. Zero dependencies, no canvas resize observer needed since size is a controlled prop rather than a fluid container.

Props

PropTypeDefaultDescription
status?GlazeStatus"healthy"the kiln state to render. default "healthy".
label?stringaccessible text announced via the component's own role="status" region on every change. Defaults to the plain status name ("Operational").
showLabel?booleantruerender `label` as a visible text node beside the badge (still announced either way). default true — a status control's text is not optional.
size?number16badge diameter in px. Reads down to ~14; below that the mechanic itself stops resolving and only the text still carries the state — see component header. default 16.
className?stringextra classes merged onto the rendered root element