Bar Chart

Sales by category Last 30 days
  1. Backpacks$4,820
  2. Bottles$3,610
  3. Outdoor gear$2,950
  4. Accessories$1,720
  5. Gift cards$940
Orders this week 412 in total
  1. Mon54
  2. Tue61
  3. Wed48
  4. Thu73
  5. Fri92
  6. Sat51
  7. Sun (today, so far)33
@use 'sprucecss/scss/spruce' as *;

.bar-charts {
  @include layout-grid('m', 20rem);
}

.bar-chart {
  --bar-chart-color: #{color('primary')};
  --bar-chart-track: #{color('surface-muted')};
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  margin: 0;
  padding: spacer('m');
  @include layout-stack('m');

  &__caption {
    display: flex;
    flex-wrap: wrap;
    gap: spacer('xxs') spacer('xs');
    justify-content: space-between;
  }

  &__title {
    color: color('heading');
    font-weight: 700;
  }

  &__subtitle {
    color: color('muted');
    font-size: config('font-size-sm', $typography);
  }

  &__items {
    display: grid;
    gap: spacer('s');
    // The parent's layout-stack sets the margins.
    list-style: none;
    padding: 0;

    // The items sit side by side: clear the stack margins of the base list style.
    > li {
      margin-block: 0;
      padding-block: 0;
    }
  }

  // Horizontal (default): label and value on one line, the bar under them.
  &__item {
    align-items: baseline;
    display: grid;
    font-size: config('font-size-sm', $typography);
    gap: spacer('xxs') spacer('s');
    grid-template-columns: 1fr auto;
  }

  &__label abbr {
    border: 0;
    cursor: default;
    text-decoration: none;
  }

  &__value {
    color: color('heading');
    font-variant-numeric: tabular-nums;
    font-weight: 700;
  }

  &__bar {
    background-color: var(--bar-chart-track);
    block-size: 0.625rem;
    border-radius: radius('pill');
    grid-column: 1 / -1;
    grid-row: 2;
    overflow: hidden;
    position: relative;

    &::before {
      background-color: var(--bar-chart-color);
      border-radius: inherit;
      content: '';
      inline-size: calc(var(--value) / var(--max) * 100%);
      inset-block: 0;
      inset-inline-start: 0;
      position: absolute;
      transform-origin: left;

      @include motion-safe {
        animation: bar-chart-grow-inline 0.6s ease-out;
      }
    }
  }

  &__item--highlight {
    --bar-chart-color: #{color('secondary')};

    .bar-chart__label {
      color: color('heading');
      font-weight: 700;
    }
  }

  // Vertical columns.
  &--column {
    .bar-chart__items {
      align-items: stretch;
      block-size: var(--bar-chart-block-size, 12rem);
      gap: spacer('xs');
      grid-auto-columns: minmax(0, 1fr);
      grid-auto-flow: column;
    }

    .bar-chart__item {
      gap: spacer('xxs');
      grid-template-columns: 1fr;
      grid-template-rows: auto 1fr auto;
      justify-items: center;
      text-align: center;
    }

    .bar-chart__value {
      font-size: 0.75rem;
      grid-row: 1;
    }

    .bar-chart__bar {
      align-self: end;
      background-color: transparent;
      block-size: calc(var(--value) / var(--max) * 100%);
      border-radius: radius('sm') radius('sm') 0 0;
      grid-column: 1;
      grid-row: 2;
      inline-size: 100%;
      max-inline-size: 2.5rem;

      &::before {
        animation-name: bar-chart-grow-block;
        block-size: 100%;
        inline-size: 100%;
        transform-origin: bottom;
      }
    }

    .bar-chart__label {
      border-block-start: config('border-width-base', $display) solid color('border');
      color: color('muted');
      grid-row: 3;
      inline-size: 100%;
      padding-block-start: spacer('xxs');
    }
  }
}

[dir='rtl'] .bar-chart:not(.bar-chart--column) .bar-chart__bar::before {
  transform-origin: right;
}

@keyframes bar-chart-grow-inline {
  from {
    transform: scaleX(0);
  }
}

