CSS + JS Data & tools #controls #hover #data #chart #json #stacked

Stacked 3D bars from JSON in CSS + JavaScript

Quarterly revenue split by product, each quarter a column of stacked glass cuboids. The legend is a row of toggles: switch a product off and its segments shrink to nothing while the ones above slide down. JS only writes where each segment starts and how tall it is.

  • 245 lines of CSS
  • 7 lines of HTML
  • 154 lines of JS
  • No dependencies
  • MIT licensed
Video 0.6 MB GitHub

How it works

  1. The data is plain JSON: quarters, and per product one value per quarter. JS turns each value into two numbers, both fractions of the scale's top: --base, the sum of the segments below it, and --v, its own height.
  2. A segment is a full-height front and side squashed from the bottom: translateY(--base × −100px) … scaleY(--v). The move comes first, so it is not squashed. Only transform changes, so a new layout is a transition with no layout work.
  3. Switch a product off and JS sets its --v to 0 and recomputes every --base above it. Collapse and slide use the same duration and easing, so the stack stays glued while it moves (an easing that overshoots would turn a shrinking face inside out, so this one does not). Only the highest segment (.is-top) shows its lid.
  4. The faces are see-through glass: the colour mixed with transparent, a hairline edge and a soft glow. The pointed-at segment fills in and glows: a ::after layer on each face whose opacity fades in.
  5. The chart is turned, so parts of it sit behind the flat boxes around it: pointer-events: none on everything, auto only on the faces.
  6. There is one tooltip for the whole chart. JS moves it to the pointed-at segment with --tx / --ty and a transition glides it there while its text changes; it hides 150ms after the pointer leaves, so crossing a gap doesn't flicker. A tap does the same on a touch screen. Names go in with textContent, never as HTML.

Key techniques

  • JSON → --base + --v per segment (fractions of the scale)
  • translateY(--base) then scaleY(--v): stacking with transform only
  • one shared tooltip gliding on --tx / --ty
  • legend = <button aria-pressed> toggles

Copy-paste code

HTML 7 lines

<div class="chart">
  <div class="scene">
    <div class="stack3d"></div>
  </div>
  <output></output>
  <div class="legend"></div>
</div>

CSS 245 lines

.chart {
  display: grid;
  justify-items: center;
  gap: 8px;
  font-family: system-ui, sans-serif;
}

.scene {
  perspective: 800px;
  padding: 36px 40px 34px;
  pointer-events: none; /* the chart is turned: only the faces take the pointer */
}

/* 4 columns × 24px + 3 gaps × 16px = 144px */
.stack3d {
  position: relative;
  width: 144px;
  height: 100px; /* the top of the scale */
  transform-style: preserve-3d;
  transform: rotateX(-18deg) rotateY(-28deg);
}

/* the floor: a grid laid flat along the bottom of the columns */
.floor {
  position: absolute;
  left: -12px;
  top: 74px;
  width: 168px;
  height: 52px;
  border: 1px solid rgb(139 108 255 / 0.45);
  border-radius: 6px;
  background:
    repeating-linear-gradient(90deg, rgb(139 108 255 / 0.22) 0 1px, transparent 1px 14px),
    repeating-linear-gradient(rgb(139 108 255 / 0.22) 0 1px, transparent 1px 13px),
    rgb(139 108 255 / 0.08);
  transform: rotateX(90deg);
}

/* the scale: lines at 0, half and the top, along the back of the floor; the numbers at the
   right end, which reaches out past the last column */
.wall {
  position: absolute;
  top: -12px; /* the box holds the numbers too: text spilling out of a 3D layer gets cut */
  left: -12px;
  width: 194px;
  height: 112px;
  transform: translateZ(-26px);
}

.wall b {
  position: absolute;
  right: 26px;
  bottom: calc(var(--t) * 100px);
  left: 0;
  height: 1px;
  background: rgb(236 238 251 / 0.24);
}

.wall span {
  position: absolute;
  top: -6px;
  left: 100%;
  padding-left: 6px;
  color: #949bc0;
  font: 700 9px/12px system-ui, sans-serif;
}

