Bar listFree

Named values ranked largest first, each with a bar sized to the largest; a real list, with optional links. Plain HTML: no chart engine.

npx shadcn@latest add https://beautifulcharts.dev/r/bar-list.json

Usage

A complete client module. Install the item first, then paste it into your app.

"use client";

import { BarList } from "@/components/beautiful-charts/themes/instrument/bar-list";

const sources = [
  { source: "Search", visits: 4820, href: "/analytics/search" },
  { source: "Direct", visits: 3110, href: "/analytics/direct" },
  { source: "Newsletter", visits: 1290, href: "/analytics/newsletter" },
  { source: "Partners", visits: null, href: "/analytics/partners" },
];

// Ranked largest first. The row with no value is listed with a dash and no bar, never as zero.
export function TrafficSources() {
  return <BarList data={sources} nameKey="source" valueKey="visits" hrefKey="href" label="Traffic sources" showShare />;
}

Props

  • data*

    TDatum[]

    Your rows.

  • nameKey*

    keyof TDatum & string

    The row's name.

  • valueKey*

    keyof TDatum & string

    The row's number.

  • hrefKey

    keyof TDatum & string

    Optional link for each row: the name becomes a link.

  • sort

    "descending" | "ascending" | "none"

    "descending" (default), "ascending", or "none" to keep your order. Missing values always go last.

  • label

    string

    What the list is, e.g. "Top pages". Names it for screen readers.

Sourcecomponents/beautiful-charts/themes/instrument/bar-list.tsx

Show the source (52 lines)
"use client";

import { useEffect, useState, type CSSProperties } from "react";
import { useBarListModel, type BarListProps } from "../../core/bar-list";
import { InstrumentStyles } from "./styles";

export type BarListChartProps<TDatum extends Record<string, unknown>> = BarListProps<TDatum> & {
  /** The bar colour; defaults to the theme's first palette colour. Any CSS colour or var(). */
  color?: string | undefined;
  /** Classes on the list. */
  className?: string | undefined;
};

/** Sample data used when you install the bar list; replace it with your own rows. */
export const barListSample = [
  { page: "/pricing", visits: 4120 }, { page: "/docs/installation", visits: 3380 }, { page: "/components/bar-chart", visits: 2650 },
  { page: "/blocks/saas-dashboard", visits: 1790 }, { page: "/changelog", visits: null }, { page: "/about", visits: 640 },
];

/**
 * Bar list, Instrument theme. Plain HTML: no chart engine.
 * Named values ranked largest first, each with a bar behind its name sized to the largest. It's a real list, so
 * screen readers read every name and value in order; rows with a link are ordinary links. A missing value is a dash
 * with no bar; a negative one is listed but not drawn.
 */
export function BarList<TDatum extends Record<string, unknown>>(props: BarListChartProps<TDatum>) {
  const m = useBarListModel(props);
  const [entered, setEntered] = useState(false);
  // Entered on the next frame (not synchronously in the effect), so the entry animation has a first frame to start from.
  useEffect(() => { const frame = requestAnimationFrame(() => setEntered(true)); return () => cancelAnimationFrame(frame); }, []);
  return (
    <div data-slot="bar-list" data-bc-theme={"Instrument".toLowerCase()} data-entered={entered && m.k > 0 ? "" : undefined} className={props.className}
      style={{ ["--bc-k" as string]: m.k, ...(props.color ? { ["--bar-list-color" as string]: props.color } : {}) } as CSSProperties}>
      <InstrumentStyles />
      <ul data-slot="bar-list-rows" aria-label={m.summary}>
        {m.rows.map((row, i) => (
          <li key={row.key} data-slot="bar-list-row" data-missing={row.value === null ? "" : undefined} style={{ ["--w" as string]: row.width ?? 0, ["--c" as string]: i } as CSSProperties}>
            {row.width !== null ? <span data-slot="bar-list-bar" aria-hidden="true" /> : null}
            <span data-slot="bar-list-name">{row.href ? <a href={row.href}>{row.name}</a> : row.name}</span>
            <span data-slot="bar-list-value">
              {row.text}
              {props.showShare && row.share !== null ? <span data-slot="bar-list-share"> · {m.f.percent(row.share, row.share < 0.1 ? 1 : 0)}</span> : null}
            </span>
          </li>
        ))}
      </ul>
      {m.note ? <p data-slot="bar-list-note">{m.note}</p> : null}
      {!m.rows.length ? <p data-slot="bar-list-note">No data yet.</p> : null}
    </div>
  );
}

Details

The other 5 bar list props
  • showShare

    boolean

    Show each row's share of the total next to its value.

  • valueFormatter

    (value: number) => string

    Formats the values.

  • motion

    MotionPreference

    "full" (default), "subtle" or "off". Reduced-motion settings always win.

  • color

    string

    The bar colour; defaults to the theme's first palette colour. Any CSS colour or var().

  • className

    string

    Classes on the list.

Props every chart shares (2): locale, timeZone
  • locale

    string

    BCP 47 locale. Defaults to "en-US" so server and client render the same text.

  • timeZone

    string

    IANA time zone for dates. Defaults to "UTC" so server and client agree.

Pointer
Rows with a link are ordinary links; otherwise not interactive.
Keyboard
No tab stops of its own; a row with a link is one ordinary link.
Screen readers
A list named by one sentence (how many rows, which leads and its share, anything missing or negative); every row's name and value is read in order.
Motion
The bars grow from the left, one after another. With reduced motion it appears settled at once.