@keyframes bar-chart-grow-block {
  from {
    transform: scaleY(0);
  }
}
<div class="bar-charts">
    <figure class="bar-chart">
        <figcaption class="bar-chart__caption">
            <span class="bar-chart__title">Sales by category</span>
            <span class="bar-chart__subtitle">Last 30 days</span>
        </figcaption>
        <ol class="bar-chart__items" role="list" style="--max: 4820;">
            <li class="bar-chart__item" style="--value: 4820;"><span class="bar-chart__label">Backpacks</span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">$4,820</span></li>
            <li class="bar-chart__item" style="--value: 3610;"><span class="bar-chart__label">Bottles</span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">$3,610</span></li>
            <li class="bar-chart__item" style="--value: 2950;"><span class="bar-chart__label">Outdoor gear</span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">$2,950</span></li>
            <li class="bar-chart__item" style="--value: 1720;"><span class="bar-chart__label">Accessories</span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">$1,720</span></li>
            <li class="bar-chart__item" style="--value: 940;"><span class="bar-chart__label">Gift cards</span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">$940</span></li>
        </ol>
    </figure>
    <figure class="bar-chart bar-chart--column">
        <figcaption class="bar-chart__caption">
            <span class="bar-chart__title">Orders this week</span>
            <span class="bar-chart__subtitle">412 in total</span>
        </figcaption>
        <ol class="bar-chart__items" role="list" style="--max: 92;">
            <li class="bar-chart__item" style="--value: 54;"><span class="bar-chart__label"><abbr title="Monday">Mon</abbr></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">54</span></li>
            <li class="bar-chart__item" style="--value: 61;"><span class="bar-chart__label"><abbr title="Tuesday">Tue</abbr></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">61</span></li>
            <li class="bar-chart__item" style="--value: 48;"><span class="bar-chart__label"><abbr title="Wednesday">Wed</abbr></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">48</span></li>
            <li class="bar-chart__item" style="--value: 73;"><span class="bar-chart__label"><abbr title="Thursday">Thu</abbr></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">73</span></li>
            <li class="bar-chart__item" style="--value: 92;"><span class="bar-chart__label"><abbr title="Friday">Fri</abbr></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">92</span></li>
            <li class="bar-chart__item" style="--value: 51;"><span class="bar-chart__label"><abbr title="Saturday">Sat</abbr></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">51</span></li>
            <li class="bar-chart__item bar-chart__item--highlight" aria-current="date" style="--value: 33;"><span class="bar-chart__label"><abbr title="Sunday">Sun</abbr> <span class="sr-only">(today, so far)</span></span><span class="bar-chart__bar" aria-hidden="true"></span><span class="bar-chart__value">33</span></li>
        </ol>
    </figure>
</div>

Bar Chart

A dependency-free bar chart in two layouts: horizontal bars for rankings and vertical columns for time series. It is a plain list with the values written out, styled with CSS.

Anatomy

  • .bar-chart: a figure; the figcaption holds the __title and the __subtitle.
  • .bar-chart__items: an ordered list with the --max custom property (the largest value, or your scale's maximum).
  • .bar-chart__item: one data point with its --value, a __label, a decorative __bar and the formatted __value. Add __item--highlight to emphasize one bar (like today).
  • .bar-chart--column: the vertical variant. Set its height with --bar-chart-block-size (default 12rem).

Accessibility and Usability

  • The data is real text: screen readers read "Backpacks, $4,820" for every item, and the text can be copied, translated and zoomed. The bar is aria-hidden.
  • The highlighted column is marked with aria-current="date" and hidden text ("today, so far"), not only with the color.
  • The day abbreviations are abbr elements with the full name.
  • The grow animation runs only without prefers-reduced-motion: reduce.

Technical Details

  • The bar length is calc(var(--value) / var(--max) * 100%), so you only print two numbers from your back-end: no JavaScript, no chart library. For interactive charts with axes and tooltips, see the Chart component.
  • The colors are the --bar-chart-color and --bar-chart-track custom properties (primary and surface-muted by default; the highlight uses secondary).
  • The horizontal bars grow from the inline start, so they mirror in right-to-left languages.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.