# Mono — Theme for React

> A self-contained Swiss-minimal theme preset — a stark black-and-white (or paper-and-ink) panel on a strict grid: tight neo-grotesque heading, numbered hairline spec rows, generous negative space, and exactly one restrained accent. A keyboard-operable radiogroup of accent swatches re-tokens the panel live and a role=switch flips light/dark; the palette is fully exposed via accents and colors. The only motion is an accent rule that draws across the top, pinned to its full frame under reduced motion.

- Page: https://crucibleui.com/components/themes/mono
- Category: [themes](https://crucibleui.com/components/themes)
- Tier: free (full source)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/mono
```

Registry JSON: https://crucibleui.com/r/mono.json

Import from `@/components/crucible/mono`.

## Props (`MonoProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `string` | — | Controlled active accent id (matches an accent's `id`). |
| `defaultVariant?` | `string` | `"red" (first accent)` | Initial accent id when uncontrolled. |
| `onVariantChange?` | `(id: string) => void` | — | Fires with the next accent id whenever the switcher changes. |
| `accents?` | `MonoAccent[]` | `the three built-in accents` | Override the built-in accent set (chips + colors). |
| `mode?` | `MonoMode` | — | Controlled light/dark mode. |
| `defaultMode?` | `MonoMode` | `"dark"` | Initial mode when uncontrolled. |
| `onModeChange?` | `(mode: MonoMode) => void` | — | Fires with the next mode whenever the light/dark switch flips. |
| `colors?` | `Partial<MonoPalette>` | — | Token overrides merged over the resolved palette — makes any palette reachable. |
| `speed?` | `number` | `1` | Draw-rule animation speed multiplier. |
| `paused?` | `boolean` | `false` | Freeze the draw rule at its resting (full) frame. |
| `eyebrow?` | `string` | `"SPECIMEN — 001"` | Small mono eyebrow above the heading. |
| `heading?` | `string` | `"Form Follows"` | Tight neo-grotesque display heading. |
| `description?` | `string` | `"A strict grid, hairline rules, and one restrained accent."` | Body line under the heading. |
| `specs?` | `MonoSpec[]` | `four type-spec rows` | Numbered spec rows (auto-indexed 01…N). |
| `tag?` | `string` | `"GRID"` | Small square tag label. |
| `primaryLabel?` | `string` | `"Continue"` | Filled (accent) button label. |
| `secondaryLabel?` | `string` | `"Reset"` | Outline button label. |
| `label?` | `string` | `"Monochrome theme preset"` | Accessible label for the panel region. |
| `switcherLabel?` | `string` | `"Accent color"` | Accessible label for the accent switcher group. |
| `modeLabel?` | `string` | `"Toggle light mode"` | Accessible label for the light/dark switch. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"section">, "onChange">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { Mono, type MonoMode } from "@/components/crucible/mono";

export default function MonoDemo() {
  const [variant, setVariant] = React.useState("red");
  const [mode, setMode] = React.useState<MonoMode>("dark");

  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950 p-6">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.03),transparent_70%)]"
      />
      <div className="relative z-10 flex flex-col items-center gap-5">
        <Mono
          variant={variant}
          onVariantChange={setVariant}
          mode={mode}
          onModeChange={setMode}
        />
        <p className="text-center text-sm text-neutral-500">
          Switch the accent — click a swatch or arrow-key it — then flip
          light/dark.
        </p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.

## Source (`mono.tsx`)

```tsx
"use client";

import { animate, motion, useMotionValue } from "motion/react";
import * as React from "react";

import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { cn } from "@/lib/utils";

export type MonoMode = "light" | "dark";

export interface MonoPalette {
  /** Recessed base tone — fills the tag chip (also exposed as `--m-bg` for a host page behind the card). */
  bg: string;
  /** Card surface. */
  surface: string;
  /** Primary foreground text. */
  text: string;
  /** Muted / secondary text. */
  muted: string;
  /** Hairline divider / border color. */
  line: string;
  /** The single restrained accent — index numbers, filled button, draw rule. */
  accent: string;
  /** Text/ink color that sits on top of the accent fill. */
  accentInk: string;
}

export interface MonoAccent {
  /** Stable id used for controlled selection + React keys. */
  id: string;
  /** Chip label shown in the switcher. */
  label: string;
  /** The accent hue applied across the panel when active. */
  color: string;
  /** Optional ink color for text on the accent fill. @default "#ffffff" */
  ink?: string;
}

export interface MonoSpec {
  /** Row caption, e.g. "Leading". */
  label: string;
  /** Row value, e.g. "1.4". */
  value: string;
}

/** Base tokens per mode — accent is layered in from the active preset. */
const DARK_BASE: Omit<MonoPalette, "accent" | "accentInk"> = {
  bg: "#0a0a0a",
  surface: "#111111",
  text: "#fafafa",
  muted: "#8a8a8a",
  line: "rgba(255,255,255,0.14)",
};
const LIGHT_BASE: Omit<MonoPalette, "accent" | "accentInk"> = {
  bg: "#f4f4f2",
  surface: "#ffffff",
  text: "#0a0a0a",
  muted: "#6b6b6b",
  line: "rgba(0,0,0,0.14)",
};

/** Built-in accents — one restrained signal color, Swiss-poster primaries. */
const ACCENTS: MonoAccent[] = [
  { id: "red", label: "Red", color: "#e5322b" },
  { id: "blue", label: "Blue", color: "#2563eb" },
  { id: "green", label: "Green", color: "#0e9f6e" },
];

const DEFAULT_SPECS: MonoSpec[] = [
  { label: "Weight", value: "Medium" },
  { label: "Leading", value: "1.40" },
  { label: "Measure", value: "62ch" },
  { label: "Columns", value: "12" },
];

export interface MonoProps
  extends Omit<React.ComponentPropsWithoutRef<"section">, "onChange"> {
  /** Controlled active accent id (matches an accent's `id`). */
  variant?: string;
  /** Initial accent id when uncontrolled. @default "red" (first accent) */
  defaultVariant?: string;
  /** Fires with the next accent id whenever the switcher changes. */
  onVariantChange?: (id: string) => void;
  /** Override the built-in accent set (chips + colors). @default the three built-in accents */
  accents?: MonoAccent[];
  /** Controlled light/dark mode. */
  mode?: MonoMode;
  /** Initial mode when uncontrolled. @default "dark" */
  defaultMode?: MonoMode;
  /** Fires with the next mode whenever the light/dark switch flips. */
  onModeChange?: (mode: MonoMode) => void;
  /** Token overrides merged over the resolved palette — makes any palette reachable. */
  colors?: Partial<MonoPalette>;
  /** Draw-rule animation speed multiplier. @default 1 */
  speed?: number;
  /** Freeze the draw rule at its resting (full) frame. @default false */
  paused?: boolean;
  /** Small mono eyebrow above the heading. @default "SPECIMEN — 001" */
  eyebrow?: string;
  /** Tight neo-grotesque display heading. @default "Form Follows" */
  heading?: string;
  /** Body line under the heading. @default "A strict grid, hairline rules, and one restrained accent." */
  description?: string;
  /** Numbered spec rows (auto-indexed 01…N). @default four type-spec rows */
  specs?: MonoSpec[];
  /** Small square tag label. @default "GRID" */
  tag?: string;
  /** Filled (accent) button label. @default "Continue" */
  primaryLabel?: string;
  /** Outline button label. @default "Reset" */
  secondaryLabel?: string;
  /** Accessible label for the panel region. @default "Monochrome theme preset" */
  label?: string;
  /** Accessible label for the accent switcher group. @default "Accent color" */
  switcherLabel?: string;
  /** Accessible label for the light/dark switch. @default "Toggle light mode" */
  modeLabel?: string;
}

/**
 * Mono — a self-contained Swiss-minimal theme preset. A stark black-and-white
 * (or paper-and-ink) panel built on a strict grid: a tight neo-grotesque
 * heading, numbered hairline spec rows, generous negative space, and exactly
 * one restrained accent. A keyboard-operable radiogroup of accent chips
 * re-tokens the whole panel live, and a real `role="switch"` flips light↔dark;
 * the palette is fully exposed via `accents` and `colors`. The only motion is a
 * single accent rule that draws across the top on change — reduced motion (or
 * `paused`) pins it to its full resting frame, so the layout is otherwise
 * completely static.
 */
export function Mono({
  variant,
  defaultVariant,
  onVariantChange,
  accents,
  mode,
  defaultMode = "dark",
  onModeChange,
  colors,
  speed = 1,
  paused = false,
  eyebrow = "SPECIMEN — 001",
  heading = "Form Follows",
  description = "A strict grid, hairline rules, and one restrained accent.",
  specs,
  tag = "GRID",
  primaryLabel = "Continue",
  secondaryLabel = "Reset",
  label = "Monochrome theme preset",
  switcherLabel = "Accent color",
  modeLabel = "Toggle light mode",
  className,
  style,
  ...rest
}: MonoProps) {
  const reducedMotion = useReducedMotion();

  const list = accents && accents.length > 0 ? accents : ACCENTS;
  const rows = specs && specs.length > 0 ? specs : DEFAULT_SPECS;

  // Accent — controlled or uncontrolled.
  const isVarControlled = variant !== undefined;
  const [internalVar, setInternalVar] = React.useState(
    defaultVariant ?? list[0].id
  );
  const activeId = isVarControlled ? variant : internalVar;
  const activeAccent = list.find((a) => a.id === activeId) ?? list[0];

  // Mode — controlled or uncontrolled.
  const isModeControlled = mode !== undefined;
  const [internalMode, setInternalMode] = React.useState<MonoMode>(defaultMode);
  const activeMode = isModeControlled ? mode : internalMode;
  const isDark = activeMode === "dark";

  const base = isDark ? DARK_BASE : LIGHT_BASE;
  const t: MonoPalette = {
    ...base,
    accent: activeAccent.color,
    accentInk: activeAccent.ink ?? "#ffffff",
    ...colors,
  };

  const spd = Math.max(0.1, speed);

  // The single motion: a hairline accent rule that draws across the top of the
  // card. It re-runs when the accent or mode changes (change = re-index feel).
  // Reduced motion / paused pin it to a full-width resting frame.
  const draw = useMotionValue(reducedMotion || paused ? 1 : 0);
  React.useEffect(() => {
    if (reducedMotion || paused) {
      draw.set(1);
      return;
    }
    draw.set(0);
    const controls = animate(draw, 1, {
      duration: 0.55 / spd,
      ease: [0.16, 1, 0.3, 1],
    });
    return controls.stop;
    // Re-draw on accent/mode change.
  }, [draw, reducedMotion, paused, spd, activeId, activeMode]);

  // Roving-focus radiogroup wiring.
  const activeIdx = Math.max(
    0,
    list.findIndex((a) => a.id === activeId)
  );
  const chipRefs = React.useRef<(HTMLButtonElement | null)[]>([]);

  const select = React.useCallback(
    (i: number) => {
      const next = list[i];
      if (!next) return;
      if (!isVarControlled) setInternalVar(next.id);
      onVariantChange?.(next.id);
    },
    [list, isVarControlled, onVariantChange]
  );

  const onSwitcherKey = React.useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      let next = activeIdx;
      switch (e.key) {
        case "ArrowRight":
        case "ArrowDown":
          next = (activeIdx + 1) % list.length;
          break;
        case "ArrowLeft":
        case "ArrowUp":
          next = (activeIdx - 1 + list.length) % list.length;
          break;
        case "Home":
          next = 0;
          break;
        case "End":
          next = list.length - 1;
          break;
        default:
          return;
      }
      e.preventDefault();
      select(next);
      chipRefs.current[next]?.focus();
    },
    [activeIdx, list, select]
  );

  const toggleMode = React.useCallback(() => {
    const next: MonoMode = isDark ? "light" : "dark";
    if (!isModeControlled) setInternalMode(next);
    onModeChange?.(next);
  }, [isDark, isModeControlled, onModeChange]);

  const vars = {
    "--m-bg": t.bg,
    "--m-surface": t.surface,
    "--m-text": t.text,
    "--m-muted": t.muted,
    "--m-line": t.line,
    "--m-accent": t.accent,
    "--m-ink": t.accentInk,
  } as React.CSSProperties;

  const focusRing =
    "outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--m-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--m-surface)]";

  return (
    <section
      aria-label={label}
      data-crucible="mono"
      data-variant={activeAccent.id}
      data-mode={activeMode}
      className={cn("relative isolate w-full max-w-sm", className)}
      style={{ ...vars, ...style }}
      {...rest}
    >
      <div
        className="relative isolate flex w-full flex-col overflow-hidden rounded-lg border"
        style={{
          background: "var(--m-surface)",
          borderColor: "var(--m-line)",
          color: "var(--m-text)",
        }}
      >
        {/* The one moving part: an accent rule that draws across the top. */}
        <div
          aria-hidden
          className="absolute inset-x-0 top-0 h-[2px]"
          style={{ background: "var(--m-line)" }}
        >
          <motion.div
            className="h-full w-full origin-left"
            style={{ background: "var(--m-accent)", scaleX: draw }}
          />
        </div>

        <div className="flex flex-col gap-6 p-6 pt-7">
          {/* Header: eyebrow + light/dark switch. */}
          <div className="flex items-start justify-between gap-4">
            <p
              className="font-mono text-[0.62rem] font-medium uppercase tracking-[0.24em]"
              style={{ color: "var(--m-muted)" }}
            >
              {eyebrow}
            </p>
            <button
              type="button"
              role="switch"
              // The switch is labelled "light mode", so it is ON in light mode.
              aria-checked={!isDark}
              aria-label={modeLabel}
              onClick={toggleMode}
              className={cn(
                "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors",
                focusRing
              )}
              style={{
                borderColor: "var(--m-line)",
                background: isDark ? "transparent" : "var(--m-accent)",
              }}
            >
              <span
                className="ml-0.5 h-3.5 w-3.5 rounded-full transition-transform"
                style={{
                  background: isDark ? "var(--m-text)" : "var(--m-ink)",
                  transform: isDark ? "translateX(0)" : "translateX(16px)",
                }}
              />
            </button>
          </div>

          {/* Heading + description. */}
          <div className="flex flex-col gap-2">
            <h3 className="text-3xl font-medium leading-[1.02] tracking-[-0.02em]">
              {heading}
            </h3>
            <p
              className="max-w-[34ch] text-sm leading-relaxed"
              style={{ color: "var(--m-muted)" }}
            >
              {description}
            </p>
          </div>

          {/* Numbered spec grid — hairline dividers, accent index numbers. */}
          <div className="grid grid-cols-2">
            {rows.map((row, i) => (
              <div
                key={`${row.label}-${i}`}
                className="flex flex-col gap-1 border-t py-3 pr-4 first:pt-0 [&:nth-child(2)]:pt-0"
                style={{ borderColor: "var(--m-line)" }}
              >
                <span
                  className="font-mono text-[0.62rem] font-semibold tabular-nums"
                  style={{ color: "var(--m-accent)" }}
                >
                  {String(i + 1).padStart(2, "0")}
                </span>
                <span className="text-sm font-medium">{row.value}</span>
                <span
                  className="text-[0.7rem] uppercase tracking-wide"
                  style={{ color: "var(--m-muted)" }}
                >
                  {row.label}
                </span>
              </div>
            ))}
          </div>

          {/* Tag + buttons. */}
          <div className="flex items-center gap-3">
            <span
              className="rounded-none border px-2 py-1 font-mono text-[0.6rem] font-semibold uppercase tracking-[0.2em]"
              style={{
                borderColor: "var(--m-line)",
                color: "var(--m-muted)",
                background: "var(--m-bg)",
              }}
            >
              {tag}
            </span>
            <div className="ml-auto flex gap-2">
              <button
                type="button"
                className={cn(
                  "rounded-md px-4 py-1.5 text-sm font-medium transition-transform active:scale-[0.98]",
                  focusRing
                )}
                style={{ background: "var(--m-accent)", color: "var(--m-ink)" }}
              >
                {primaryLabel}
              </button>
              <button
                type="button"
                className={cn(
                  "rounded-md border px-4 py-1.5 text-sm font-medium transition-colors active:scale-[0.98]",
                  focusRing
                )}
                style={{ borderColor: "var(--m-line)", color: "var(--m-text)" }}
              >
                {secondaryLabel}
              </button>
            </div>
          </div>

          {/* Hairline divider before the switcher. */}
          <div
            className="h-px w-full"
            style={{ background: "var(--m-line)" }}
            aria-hidden
          />

          {/* Accent switcher — a real radiogroup with roving focus. */}
          <div className="flex items-center justify-between gap-3">
            <span
              className="font-mono text-[0.6rem] uppercase tracking-[0.22em]"
              style={{ color: "var(--m-muted)" }}
            >
              {switcherLabel}
            </span>
            <div
              role="radiogroup"
              aria-label={switcherLabel}
              onKeyDown={onSwitcherKey}
              className="flex gap-2"
            >
              {list.map((a, i) => {
                const selected = a.id === activeAccent.id;
                return (
                  <button
                    key={a.id}
                    ref={(el) => {
                      chipRefs.current[i] = el;
                    }}
                    type="button"
                    role="radio"
                    aria-checked={selected}
                    aria-label={a.label}
                    tabIndex={selected ? 0 : -1}
                    onClick={() => select(i)}
                    className={cn(
                      "relative h-5 w-5 rounded-full border transition-transform active:scale-95",
                      focusRing
                    )}
                    style={{
                      background: a.color,
                      borderColor: selected
                        ? "var(--m-text)"
                        : "var(--m-line)",
                      boxShadow: selected
                        ? "inset 0 0 0 2px var(--m-surface)"
                        : "none",
                    }}
                  />
                );
              })}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}
```
