# Isometric Bars (`isometric-bars`)

> An isometric bar chart illustration — extruded bars grow out of a plinth on staggered springs when scrolled into view, with live value counters riding their tops.

- **Docs:** https://www.mellowui.com/components/isometric-bars
- **Markdown:** https://www.mellowui.com/components/isometric-bars.md
- **Registry:** https://www.mellowui.com/r/isometric-bars.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/isometric-bars
- **Categories:** display, 3d, scroll, animation
- **Dependencies:** motion

## AI prompt

Add an IsometricBars component from the mellow library — an isometric bar chart. Pass `values` (0–100) and optional `labels`; bars extrude on staggered springs the moment they scroll into view, with a live counter riding each bar top. Click to replay. Tune `stiffness` and `damping` for the growth spring.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/isometric-bars.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `values` | `number[]` | `[34, 58, 42, 88, 66]` | Bar values, 0–100. |
| `labels` | `string[]` | — | Mono labels under each bar. |
| `size` | `number` | `380` | Rendered width in px. |
| `stiffness` | `number` | `150` | Growth spring stiffness — higher snaps the bars up harder. |
| `damping` | `number` | `20` | Growth spring damping — lower overshoots more. |
| `className` | `string` | — | Additional CSS classes. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, { useEffect, useRef, useState } from "react";
import { useInView, useReducedMotion, useSpring } from "motion/react";

export interface IsometricBarsProps {
  /** Bar values, 0–100. */
  values?: number[];
  /** Mono labels under each bar. */
  labels?: string[];
  /** Rendered width in px. */
  size?: number;
  /** Growth spring stiffness — higher snaps the bars up harder. */
  stiffness?: number;
  /** Growth spring damping — lower overshoots more. */
  damping?: number;
  className?: string;
  style?: React.CSSProperties;
}

const iso = (x: number, y: number, z: number): [number, number] => [
  0.866 * (x - y),
  0.5 * (x + y) - z,
];
const poly = (pts: [number, number][]) =>
  pts.map((p) => `${p[0].toFixed(2)},${p[1].toFixed(2)}`).join(" ");

const S = 13; // bar half-size
const GAP = 36;
const MAXH = 96;
const VALUE_RISE = 20; // lift value labels above bar tops
const LABEL_OUT_Y = 18; // push quarter labels past the plinth front
const LABEL_OUT_Z = -14;

const TOP = "color-mix(in oklab, var(--ink) 6%, var(--background))";
const RIGHT = "color-mix(in oklab, var(--ink) 16%, var(--background))";
const FRONT = "color-mix(in oklab, var(--ink) 26%, var(--background))";
const PLATE = "color-mix(in oklab, var(--ink) 4%, var(--background))";

function barFaces(cx: number, h: number) {
  const cy = 0;
  return {
    top: poly([
      iso(cx - S, cy - S, h),
      iso(cx + S, cy - S, h),
      iso(cx + S, cy + S, h),
      iso(cx - S, cy + S, h),
    ]),
    right: poly([
      iso(cx + S, cy - S, h),
      iso(cx + S, cy + S, h),
      iso(cx + S, cy + S, 0),
      iso(cx + S, cy - S, 0),
    ]),
    front: poly([
      iso(cx + S, cy + S, h),
      iso(cx - S, cy + S, h),
      iso(cx - S, cy + S, 0),
      iso(cx + S, cy + S, 0),
    ]),
  };
}

/**
 * An isometric bar chart illustration — extruded bars grow out of a plinth on
 * staggered springs when scrolled into view, with live value counters riding
 * their tops. Click to regrow.
 */
