diff --git a/packages/demo/src/components/demo/bar-graph.tsx b/packages/demo/src/components/demo/bar-graph.tsx
new file mode 100644
index 0000000..19a9eb3
--- /dev/null
+++ b/packages/demo/src/components/demo/bar-graph.tsx
@@ -0,0 +1,145 @@
+import { BarGraph, BarGraphSegment } from "@eqtylab/equality";
+
+export function BarGraphDemo() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const controlsSegments = () => [
+ ,
+ ,
+ ,
+];
+
+export function BarGraphSizeSmDemo() {
+ return (
+
+ {controlsSegments()}
+
+ );
+}
+
+export function BarGraphSizeMdDemo() {
+ return (
+
+ {controlsSegments()}
+
+ );
+}
+
+export function BarGraphSizeLgDemo() {
+ return (
+
+ {controlsSegments()}
+
+ );
+}
+
+export function BarGraphWithLabelsDemo() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export function BarGraphRichTooltipDemo() {
+ return (
+
+
+
+ Success
+ 10 controls
+
+ }
+ />
+
+ Pending
+ 290 controls
+
+ }
+ />
+
+ Failure
+ 8 controls
+
+ }
+ />
+
+
+ );
+}
diff --git a/packages/demo/src/content/components/bar-graph.mdx b/packages/demo/src/content/components/bar-graph.mdx
new file mode 100644
index 0000000..6ad5c11
--- /dev/null
+++ b/packages/demo/src/content/components/bar-graph.mdx
@@ -0,0 +1,114 @@
+---
+layout: "@demo/layouts/mdx-layout.astro"
+heading: "Bar Graph"
+description: "Single line segmented bar graph with segment tooltips"
+---
+
+import {
+ BarGraphDemo,
+ BarGraphSizeSmDemo,
+ BarGraphSizeMdDemo,
+ BarGraphSizeLgDemo,
+ BarGraphWithLabelsDemo,
+ BarGraphRichTooltipDemo,
+} from "@demo/components/demo/bar-graph";
+
+## Overview
+
+`BarGraph` shows a single horizontal bar split into colored segments. Use it to break a total down into its parts, such as controls status.
+
+Add a `BarGraphSegment` for each part. Widths come from each segment's `value` as a share of the total (`value / sum of all values`), so you pass raw numbers rather than percentages. Children that aren't a `BarGraphSegment` are ignored.
+
+Segments are focusable and show a [Tooltip](tooltip) on hover and focus, so they work by keyboard alone. Each segment is exposed individually to screen readers, with its `tooltip` as the accessible name — there's no single label for the bar as a whole, so screen reader users read it one segment at a time. Write each `tooltip` to describe its segment on its own.
+
+## Usage
+
+Import the component:
+
+```tsx
+import { BarGraph, BarGraphSegment } from "@eqtylab/equality";
+```
+
+Basic usage with required properties:
+
+```tsx
+
+
+
+
+
+```
+
+## Variants
+
+### Default
+
+There's no visible legend by default — each segment's accessible name comes from its tooltip. Hover or focus a segment to reveal it.
+
+
+
+### Sizes
+
+Use `size` to control the bar's height. `sm` is a thin pill; the default `md` and `lg` are bolder and use rounded rectangles instead of a full pill. Note the small `Failure` segment, which is floored at 2px so it stays visible and focusable at every size.
+
+#### Small
+
+
+
+#### Medium (default)
+
+
+
+#### Large
+
+
+
+```tsx
+{/* segments */}
+```
+
+### With visible labels
+
+Set `showLabels` to render a legend beneath the bar. Note that this legend will not be read aloud by screen readers, content inside it should also be exposed in the section tooltip.
+
+
+
+### Rich tooltip content
+
+The `tooltip` prop accepts a string or any `ReactNode`, so segments can show formatted content.
+
+
+
+## Props
+
+### `BarGraph`
+
+| Name | Description | Type | Default | Required |
+| ------------ | ------------------------------------------------------------------------ | ----------------- | ------- | -------- |
+| `size` | Bar height. `md` and `lg` use rounded rectangles instead of a full pill. | `sm`, `md`, `lg` | `md` | ❌ |
+| `showLabels` | Render a visible legend (color swatch + label) beneath the bar. | `boolean` | `false` | ❌ |
+| `children` | One or more segments to render. | `BarGraphSegment` | — | ✅ |
+
+### `BarGraphSegment`
+
+| Name | Description | Type | Default | Required |
+| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | ------- | -------- |
+| `value` | Sets the segment's width as a share of the total (`value / sum of all values`). Every segment is floored at 2px so small and zero values stay visible. | `number` | — | ✅ |
+| `color` | Any valid CSS color (e.g. a token like `var(--color-brand-green)` or `#10b981`). | `string` | — | ✅ |
+| `label` | Visible legend label, shown when `showLabels` is set. Not announced to screen readers. | `string` | — | ✅ |
+| `tooltip` | Tooltip content shown on hover and keyboard focus; also the segment's accessible name, so it must describe the segment on its own. | `string \| ReactNode` | — | ✅ |
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 9c0d810..436a533 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -2,7 +2,7 @@
"name": "@eqtylab/equality",
"description": "EQTYLab's component and token-based design system",
"homepage": "https://equality.eqtylab.io/",
- "version": "1.8.2",
+ "version": "1.9.0",
"license": "Apache-2.0",
"keywords": [
"component library",
diff --git a/packages/ui/src/components/bar-graph/bar-graph.module.css b/packages/ui/src/components/bar-graph/bar-graph.module.css
new file mode 100644
index 0000000..f3681a3
--- /dev/null
+++ b/packages/ui/src/components/bar-graph/bar-graph.module.css
@@ -0,0 +1,52 @@
+@reference '../../theme/theme.module.css';
+
+.bar-graph {
+ @apply flex w-full flex-col gap-2;
+}
+
+.bar {
+ @apply bg-greyscale-800;
+ @apply flex w-full gap-0.5 overflow-hidden;
+}
+
+/* Size Variants */
+
+.size--sm {
+ @apply h-2 rounded-full;
+}
+
+.size--md {
+ @apply h-5 rounded-sm;
+}
+
+.size--lg {
+ @apply h-8 rounded-sm;
+}
+
+.segment {
+ @apply h-full;
+ flex: 1 1 0%;
+ @apply outline-none;
+ @apply focus-visible:ring-focus-ring focus-visible:ring-2 focus-visible:ring-inset;
+}
+
+/* Screen-reader-only text that backs each segment's accessible name. */
+.visually-hidden {
+ @apply sr-only;
+}
+
+.legend {
+ @apply m-0 flex list-none flex-wrap gap-x-4 gap-y-1 p-0;
+}
+
+.legend-item {
+ @apply flex items-center gap-1.5;
+}
+
+.legend-swatch {
+ @apply size-2.5 shrink-0 rounded-full;
+}
+
+.legend-label {
+ @apply text-text-primary text-xs;
+}
diff --git a/packages/ui/src/components/bar-graph/bar-graph.tsx b/packages/ui/src/components/bar-graph/bar-graph.tsx
new file mode 100644
index 0000000..da9739b
--- /dev/null
+++ b/packages/ui/src/components/bar-graph/bar-graph.tsx
@@ -0,0 +1,140 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import styles from '@/components/bar-graph/bar-graph.module.css';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/tooltip/tooltip';
+import { cn } from '@/lib/utils';
+
+export interface BarGraphSegmentProps extends Omit, 'color'> {
+ /** Proportional value used to compute the segment's width (`value / sum of all values`). */
+ value: number;
+ /** Any valid CSS color, applied as the segment's background. */
+ color: string;
+ /**
+ * Visible legend label, shown when `showLabels` is set on the parent `BarGraph`.
+ * Not announced to screen readers — the segment's accessible name is its `tooltip`.
+ */
+ label: string;
+ /**
+ * Tooltip content shown on hover and keyboard focus. Also the segment's accessible
+ * name, so it must describe the segment on its own. Required.
+ */
+ tooltip: React.ReactNode;
+}
+
+const BarGraphSegment = React.forwardRef(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ({ value, color, label, tooltip, className, style, ...props }, ref) => {
+ const tooltipId = React.useId();
+
+ return (
+ // Widths are proportional (`value / sum`): each segment grows by its `value`
+ // within the flex row, so the parent never needs to know the total.
+ //
+ // Accessibility: `tooltip` is the segment's accessible *name*, not its
+ // description. VoiceOver treats `aria-describedby` as an optional, delayed
+ // hint (only spoken when "Speak hints" is on), so a description can't be
+ // relied on to convey the value — the name always is. We expose it via
+ // `aria-labelledby` → a visually-hidden copy, and null Radix's own
+ // `aria-describedby` so its `role="tooltip"` announcer isn't read on top.
+ // This means `tooltip` is rendered twice (hidden name + visible popup); the
+ // duplication is the cost of a reliably-announced ReactNode name.
+
+
+
+
+
+
+ {tooltip}
+
+
+ );
+ }
+);
+BarGraphSegment.displayName = 'BarGraphSegment';
+
+const barVariants = cva(styles['bar'], {
+ variants: {
+ size: {
+ sm: styles['size--sm'],
+ md: styles['size--md'],
+ lg: styles['size--lg'],
+ },
+ },
+ defaultVariants: {
+ size: 'md',
+ },
+});
+
+/**
+ * A single `BarGraphSegment` element. False values are allowed so conditional
+ * rendering (`{condition && }`) keeps working. Non-segment
+ * children are filtered out at runtime — see `BarGraph`.
+ */
+type BarGraphChild = React.ReactElement | boolean | null | undefined;
+
+export interface BarGraphProps
+ extends Omit, 'children'>, VariantProps {
+ /** Render a visible legend (color swatch + label) beneath the bar. Defaults to `false`. */
+ showLabels?: boolean;
+ /** Bar height. `md` and `lg` use rounded rectangles instead of a full pill. Defaults to `md`. */
+ size?: 'sm' | 'md' | 'lg';
+ /** One or more `BarGraphSegment` elements. */
+ children: BarGraphChild | BarGraphChild[];
+}
+
+const BarGraph = React.forwardRef(
+ ({ className, children, showLabels = false, size, ...props }, ref) => {
+ const validChildren = React.Children.toArray(children).filter(React.isValidElement);
+ const segments = validChildren.filter(
+ (child): child is React.ReactElement => child.type === BarGraphSegment
+ );
+
+ return (
+
+
+
{segments}
+ {showLabels && (
+
+ {segments.map((child, index) => (
+ -
+
+ {child.props.label}
+
+ ))}
+
+ )}
+
+
+ );
+ }
+);
+BarGraph.displayName = 'BarGraph';
+
+export { BarGraph, BarGraphSegment };
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 86ac1a1..3332d27 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -2,6 +2,7 @@ export * from './alert/alert';
export * from './alert-dialog/alert-dialog';
export * from './avatar/avatar';
export * from './badge/badge';
+export * from './bar-graph/bar-graph';
export * from './bg-gradient/bg-gradient';
export * from './button/button';
export * from './resource-badge/resource-badge';