.cols {
  position: absolute;
  inset: 0;
  display: flex;
  gap: 16px;
  transform-style: preserve-3d;
}

.col {
  position: relative;
  flex: none;
  width: 24px;
  height: 100%;
  transform-style: preserve-3d;
}

/* the quarter, standing in front of the column */
.col span {
  position: absolute;
  top: calc(100% + 8px);
  left: -6px;
  width: 36px;
  color: #949bc0;
  font: 700 9px/12px system-ui, sans-serif;
  text-align: center;
  transform: translateZ(12px);
}

/* one colour per product */
[data-p='0'] { --c: #8b6cff; }
[data-p='1'] { --c: #2ee6d6; }
[data-p='2'] { --c: #ff4d9d; }

/* A segment only groups its three faces and carries --base / --v: it draws no box itself */
.seg {
  --base: 0; /* JS writes both: fractions of the scale */
  --v: 0;
  display: contents;
}

/* see-through glass faces with a fine bright edge */
.seg i {
  position: absolute;
  top: 0;
  left: 0;
  box-sizing: border-box;
  width: 24px;
  height: 100%;
  border: 0.6px solid color-mix(in srgb, color-mix(in srgb, var(--c) 80%, #fff) 75%, transparent);
  background: color-mix(in srgb, var(--c) 58%, transparent);
  box-shadow:
    inset 0 0 8px color-mix(in srgb, var(--c) 30%, transparent),
    0 0 10px color-mix(in srgb, var(--c) 22%, transparent);
  outline: none;
  pointer-events: auto;
  cursor: pointer;
  transform-origin: bottom center;
  /* each column 50ms after the one before; no overshoot, so a face never turns inside out */
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--i) * 50ms), opacity 0.25s;
}

/* the pointed-at segment fills in and glows: a layer on each face that fades in */
.seg i::after {
  content: '';
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--c) 50%, transparent);
  box-shadow: 0 0 20px color-mix(in srgb, var(--c) 60%, transparent);
  opacity: 0;
  transition: opacity 0.25s;
}

.seg.is-active i::after {
  opacity: 1;
}

/* front: lift by the segments below, step out to the front, then squash to its value */
.seg i:nth-child(1) {
  transform: translateY(calc(var(--base) * -100px)) translateZ(12px) scaleY(var(--v));
}

/* right side, darker */
.seg i:nth-child(2) {
  background: color-mix(in srgb, color-mix(in srgb, var(--c) 55%, #05060c) 64%, transparent);
  transform: translateY(calc(var(--base) * -100px)) rotateY(90deg) translateZ(12px) scaleY(var(--v));
}

/* the lid: a 24px square laid flat on the segment's top. Only the highest segment shows it */
.seg i:nth-child(3) {
  height: 24px;
  background: color-mix(in srgb, var(--c) 80%, transparent);
  opacity: 0;
  pointer-events: none;
  transform-origin: center;
  transform: translateY(calc((1 - var(--base) - var(--v)) * 100px)) rotateX(90deg) translateZ(12px);
}

.seg.is-top i:nth-child(3) {
  opacity: 1;
  pointer-events: auto;
}

.seg.is-off i {
  pointer-events: none; /* collapsed: nothing to point at */
}

/* One tooltip for the whole chart: JS moves it to the pointed-at segment (--tx, --ty) and it
   glides there. It floats 40px towards you (the columns to the right stand nearer); its
   thickness is a flat edge offset up and to the right, the way the columns' depth runs */
.tip {
  position: absolute;
  top: 0;
  left: 0;
  padding: 3px 7px;
  border: 1px solid var(--c);
  border-radius: 6px;
  background: #141830;
  box-shadow:
    3px -3px 0 color-mix(in srgb, var(--c) 55%, #05060c),
    0 0 14px color-mix(in srgb, var(--c) 40%, transparent);
  color: #eceefb;
  font: 700 9px/12px system-ui, sans-serif;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transform: translate(var(--tx, 0px), calc(var(--ty, 0px) - 30px)) translate(-50%, -100%) translateZ(40px);
  transition:
    transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity 0.2s;
}

.tip.is-on {
  opacity: 1;
}

output {
  color: #949bc0;
  font-size: 12px;
}

/* the legend is the switches: one real button per product */
.legend {
  display: flex;
  gap: 6px;
}

.legend button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px 4px 8px;
  border: 1px solid color-mix(in srgb, var(--c) 60%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--c) 16%, transparent);
  color: #eceefb;
  font: 700 12px system-ui, sans-serif;
  cursor: pointer;
}

/* the swatch: filled when on, a hollow ring when off */
.legend button::before {
  content: '';
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: inset 0 0 0 1.5px var(--c);
}

.legend button[aria-pressed='false'] {
  border-color: rgb(255 255 255 / 0.14);
  background: transparent;
  color: #949bc0;
}

.legend button[aria-pressed='false']::before {
  background: transparent;
}

JS 154 lines

// The data, as an API would send it back
const REVENUE = {
  "prefix": "$",
  "suffix": "k",
  "quarters": ["Q1", "Q2", "Q3", "Q4"],
  "products": [
    {
      "name": "Pro",
      "values": [42, 48, 55, 63]
    },
    {
      "name": "Team",
      "values": [30, 38, 41, 50]
    },
    {
      "name": "Starter",
      "values": [18, 22, 20, 27]
    }
  ]
};

const { quarters, products } = REVENUE;
const chart = document.querySelector('.stack3d');
const out = document.querySelector('.chart output');
const legend = document.querySelector('.legend');
const TICKS = [0, 0.5, 1]; // the scale's lines, as fractions of its top
const W = 24, PITCH = 40, H = 100; // a column's width, width + gap, and the scale's height (as in the CSS)
const on = products.map(() => true); // which products are shown

// money as most dashboards write it: the sign in front, the k right after the number ($38k)
const money = (v) => REVENUE.prefix + v + REVENUE.suffix;
const tipText = (q, p) => `${quarters[q]} · ${products[p].name} · ${money(products[p].values[q])}`;

// Build the chart once: the floor, the scale, per quarter a column with one segment (front, side,
// lid) per product, and one tooltip
chart.innerHTML =
  '<div class="floor"></div>' +
  '<div class="wall">' + TICKS.map((t) => `<b style="--t:${t}"><span></span></b>`).join('') + '</div>' +
  '<div class="cols">' +
  quarters.map((_, q) => `<div class="col" style="--i:${q}">` +
    products.map((_, p) => `<div class="seg" data-p="${p}"><i role="img" tabindex="0"></i><i></i><i></i></div>`).join('') +
    '<span></span></div>').join('') +
  '</div>' +
  '<b class="tip" data-p="0" aria-hidden="true"></b>';
legend.innerHTML = products.map((_, p) => `<button type="button" data-p="${p}"></button>`).join('');

const cols = [...chart.querySelectorAll('.col')];
const segs = cols.map((c) => [...c.querySelectorAll('.seg')]);
const ticks = chart.querySelectorAll('.wall span');
const tipEl = chart.querySelector('.tip');
const buttons = [...legend.querySelectorAll('button')];

// names and labels go in as text, never as HTML: data from an API is not trusted markup
cols.forEach((c, q) => (c.querySelector('span').textContent = quarters[q]));
buttons.forEach((b, p) => (b.textContent = products[p].name));
segs.forEach((col, q) => col.forEach((s, p) => s.firstElementChild.setAttribute('aria-label', tipText(q, p))));

let active = null; // [quarter, product] the tooltip is on
let hideTimer;

// Lay out the shown products: each segment gets --base (the sum below it) and --v (its value),
// both as fractions of the scale's top. CSS does the rest.
function render() {
  const totals = quarters.map((_, q) => products.reduce((s, p, k) => s + (on[k] ? p.values[q] : 0), 0));
  const most = Math.max(...totals);
  const top = Math.max(20, Math.ceil(most / 20) * 20); // a tidy top for the scale
  segs.forEach((col, q) => {
    let base = 0;
    let highest = null;
    col.forEach((s, p) => {
      const v = on[p] ? products[p].values[q] / top : 0;
      s.style.setProperty('--base', base);
      s.style.setProperty('--v', v);
      s.classList.remove('is-top');
      s.classList.toggle('is-off', !on[p]);
      s.firstElementChild.tabIndex = on[p] ? 0 : -1; // a switched-off segment leaves the tab order
      s.firstElementChild.toggleAttribute('aria-hidden', !on[p]);
      if (v > 0) highest = s;
      base += v;
    });
    if (highest) highest.classList.add('is-top'); // only the highest segment shows its lid
  });
  ticks.forEach((t, k) => (t.textContent = Math.round(TICKS[k] * top)));
  const names = products.filter((_, k) => on[k]).map((p) => p.name);
  const sum = totals.reduce((a, b) => a + b, 0);
  const best = quarters[totals.indexOf(most)];
  out.textContent = !names.length
    ? 'No product shown · switch one on'
    : `${names.length === products.length ? 'All products' : names.join(' + ')} · ${money(sum)} · best ${best}, ${money(most)}`;
  buttons.forEach((b, k) => b.setAttribute('aria-pressed', on[k]));
  // the tooltip follows its segment to its new place, or goes if the segment did
  if (active) on[active[1]] ? place(...active) : hideNow();
}

// One tooltip for the chart: it glides to the segment (--tx, --ty) and its text changes on the way
function place(q, p) {
  clearTimeout(hideTimer);
  const seg = segs[q][p];
  const wasOn = tipEl.classList.contains('is-on');
  if (!wasOn) tipEl.style.transition = 'none'; // from hidden: appear in place, don't fly in
  tipEl.textContent = tipText(q, p);
  tipEl.dataset.p = p; // its colour
  const topOf = parseFloat(seg.style.getPropertyValue('--base')) + parseFloat(seg.style.getPropertyValue('--v'));
  tipEl.style.setProperty('--tx', q * PITCH + W / 2 + 'px');
  tipEl.style.setProperty('--ty', (1 - topOf) * H + 'px');
  if (!wasOn) {
    void tipEl.offsetWidth; // apply the new place before the transition comes back
    tipEl.style.transition = '';
  }
  tipEl.classList.add('is-on');
  segs.flat().forEach((s) => s.classList.toggle('is-active', s === seg)); // it fills in and glows
  active = [q, p];
}
function hideNow() {
  clearTimeout(hideTimer);
  tipEl.classList.remove('is-on');
  segs.flat().forEach((s) => s.classList.remove('is-active'));
  active = null;
}
// a short grace period, so crossing the gap between two segments does not flicker it
function hide() {
  clearTimeout(hideTimer);
  hideTimer = setTimeout(hideNow, 150);
}

const segOf = (e) => {
  const s = e.target.closest?.('.seg');
  return s && !s.classList.contains('is-off') ? s : null;
};
const pointAt = (s) => place(cols.indexOf(s.parentElement), +s.dataset.p);
chart.addEventListener('pointerover', (e) => segOf(e) && pointAt(segOf(e)));
chart.addEventListener('focusin', (e) => segOf(e) && pointAt(segOf(e)));
const leave = (e) => {
  if (!e.relatedTarget?.closest?.('.seg')) hide();
};
chart.addEventListener('pointerout', leave);
chart.addEventListener('focusout', leave);
// a finger has no hover: a tap on a segment shows its tooltip, a tap elsewhere hides it
document.addEventListener('pointerup', (e) => {
  if (e.pointerType === 'mouse') return;
  const s = segOf(e);
  if (s) pointAt(s);
  else hide();
});

// the legend is the switches
legend.addEventListener('click', (e) => {
  const b = e.target.closest('button');
  if (!b) return;
  on[b.dataset.p] = !on[b.dataset.p];
  render();
});

render();

Sass source 292 lines

// Stacked bars drawn from JSON. JS writes two numbers per segment, both fractions of the scale:
// --base (the sum of the segments below it) and --v (its own height). Each segment's front face is a
// full-height box lifted by --base and squashed with scaleY(--v) from its bottom, and so is its
// side; the lid rides on top. Switching a product off sets its --v to 0 and
// lowers every --base above it: one transition on transform, no layout.
@use 'sass:list';

$w: 24px; // column width, and depth
$gap: 16px;
$h: 100px; // the top of the scale
$floor: 52px; // the floor's depth, front to back
$n: 4;
$span: $w * $n + $gap * ($n - 1);
// no overshoot: a face shrinking to 0 would turn inside out, and collapse and slide must stay glued
$ease: cubic-bezier(0.22, 1, 0.36, 1);
$time: 0.7s;
// one colour per product, bottom to top
$colors: var(--accent), var(--accent-2), var(--hot);

.d-stackbars {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  width: 100%;
  height: 100%;
  padding: 10px 14px 14px; // the control dock: the same place in every model with controls
  // The chart is turned, so its left columns sit behind the plane of the flat boxes around it
  // (this one and the view), which would catch the pointer first: only the faces and the dock
  // take it.
  pointer-events: none;

  @for $k from 1 through list.length($colors) {
    [data-p='#{$k - 1}'] {
      --c: #{list.nth($colors, $k)};
    }
  }

  &__view {
    display: grid;
    place-items: center;
    min-height: 0;
    pointer-events: none;
  }

  &__chart {
    position: relative;
    width: $span;
    height: $h;
    margin-top: 16px; // room for a tooltip over the tallest column
    transform-style: preserve-3d;
    transform: rotateX(-18deg) rotateY(-28deg);
  }

  // the floor: a grid laid flat along the bottom of the columns
  &__floor {
    position: absolute;
    left: -12px;
    top: $h - $floor * 0.5;
    width: $span + 24px;
    height: $floor;
    border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
    border-radius: 6px;
    background:
      repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 22%, transparent) 0 1px, transparent 1px 14px),
      repeating-linear-gradient(color-mix(in srgb, var(--accent) 22%, transparent) 0 1px, transparent 1px 13px),
      color-mix(in srgb, var(--accent) 8%, transparent);
    transform: rotateX(90deg);
  }

  // the scale: lines at 0, half and the top, on a wall along the back edge of the floor, numbers
  // at the right end, which reaches out past the last column
  // (The wall's box also holds the numbers, 12px above the top line and 26px past its end: text
  // that spilled out of the box of a 3D layer got cut off.)
  &__wall {
    position: absolute;
    top: -12px;
    left: -12px;
    width: $span + 24px + 26px;
    height: $h + 12px;
    transform: translateZ($floor * -0.5);

    b {
      position: absolute;
      right: 26px;
      bottom: calc(var(--t) * #{$h});
      left: 0;
      height: 1px;
      background: color-mix(in srgb, var(--text) 24%, transparent);
    }

    span {
      position: absolute;
      top: -6px;
      left: 100%;
      padding-left: 6px;
      color: var(--muted);
      font: 700 9px/12px system-ui, sans-serif;
      font-variant-numeric: tabular-nums;
    }
  }

  &__cols {
    position: absolute;
    inset: 0;
    display: flex;
    gap: $gap;
    transform-style: preserve-3d;
  }

  &__col {
    position: relative;
    flex: none;
    width: $w;
    height: 100%;
    transform-style: preserve-3d;

    // the quarter, standing in front of the column
    span {
      position: absolute;
      top: calc(100% + 8px);
      left: -6px;
      width: $w + 12px;
      color: var(--muted);
      font: 700 9px/12px system-ui, sans-serif;
      text-align: center;
      transform: translateZ($w * 0.5);
    }
  }

  // A segment only groups its three faces and carries --base / --v (display: contents: it draws
  // no box, so nothing flat of it sits in the turned chart to catch the pointer). The faces are the
  // hit targets; JS finds the segment with closest().
  &__seg {
    display: contents;

    // see-through glass faces with a fine bright edge
    i {
      position: absolute;
      top: 0;
      left: 0;
      width: $w;
      height: 100%;
      // a fine edge: under 1px shows as a hairline on sharp screens (a standard one draws 1px)
      border: 0.6px solid color-mix(in srgb, color-mix(in srgb, var(--c) 80%, #fff) 75%, transparent);
      background: color-mix(in srgb, var(--c) 58%, transparent);
      box-shadow:
        inset 0 0 8px color-mix(in srgb, var(--c) 30%, transparent),
        0 0 10px color-mix(in srgb, var(--c) 22%, transparent);
      outline: none;
      pointer-events: auto;
      cursor: pointer;
      transform-origin: bottom center;
      transition:
        transform $time $ease calc(var(--i) * 50ms),
        opacity 0.25s;

      // the pointed-at segment fills in and glows (.is-active, set with the tooltip); faded in with
      // opacity, so it is smooth
      &::after {
        content: '';
        position: absolute;
        inset: 0;
        background: color-mix(in srgb, var(--c) 50%, transparent);
        box-shadow: 0 0 20px color-mix(in srgb, var(--c) 60%, transparent);
        opacity: 0;
        transition: opacity 0.25s;
      }
    }

    &.is-active i::after {
      opacity: 1;
    }

    // front: lifted by the segments below, stepped out to the front, then squashed to its value
    // (the lift comes first, so it is not squashed)
    i:nth-child(1) {
      transform: translateY(calc(var(--base) * #{-$h})) translateZ($w * 0.5) scaleY(var(--v));
    }

    // right side, darker
    i:nth-child(2) {
      background: color-mix(in srgb, color-mix(in srgb, var(--c) 55%, #05060c) 64%, transparent);
      transform: translateY(calc(var(--base) * #{-$h})) rotateY(90deg) translateZ($w * 0.5) scaleY(var(--v));
    }

    // the lid: a $w square laid flat on the segment's top. Only the highest segment shows it (and
    // only then takes the pointer).
    i:nth-child(3) {
      height: $w;
      background: color-mix(in srgb, var(--c) 80%, transparent);
      opacity: 0;
      pointer-events: none;
      transform-origin: center;
      transform: translateY(calc((1 - var(--base) - var(--v)) * #{$h})) rotateX(90deg) translateZ($w * 0.5);
    }

    &.is-top i:nth-child(3) {
      opacity: 1;
      pointer-events: auto;
    }

    // collapsed: nothing to point at
    &.is-off i {
      pointer-events: none;
    }
  }

  // One tooltip for the chart: JS moves it to the pointed-at segment (--tx, --ty) and CSS glides
  // it there, so it slides from segment to segment with its text changing instead of blinking. It
  // lives in the chart's turned plane, 40px towards you (each column to the right stands nearer
  // than the one before). It stays flat (opacity on a preserve-3d box flattens it and flickers):
  // its thickness is a hard edge offset up and to the right, the way the columns' depth runs.
  &__tip {
    position: absolute;
    top: 0;
    left: 0;
    padding: 3px 7px;
    border: 1px solid var(--c);
    border-radius: 6px;
    background: var(--surface-solid);
    box-shadow:
      3px -3px 0 color-mix(in srgb, var(--c) 55%, #05060c),
      0 0 14px color-mix(in srgb, var(--c) 40%, transparent);
    color: var(--text);
    font: 700 9px/12px system-ui, sans-serif;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transform: translate(var(--tx, 0px), calc(var(--ty, 0px) - 30px)) translate(-50%, -100%) translateZ(40px);
    transition:
      transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1),
      opacity 0.2s;

    &.is-on {
      opacity: 1;
    }
  }

  &__dock {
    display: grid;
    justify-items: center;
    gap: 6px;
    font-size: 12px;
    pointer-events: auto;

    output {
      color: var(--muted);
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }
  }

  // the legend is the switches: one real button per product
  &__legend {
    display: flex;
    gap: 6px;

    button {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 4px 11px 4px 8px;
      border: 1px solid color-mix(in srgb, var(--c) 60%, transparent);
      border-radius: 999px;
      background: color-mix(in srgb, var(--c) 16%, transparent);
      color: var(--text);
      font: inherit;
      font-weight: 700;
      cursor: pointer;

      // the swatch: filled when on, a hollow ring when off
      &::before {
        content: '';
        width: 9px;
        height: 9px;
        border-radius: 50%;
        background: var(--c);
        box-shadow: inset 0 0 0 1.5px var(--c);
      }

      &[aria-pressed='false'] {
        border-color: var(--border-strong);
        background: transparent;
        color: var(--muted);

        &::before {
          background: transparent;
        }
      }
    }
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.