export function IsometricBars({
  values = [34, 58, 42, 88, 66],
  labels,
  size = 380,
  stiffness = 150,
  damping = 20,
  className,
  style,
}: IsometricBarsProps) {
  const rootRef = useRef<HTMLButtonElement>(null);
  const inView = useInView(rootRef, { once: true, amount: 0.4 });
  const reduced = useReducedMotion();
  const [run, setRun] = useState(0);

  const n = values.length;
  const halfSpan = ((n - 1) / 2) * GAP + S;
  const extent = 0.866 * (halfSpan + S) + 30;
  const vb = {
    x: -extent,
    y: -0.5 * (halfSpan + S) - MAXH - 26 - VALUE_RISE,
    w: 2 * extent,
    h: (halfSpan + S) + MAXH + 26 + 28,
  };

  return (
    <button
      ref={rootRef}
      type="button"
      aria-label={`Bar chart: ${values.map((v) => Math.round(v)).join(", ")}. Click to replay.`}
      onClick={() => setRun((r) => r + 1)}
      className={[
        "inline-block cursor-pointer bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-[rgba(var(--ink-rgb),0.3)]",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
      style={style}
    >
      <svg
        viewBox={`${vb.x} ${vb.y} ${vb.w} ${vb.h}`}
        preserveAspectRatio="xMidYMid meet"
        aria-hidden="true"
        className="block h-auto w-full"
        style={{ maxWidth: size, aspectRatio: `${vb.w} / ${vb.h}` }}
      >
        {/* plinth */}
        {[6, 4, 2, 0].map((z) => (
          <g key={z} transform={`translate(0, ${-6 + z})`}>
            <polygon
              points={poly([
                iso(-halfSpan - 14, -S - 14, 0),
                iso(halfSpan + 14, -S - 14, 0),
                iso(halfSpan + 14, S + 14, 0),
                iso(-halfSpan - 14, S + 14, 0),
              ])}
              fill={PLATE}
              stroke="var(--ink)"
              strokeOpacity={z === 0 ? 0.8 : 0.4}
              strokeWidth={z === 0 ? 1.4 : 0.8}
              vectorEffect="non-scaling-stroke"
              className="transition-[fill] duration-300"
            />
          </g>
        ))}
        {values.map((value, i) => (
          <Bar
            key={`${i}-${run}`}
            cx={(i - (n - 1) / 2) * GAP}
            value={Math.min(100, Math.max(0, value))}
            label={labels?.[i]}
            delay={i * 90}
            started={inView}
            stiffness={stiffness}
            damping={damping}
            reduced={!!reduced}
          />
        ))}
      </svg>
    </button>
  );
}

function Bar({
  cx,
  value,
  label,
  delay,
  started,
  stiffness,
  damping,
  reduced,
}: {
  cx: number;
  value: number;
  label?: string;
  delay: number;
  started: boolean;
  stiffness: number;
  damping: number;
  reduced: boolean;
}) {
  const topRef = useRef<SVGPolygonElement>(null);
  const rightRef = useRef<SVGPolygonElement>(null);
  const frontRef = useRef<SVGPolygonElement>(null);
  const valueRef = useRef<SVGTextElement>(null);

  const h = useSpring(0, { stiffness, damping });

  useEffect(() => {
    const apply = (v: number) => {
      const faces = barFaces(cx, Math.max(0.5, v));
      topRef.current?.setAttribute("points", faces.top);
      rightRef.current?.setAttribute("points", faces.right);
      frontRef.current?.setAttribute("points", faces.front);
      const [tx, ty] = iso(cx, 0, Math.max(0.5, v) + VALUE_RISE);
      if (valueRef.current) {
        valueRef.current.setAttribute("x", String(tx));
        valueRef.current.setAttribute("y", String(ty + 5));
        valueRef.current.textContent = String(Math.round((v / MAXH) * 100));
      }
    };
    apply(h.get());
    return h.on("change", apply);
  }, [h, cx]);

  useEffect(() => {
    if (!started) return;
    const target = (value / 100) * MAXH;
    if (reduced) {
      h.jump(target);
      return;
    }
    const timeout = setTimeout(() => h.set(target), delay);
    return () => clearTimeout(timeout);
  }, [started, value, delay, reduced, h]);

  const faces = barFaces(cx, 0.5);
  const [lx, ly] = iso(cx, S + LABEL_OUT_Y, LABEL_OUT_Z);

  return (
    <g>
      <polygon ref={frontRef} points={faces.front} fill={FRONT} stroke="var(--ink)" strokeOpacity={0.55} strokeWidth={1} vectorEffect="non-scaling-stroke" className="transition-[fill] duration-300" />
      <polygon ref={rightRef} points={faces.right} fill={RIGHT} stroke="var(--ink)" strokeOpacity={0.55} strokeWidth={1} vectorEffect="non-scaling-stroke" className="transition-[fill] duration-300" />
      <polygon ref={topRef} points={faces.top} fill={TOP} stroke="var(--ink)" strokeOpacity={0.85} strokeWidth={1.4} vectorEffect="non-scaling-stroke" className="transition-[fill] duration-300" />
      <text
        ref={valueRef}
        textAnchor="middle"
        fill="var(--ink)"
        style={{
          fontFamily: "var(--font-mono)",
          fontSize: 11,
          letterSpacing: "0.08em",
        }}
      />
      {label && (
        <text
          x={lx}
          y={ly}
          textAnchor="middle"
          fill="var(--ink)"
          fillOpacity={0.55}
          style={{
            fontFamily: "var(--font-mono)",
            fontSize: 9,
            letterSpacing: "0.14em",
            textTransform: "uppercase",
          }}
        >
          {label}
        </text>
      )}
    </g>
  );
}

export default IsometricBars;

```

## Demo

```tsx
"use client";

import React from "react";
import { IsometricBars } from "../mellow/isometric-bars";

export const ISOMETRIC_BARS_VALUES = [28, 46, 64, 52, 88];
export const ISOMETRIC_BARS_LABELS = ["Q1", "Q2", "Q3", "Q4", "Q5"];

export default function IsometricBarsDemo() {
  return (
    <div className="flex flex-col items-center gap-2 p-2 sm:gap-3 sm:p-4">
      <div className="w-full max-w-md rounded-2xl border border-[var(--rule)] bg-[rgba(var(--ink-rgb),0.02)] p-4">
        <IsometricBars
          values={ISOMETRIC_BARS_VALUES}
          labels={ISOMETRIC_BARS_LABELS}
          size={360}
          className="mx-auto w-full [&_svg]:max-h-[min(220px,42dvh)]"
        />
        <div className="mt-2">
          <div className="[font-family:var(--font-mono)] text-[0.625rem] font-medium tracking-[0.16em] text-[rgba(var(--ink-rgb),0.45)] uppercase">
            Analytics
          </div>
          <div className="mt-1 [font-family:var(--font-sans)] text-base font-medium tracking-[-0.01em] text-[var(--ink)]">
            Growth you can see
          </div>
        </div>
      </div>
      <p className="text-[0.8125rem] text-[rgba(var(--ink-rgb),0.35)]">
        Bars grow on view · click the chart to replay
      </p>
    </div>
  );
}

```
