Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Isometric bars

New

A with-or-without comparison drawn as isometric prisms that rise on entry, count their pills up and drop a floating cap.

iso-bars/iso-bars.tsx
Live · interactive
WithGridwellWithout
  • Data points
  • Hit rate

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A before-and-after or with-and-without claim on a landing page, where two figures per metric carry the story.
  • Two to four headline metrics, each compared on its own scale, so a count and a percentage can sit side by side.
  • A figure that is a floor rather than a count (“150+”): the floating cap says “at least” without a footnote.
  • A section the reader scrolls into, where a single, short entrance (the prisms rising, the pills counting) earns attention.

When not to use

  • More than two series, or more than four or five groups: the prisms get thin. Use a grouped bar chart.
  • Values the reader compares across groups on one axis: each group here has its own max. Use a flat bar chart with a shared scale.
  • A trend over time. Use a line or area chart.
  • Exact reading of many numbers, or values to sort and export. Use a table.
  • A dense dashboard tile, under about 300 px wide: the depth and pills crowd. Use a stat with a delta, or a sparkline.

You can add this component with the shadcn CLI or by hand:

Into components/iso-bars of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/iso-bars.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

impact.tsx
import { IsoBars } from './iso-bars';export function Impact() {  return (    <IsoBars      product="Gridwell"      groups={[        { label: 'Data points', icon: 'chart', with: 150, without: 30, max: 171, open: true },        { label: 'Hit rate', icon: 'target', with: 98, without: 40, unit: '%' },        { label: 'Setup time', icon: 'clock', with: 4, without: 26, unit: ' min' },      ]}      autoplay={false}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

Two series side by side, group by group: a lavender-to-magenta prism for the value with a product and a plain grey one for the value without it, each under a pill with its figure. On entering the screen the prisms rise out of their plinths one after another while the pills count up, and an open value such as 150+ drops its separated cap into place. Hover or focus a prism to dim the rest and read both values in a tooltip.

  • Each group is an SVG of its own, drawn in px of a 203 × 300 box and scaled to its column, so the outlines, pills and prisms shrink together on a narrow card. A prism is two sides and a top, each a polygon with an objectBoundingBox gradient and a shade that darkens its lower part, over a darker band at its foot.
  • A prism’s side is its value over the group’s max, of 197 px: by default the max is the larger of the two values, so the taller prism fills the height. An open value (open: true) is a floor, not a count: the top 28.5 px of its prism float off as a cap, 8.5 px above the rest, and its pill reads “150+”.
  • The growth is transforms only. Each body is clipped above its plinth and starts sunk inside it; on entry it slides up in 1 s on an ease-out curve, 90 ms after the prism before. The pill rides along and fades in, and its figure counts up on the same curve, written straight into its text node every frame. The floating cap waits 820 ms, then drops 44 px with a small overshoot.
  • Hovering or focusing a prism fades the others to 35% and opens a tooltip over its pill with the group’s two values, the one in focus first. The first and last prisms anchor their tooltip inward so it stays on the card.
  • While on screen and untouched, the demo points at each prism in turn for 1.5 s, rests, and grows the chart again. Any pointer, key or wheel input hands it to the reader; it takes back over after 6 s of quiet. It never plays offscreen, in a hidden tab or under reduced motion.
  • Every colour is a light-dark() token on the root, so the chart follows the page’s color-scheme, or the colorScheme prop. At night the card turns near-black, the grey prisms go slate, and the “with” pill turns white with dark figures; the gradient prisms keep their colours.
PropTypeDefaultDescription
groupsIsoBarsGroup[]ISO_BARS_GROUPSThe groups, left to right. Each has a label, an icon ('chart', 'target', 'clock', any node, or null), the two values with and without, and optionally max (the value that reaches full height), unit (appended to the figures), open (the “with” value is a floor: its cap floats and its pill reads “+”) and format(value, series), which writes the figures itself.
productstring'Gridwell'The product the “with” series is about: the legend reads “With [mark] product”, and so do the tooltips and the prisms’ names.
markReactNode | null—The mark in the legend and, faded, on the “with” prisms’ faces: SVG content in a 14 × 14 box. A pixel diamond by default; null shows none.
withoutLabelstring'Without'The legend’s name for the other series.
animatebooleantrueGrow the prisms, count the pills up and drop the caps when the chart first comes on screen. Off, it is drawn complete.
autoplaybooleantrueWhile on screen and untouched, point at each prism in turn, then grow the chart again.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
defaultActive{ group: number; series: 'with' | 'without' }—Opens with this prism lit, as on hover: the others dimmed and its tooltip open. It also turns the demo off.
classNamestring—Extra class for the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

iso-bars.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useCallback,  useEffect,  useId,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,  type ReactNode,} from 'react';import { useAutoplay } from './use-autoplay';import styles from './iso-bars.module.css';/* * Isometric bars: a card that compares two series, "with" a product and "without" it, group by * group, as pairs of isometric prisms on a white card. The "with" prism wears a lavender-to-magenta * gradient and a faint pixel mark; the "without" one is plain grey. A pill over each prism gives * its value, and a bracket and a label sit under each group. * * - Each group is an SVG of its own, drawn in px of a 203 × 300 box and scaled to its column, so
iso-bars.module.css
/* Isometric bars (iso-bars.tsx): a white card on a pale grey stage, or at night a near-black one.   The card is laid out in px for a width of 622; narrower, its padding and the gaps shrink, and   each group's drawing (an SVG of 203 × 300) scales down with its column.   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes   the color-scheme it inherits from the page (light unless the page says otherwise), or the one   its colorScheme prop sets (data-color-scheme). The "with" prisms' gradients are the same on both   schemes, and live in the SVG.   Memory. The prisms, pills and caps move by transforms and opacity only; the pills' figures are   written straight into their text nodes while they count. */.root {  /* The stage, and the card on it with its edge. */  --stage: #f4f6f8;  --card: #fff;  --card-edge: #e7e9e9;  /* The legend's words; the labels under the groups and their discs; the brackets. */  --ink: #0f0f10;  --label: #7a7c7f;  --icon: #bfc1c3;  --icon-glyph: #fff;  --bracket: #e6e6e6;  /* The prisms' outline. */
use-autoplay.ts
'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(      ([entry]) => {        onScreen = entry.isIntersecting;
  • The card is a figure whose caption is the legend; the groups are a list, each drawing an SVG group named after its label.
  • Every prism is focusable, with role="img" and a name that gives its series and figure (“With Gridwell: 150+”). Tab or the arrow keys, Home and End move between prisms; focus shows a ring around the prism’s outline and opens the same tooltip as hover.
  • The pills, the brackets, the icons and the tooltip are hidden from screen readers, which hear each prism’s name instead; the counting figures never reach them.
  • Under reduced motion the chart is drawn complete, with no rise, count, drop or demo, and the dimming is instant.