Bar chart
The custom element is <hui-bar-chart>.
Overview
Bars drawn in SVG from data the server writes into a <script type="application/json"> child: one series or several, side by side or stacked, rising or running across. Geometry follows Recharts, which shadcn's charts are drawn with, so widths and gaps match its to the pixel.
Example
Source
<hui-bar-chart index="month" label="Desktop and mobile visitors" grid legend max="320" radius="6" indicator="dashed" tick-length="3" style="height:180px"> <script type="application/json">{"series":[{"key":"desktop","label":"Desktop"},{"key":"mobile","label":"Mobile"}],"rows":[{"month":"January","desktop":186,"mobile":80},{"month":"February","desktop":305,"mobile":200},{"month":"March","desktop":237,"mobile":120},{"month":"April","desktop":73,"mobile":190},{"month":"May","desktop":209,"mobile":130},{"month":"June","desktop":214,"mobile":140}]}</script></hui-bar-chart>The Go template that renders it:
<hui-bar-chart index="month" label="Desktop and mobile visitors" grid legend max="320" radius="6" indicator="dashed" tick-length="3" style="height:180px"> <script type="application/json">{{ .Traffic }}</script></hui-bar-chart>On a phone
On a narrow chart the labels are thinned as Recharts thins them by default (preserveEnd, with a 5px gap): working back from the last, a label is drawn only if it clears the one after it. The first and last are kept inside the element’s box.
Source
<hui-bar-chart index="month" label="Visitors by month" grid style="height:220px"> <script type="application/json">{"series":[{"key":"desktop","label":"Desktop"}],"rows":[{"month":"January","desktop":186},{"month":"February","desktop":305},{"month":"March","desktop":237},{"month":"April","desktop":73},{"month":"May","desktop":209},{"month":"June","desktop":214},{"month":"July","desktop":190},{"month":"August","desktop":260},{"month":"September","desktop":240},{"month":"October","desktop":310},{"month":"November","desktop":280},{"month":"December","desktop":330}]}</script></hui-bar-chart>API
Attributes22
| Name | Type | Default | Description |
|---|---|---|---|
label | string | "" | Required. The chart's accessible name, and the caption of the data table assistive technology reads. |
index | string | "label" | The row property that holds each category. |
layout | "vertical" | "horizontal" | "vertical" | Bars rise from the category axis, or run along it with the categories on the left. |
stacked | boolean | false | One bar per category with the series stacked in it. Only the outer end is rounded, and segments part by a pixel. |
radius | number | 4 | Corner radius of each bar's outer end, in pixels. Never more than half the bar's width. |
bar-size | number | - | A fixed bar width in pixels, centred in its category. |
max-bar-size | number | - | A cap on the bar width, in pixels. Narrower categories still shrink the bars. |
grid | boolean | false | Dashed gridlines at each value tick. The value axis then widens to round ticks. |
value-axis | boolean | false | Value labels along the value axis, formatted by format. |
bare | boolean | false | No axes, no grid, no margin: a strip or a sparkline. |
min | number | - | The value axis' low end. With max, fixes the domain, and gridlines are spaced evenly across it. |
max | number | - | The value axis' high end. Without it the tallest bar fills the plot. |
margin | string | "8 0 0 0" | The plot's margin in pixels, one to four values in CSS order. A bare chart has none. |
tick-margin | number | 8 | Space between the plot and its category labels, in pixels. |
tick-length | number | - | Shortens the category labels on the axis to this many characters. The tooltip and the table keep the whole label. |
tooltip | "index" | "item" | "none" | "index" | What pointing at a category shows: every series in it, only the bar under the pointer, or nothing. |
indicator | "dot" | "line" | "dashed" | "dot" | The marker beside each tooltip row, as shadcn's ChartTooltipContent draws it. |
hide-tooltip-label | boolean | false | Leaves the category heading out of the tooltip. |
legend | boolean | false | A legend under the plot, naming each series. The plot gives up its 28px. |
cursor | boolean | false | A muted band behind the active category. |
format | string | - | Intl.NumberFormat options as JSON, used by the value axis and the data table: {"style":"currency","currency":"USD"}. |
locale | string | the document's lang | The locale values are formatted in. |
Properties22
| Name | Type | Default | Description |
|---|---|---|---|
tickLength | number | null | as <code>tick-length</code> | Reflects to the tick-length attribute. |
tooltip | TooltipMode | as <code>tooltip</code> | Reflects to the tooltip attribute. |
indicator | Indicator | as <code>indicator</code> | Reflects to the indicator attribute. |
hideTooltipLabel | boolean | as <code>hide-tooltip-label</code> | Reflects to the hide-tooltip-label attribute. |
legend | boolean | as <code>legend</code> | Reflects to the legend attribute. |
cursor | boolean | as <code>cursor</code> | Reflects to the cursor attribute. |
label | string | as <code>label</code> | Reflects to the label attribute. |
index | string | as <code>index</code> | Reflects to the index attribute. |
layout | BarLayout | as <code>layout</code> | Reflects to the layout attribute. |
stacked | boolean | as <code>stacked</code> | Reflects to the stacked attribute. |
radius | number | as <code>radius</code> | Reflects to the radius attribute. |
barSize | number | null | as <code>bar-size</code> | Reflects to the bar-size attribute. |
maxBarSize | number | null | as <code>max-bar-size</code> | Reflects to the max-bar-size attribute. |
grid | boolean | as <code>grid</code> | Reflects to the grid attribute. |
valueAxis | boolean | as <code>value-axis</code> | Reflects to the value-axis attribute. |
bare | boolean | as <code>bare</code> | Reflects to the bare attribute. |
min | number | null | as <code>min</code> | Reflects to the min attribute. |
max | number | null | as <code>max</code> | Reflects to the max attribute. |
margin | string | null | as <code>margin</code> | Reflects to the margin attribute. |
tickMargin | number | as <code>tick-margin</code> | Reflects to the tick-margin attribute. |
format | string | null | as <code>format</code> | Reflects to the format attribute. |
locale | string | null | as <code>locale</code> | Reflects to the locale attribute. |
Events1
| Name | Description |
|---|---|
hui-chart-focus | The reader moved to a category, by pointer or keyboard: its position, and the row as the server sent it. It bubbles and is composed. Announced, never stored: a host can mirror it elsewhere on the page. |
Slots1
| Name | Description |
|---|---|
(default) | One <script type="application/json"> holding series and rows. Replacing it, or its text, redraws the chart. |
CSS custom properties2
| Name | Default | Description |
|---|---|---|
--hui-chart-1 … --hui-chart-5 | the preset's violet | Series colours, by position, repeating after the fifth. |
--hui-series-<key> | - | One series' colour, set from the host's stylesheet: --hui-series-mobile: var(--hui-chart-4). A colour field in the JSON wins over it. |
::part() hooks11
| Name | Description |
|---|---|
figure | The wrapper carrying role="figure" and the label. |
plot | The <svg>. Hidden from assistive technology; the table stands in for it. |
grid | The gridlines group. |
axis-x | The labels under the plot. |
axis-y | The labels beside the plot. |
series | One group per series, with data-key. |
mark | One bar, with data-series-index and data-index. |
table | The visually hidden data table. |
tooltip | The tooltip: a manual popover, placed beside the active category. |
legend | The legend row, when legend is set. |
announcer | The visually hidden live region. |
States
One series, capped width
One series, with max-bar-size capping how wide a bar may grow, a per-series colour carried in the data, and hide-tooltip-label dropping the tooltip's heading.
Source
<hui-bar-chart label="Contributions, last six months" max-bar-size="40" radius="6" hide-tooltip-label style="height:200px"> <script type="application/json">{"series":[{"key":"amount","label":"Contribution","colour":"var(--hui-chart-2)"}],"rows":[{"label":"Dec","amount":800},{"label":"Jan","amount":1100},{"label":"Feb","amount":900},{"label":"Mar","amount":1300},{"label":"Apr","amount":750},{"label":"May","amount":1400}]}</script></hui-bar-chart>Stacked, bare
stacked piles the series within each row, and bare strips the axes, grid and inset for a chart that sits inside a card.
Source
<hui-bar-chart label="Sleep by stage, hours" stacked bare bar-size="16" radius="2" style="height:128px"> <script type="application/json">{"series":[{"key":"deep","label":"Deep"},{"key":"light","label":"Light"},{"key":"rem","label":"REM"}],"rows":[{"label":"Mon","light":6},{"label":"Tue","deep":4,"light":2},{"label":"Wed","deep":8,"rem":2},{"label":"Thu","deep":6,"light":1,"rem":3},{"label":"Fri","deep":2,"light":4,"rem":6},{"label":"Sat","deep":5,"light":2,"rem":4},{"label":"Sun","deep":3,"light":5,"rem":2}]}</script></hui-bar-chart>Horizontal, with a value axis
layout="horizontal" turns the bars across the page, value-axis labels the values along the bottom, and cursor shades the row under the pointer.
Source
<hui-bar-chart index="month" label="Desktop visitors" layout="horizontal" value-axis grid tick-length="3" cursor style="height:200px"> <script type="application/json">{"series":[{"key":"desktop","label":"Desktop"}],"rows":[{"month":"January","desktop":186},{"month":"February","desktop":305},{"month":"March","desktop":237},{"month":"April","desktop":73},{"month":"May","desktop":209},{"month":"June","desktop":214}]}</script></hui-bar-chart>A sparkline in a list row
The bare chart at the size of a list row, inside a .hui-item: no axes, a fixed max so rows can be compared, and its data still readable as a table by a screen reader.
Source
<div class="hui-item hui-item--muted" style="max-width:22rem"> <div class="hui-item__content"> <div class="hui-item__title">Vanguard VIG</div> <p class="hui-item__description" style="margin:0">450 shares</p> </div> <hui-bar-chart label="Vanguard VIG dividends by quarter" bare max="800" radius="3" hide-tooltip-label style="width:96px;height:32px;flex:none"> <script type="application/json">{"series":[{"key":"dividend","label":"Dividend","colour":"var(--hui-chart-2)"}],"rows":[{"label":"Q1","dividend":380},{"label":"Q2","dividend":420},{"label":"Q3","dividend":390},{"label":"Q4","dividend":652}]}</script> </hui-bar-chart> <span style="font-weight:500">$1,842.10</span></div>Server-side mechanics
The server owns all of it: it writes the series and the rows as JSON into the element's <script type="application/json"> child, sorted as they should be drawn - the chart never reorders, filters or aggregates. html/template escapes a Go value into that script correctly: {{ .Chart }}. An hx-swap that replaces the child redraws the same element without animating; one that replaces the element draws the new one from its markup. The chart keeps nothing of its own to lose.
Accessibility
- The chart is a
figurenamed bylabel, and its data is a real, visually hidden table with that label as its caption: a screen reader reads the numbers rather than a description of a picture. The SVG is hidden from the accessibility tree. - The preset's five chart colours are steps of one violet, so neighbouring series can sit 1.24:1 apart. Colour is never the only cue: stacked segments part by a pixel, and every series is named in the table.
- Under forced colours every bar is outlined in
CanvasTextand each series position takes its own fill pattern. - The plot is one tab stop. The arrow keys move between categories with the tooltip following, and a polite live region reads each move - the category and every series' value. Pointing is not read out.
- The tooltip is a manual popover in the top layer, hidden from assistive technology: what it shows, the live region and the table already say.
- The first draw grows the bars from the baseline over 400ms. Under
prefers-reduced-motion: reducethey appear at full height, and no redraw after a swap animates.
Keyboard
| Keys | Action |
|---|---|
Tab | Focuses the plot, one stop per chart, and shows the tooltip at the category the keyboard was last on. |
Arrow Right, Arrow Down | The next category. |
Arrow Left, Arrow Up | The previous category. |
Home, End | The first and the last category. |
Escape | Hides the tooltip; focus stays on the plot. |
Gotchas
- A chart fills its container's width and is 16:9 unless given a height. In a flex row, give it or its wrapper a width.
- A key must be a plain identifier - letters, digits,
-and_- because it names the--hui-series-<key>variable. Anything else is rejected, the element getsdata-invalid, and one console warning says why. - The charts are a third file,
home-ui-charts.js, not part ofhome-ui.js: a page that draws a chart loads it too.