Charts

Chart legend

A list of series names with a mark and an optional value, for any chart or one of your own.

Built on
<svg>color-mix()subgrid
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.8 kB
Dependencies
React
  • Team1,284
  • Business642
  • Enterprise118
  • Trial407
Try its options

Loading its options.

Code
import { ChartLegend } from "@/components/equip/chart-legend";

const plans = [
  { label: "Team", value: 1284 },
  { label: "Business", value: 642 },
  { label: "Enterprise", value: 118 },
  { label: "Trial", value: 407 },
];

export default function Demo() {
  return <ChartLegend items={plans} orientation="column" aria-label="Accounts by plan" style={{ width: 168 }} />;
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<ul role="list" aria-label="Accounts by plan" style="width:168px" class="eq-chart-legend" data-orientation="column">
  <li class="eq-chart-legend-item" data-series="0"><span class="eq-chart-legend-mark" data-mark="square" aria-hidden="true"></span><span class="eq-chart-legend-name">Team</span><span class="eq-chart-legend-value">1,284</span></li>
  <li class="eq-chart-legend-item" data-series="1"><span class="eq-chart-legend-mark" data-mark="square" aria-hidden="true"></span><span class="eq-chart-legend-name">Business</span><span class="eq-chart-legend-value">642</span></li>
  <li class="eq-chart-legend-item" data-series="2"><span class="eq-chart-legend-mark" data-mark="square" aria-hidden="true"></span><span class="eq-chart-legend-name">Enterprise</span><span class="eq-chart-legend-value">118</span></li>
  <li class="eq-chart-legend-item" data-series="3"><span class="eq-chart-legend-mark" data-mark="square" aria-hidden="true"></span><span class="eq-chart-legend-name">Trial</span><span class="eq-chart-legend-value">407</span></li>
</ul>

Examples

Line marks

Each series step has its own dash as well as its own strength, so a legend for lines still reads in greyscale or print.

  • p50
  • p90
  • p95
  • p99
Code
import { ChartLegend } from "@/components/equip/chart-legend";

export default function Example() {
  return (
    <ChartLegend
      mark="line"
      aria-label="Latency percentiles"
      items={[{ label: "p50" }, { label: "p90" }, { label: "p95" }, { label: "p99" }]}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<ul role="list" aria-label="Latency percentiles" class="eq-chart-legend" data-orientation="row">
  <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p50</span></li>
  <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p90</span></li>
  <li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p95</span></li>
  <li class="eq-chart-legend-item" data-series="3"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">p99</span></li>
</ul>

Point shapes with values

Point marks change shape per series, and numbers are formatted with the same options as the chart they sit beside.

  • Organic search$84
  • Paid social$61
  • Email$112
  • Referral$97
Code
import { ChartLegend } from "@/components/equip/chart-legend";

export default function Example() {
  return (
    <ChartLegend
      mark="point"
      orientation="column"
      aria-label="Average order value by channel"
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      items={[
        { label: "Organic search", value: 84 },
        { label: "Paid social", value: 61 },
        { label: "Email", value: 112 },
        { label: "Referral", value: 97 },
      ]}
      style={{ maxWidth: 260 }}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<ul role="list" aria-label="Average order value by channel" style="max-width:260px" class="eq-chart-legend" data-orientation="column">
  <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="3.75" /></svg><span class="eq-chart-legend-name">Organic search</span><span class="eq-chart-legend-value">$84</span></li>
  <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7" /></svg><span class="eq-chart-legend-name">Paid social</span><span class="eq-chart-legend-value">$61</span></li>
  <li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><path d="M5 0.75 9.25 5 5 9.25 0.75 5Z" /></svg><span class="eq-chart-legend-name">Email</span><span class="eq-chart-legend-value">$112</span></li>
  <li class="eq-chart-legend-item" data-series="3"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><path d="M5 1 9.25 8.75H0.75Z" /></svg><span class="eq-chart-legend-name">Referral</span><span class="eq-chart-legend-value">$97</span></li>
</ul>

Beside your own chart

Pass series to pin an item to a step, so a legend for a filtered chart keeps the same marks as the full one.

Showing paid channels only
  • Paid search38%
  • Paid social21%
Code
import { ChartLegend } from "@/components/equip/chart-legend";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 8, maxWidth: 320 }}>
      <span style={{ fontSize: "var(--eq-text-s)", color: "var(--eq-ink-2)" }}>Showing paid channels only</span>
      <ChartLegend
        aria-label="Paid channels"
        items={[
          { label: "Paid search", value: "38%", series: 1 },
          { label: "Paid social", value: "21%", series: 3 },
        ]}
      />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;max-width:320px">
  <span style="font-size:var(--eq-text-s);color:var(--eq-ink-2)">Showing paid channels only</span>
  <ul role="list" aria-label="Paid channels" class="eq-chart-legend" data-orientation="row">
    <li class="eq-chart-legend-item" data-series="1"><span class="eq-chart-legend-mark" data-mark="square" aria-hidden="true"></span><span class="eq-chart-legend-name">Paid search</span><span class="eq-chart-legend-value">38%</span></li>
    <li class="eq-chart-legend-item" data-series="3"><span class="eq-chart-legend-mark" data-mark="square" aria-hidden="true"></span><span class="eq-chart-legend-name">Paid social</span><span class="eq-chart-legend-value">21%</span></li>
  </ul>
</div>

Props

PropTypeDefaultWhat it does
items{ label: string; value?: number | ReactNode; series?: number }[]noneOne entry per series. A numeric value is formatted; series picks the step and defaults to the position.
mark"square" | "line" | "point""square"A filled square for bars and slices, a short dashed line for lines and areas, a shape for points.
orientation"row" | "column""row"A wrapping row, or a column with names and values lined up.
formatIntl.NumberFormatOptionsnoneHow numeric values are written.
localestring"en-US"Passed to Intl.NumberFormat.
...restComponentProps<"ul">noneEvery native list attribute. Name it with aria-label when it stands alone.

Anatomy

root.eq-chart-legendA list; data-orientation sets row or column.
item.eq-chart-legend-itemdata-series sets the ink strength and dash for that step.
mark.eq-chart-legend-markdata-mark is square, line or point.
name.eq-chart-legend-name
value.eq-chart-legend-value

Notes

  • Steps are ink at 100, 70, 48, 32 and 20 percent, mixed with transparent, so they follow every finish, theme and accent.
  • Line marks add a dash per step and point marks a shape per step, so series are never told apart by colour alone.
  • The marks are hidden from assistive tech; the names and values are real text.
  • Line chart, area chart, stacked bar, scatter plot and radar chart use it for their legends and readouts.
  • Renders on the server with no client JavaScript.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/chart-legend.tsx72 lines
import type { ComponentProps, ReactNode } from "react";
import "./chart-legend.css";

export type ChartLegendItem = {
  label: string;
  /** A number is formatted with format and locale; anything else is shown as given. */
  value?: number | ReactNode;
  /** Which series step the mark draws, from 0. Defaults to the item's position. */
  series?: number;
};

export type ChartLegendProps = ComponentProps<"ul"> & {
  items: ChartLegendItem[];
  /** How each mark is drawn: a filled square for bars and slices, a short line for lines and areas, a shape for points. */
  mark?: "square" | "line" | "point";
  /** A row that wraps, or a column with the values lined up. */
  orientation?: "row" | "column";
  format?: Intl.NumberFormatOptions;
  locale?: string;
};

export function ChartLegend({
  items,
  mark = "square",
  orientation = "row",
  format,
  locale = "en-US",
  className,
  ...rest
}: ChartLegendProps) {
  const number = new Intl.NumberFormat(locale, format);

  return (
    <ul
      role="list"
      {...rest}
      className={["eq-chart-legend", className].filter(Boolean).join(" ")}
      data-orientation={orientation}
    >
      {items.map((item, i) => {
        const s = Math.max(0, Math.min(4, Math.floor(item.series ?? i)));
        const value = typeof item.value === "number" ? number.format(item.value) : item.value;
        return (
          <li key={`${item.label}-${i}`} className="eq-chart-legend-item" data-series={s}>
            <Mark kind={mark} series={s} />
            <span className="eq-chart-legend-name">{item.label}</span>
            {value != null && <span className="eq-chart-legend-value">{value}</span>}
          </li>
        );
      })}
    </ul>
  );
}

function Mark({ kind, series }: { kind: "square" | "line" | "point"; series: number }) {
  if (kind === "square") return <span className="eq-chart-legend-mark" data-mark="square" aria-hidden="true" />;
  if (kind === "line") {
    return (
      <svg className="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10">
        <line x1="1" y1="5" x2="17" y2="5" />
      </svg>
    );
  }
  return (
    <svg className="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10">
      {series % 4 === 0 && <circle cx="5" cy="5" r="3.75" />}
      {series % 4 === 1 && <rect x="1.5" y="1.5" width="7" height="7" />}
      {series % 4 === 2 && <path d="M5 0.75 9.25 5 5 9.25 0.75 5Z" />}
      {series % 4 === 3 && <path d="M5 1 9.25 8.75H0.75Z" />}
    </svg>
  );
}
components/equip/chart-legend.css101 lines
@layer equip {
  .eq-chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--eq-space-1) var(--eq-space-3);
    margin: 0;
    padding: 0;
    list-style: none;
    font: var(--eq-weight) var(--eq-text-xs) / 1.3 var(--eq-font);
    color: var(--eq-ink-2);
  }

  .eq-chart-legend[data-orientation="column"] {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: var(--eq-space-1) var(--eq-space-2);
  }

  /* The series steps every chart shares: ink at a lower strength for each step,
     and for lines a dash of its own, so no series is told apart by colour alone. */
  .eq-chart-legend-item {
    --_tone: var(--eq-chart-1);
    --_dash: none;

    display: inline-flex;
    align-items: center;
    gap: var(--eq-space-2);
    min-width: 0;
  }

  .eq-chart-legend-item[data-series="1"] {
    --_tone: var(--eq-chart-2);
    --_dash: 5 4;
  }

  .eq-chart-legend-item[data-series="2"] {
    --_tone: var(--eq-chart-3);
    --_dash: 0.1 4;
  }

  .eq-chart-legend-item[data-series="3"] {
    --_tone: var(--eq-chart-4);
    --_dash: 8 4 0.1 4;
  }

  .eq-chart-legend-item[data-series="4"] {
    --_tone: var(--eq-chart-5);
  }

  .eq-chart-legend[data-orientation="column"] .eq-chart-legend-item {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
  }

  .eq-chart-legend-mark {
    display: block;
    flex: none;
    overflow: visible;
  }

  .eq-chart-legend-mark[data-mark="square"] {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: var(--_tone, var(--eq-ink));
    box-shadow: inset 0 0 0 1px var(--eq-line);
  }

  .eq-chart-legend-mark[data-mark="line"] line {
    stroke: var(--_tone, var(--eq-ink));
    stroke-width: 2;
    stroke-linecap: round;
    stroke-dasharray: var(--_dash);
  }

  .eq-chart-legend-mark[data-mark="point"] > * {
    fill: color-mix(in srgb, var(--_tone, var(--eq-ink)) 35%, transparent);
    stroke: var(--_tone, var(--eq-ink));
    stroke-width: 1.5;
    stroke-linejoin: round;
  }

  .eq-chart-legend-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .eq-chart-legend-value {
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .eq-chart-legend[data-orientation="column"] .eq-chart-legend-value {
    text-align: end;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index