Skip to content
Home Theme Gallery

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

Grouped, with gridlines and a legend
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.

Twelve months on a phone 375 px wide
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
Attributes
NameTypeDefaultDescription
labelstring""Required. The chart's accessible name, and the caption of the data table assistive technology reads.
indexstring"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.
stackedbooleanfalseOne bar per category with the series stacked in it. Only the outer end is rounded, and segments part by a pixel.
radiusnumber4Corner radius of each bar's outer end, in pixels. Never more than half the bar's width.
bar-sizenumber-A fixed bar width in pixels, centred in its category.
max-bar-sizenumber-A cap on the bar width, in pixels. Narrower categories still shrink the bars.
gridbooleanfalseDashed gridlines at each value tick. The value axis then widens to round ticks.
value-axisbooleanfalseValue labels along the value axis, formatted by format.
barebooleanfalseNo axes, no grid, no margin: a strip or a sparkline.
minnumber-The value axis' low end. With max, fixes the domain, and gridlines are spaced evenly across it.
maxnumber-The value axis' high end. Without it the tallest bar fills the plot.
marginstring"8 0 0 0"The plot's margin in pixels, one to four values in CSS order. A bare chart has none.
tick-marginnumber8Space between the plot and its category labels, in pixels.
tick-lengthnumber-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-labelbooleanfalseLeaves the category heading out of the tooltip.
legendbooleanfalseA legend under the plot, naming each series. The plot gives up its 28px.
cursorbooleanfalseA muted band behind the active category.
formatstring-Intl.NumberFormat options as JSON, used by the value axis and the data table: {"style":"currency","currency":"USD"}.
localestringthe document's langThe locale values are formatted in.
Properties22
Properties
NameTypeDefaultDescription
tickLengthnumber | nullas <code>tick-length</code>Reflects to the tick-length attribute.
tooltipTooltipModeas <code>tooltip</code>Reflects to the tooltip attribute.
indicatorIndicatoras <code>indicator</code>Reflects to the indicator attribute.
hideTooltipLabelbooleanas <code>hide-tooltip-label</code>Reflects to the hide-tooltip-label attribute.
legendbooleanas <code>legend</code>Reflects to the legend attribute.
cursorbooleanas <code>cursor</code>Reflects to the cursor attribute.
labelstringas <code>label</code>Reflects to the label attribute.
indexstringas <code>index</code>Reflects to the index attribute.
layoutBarLayoutas <code>layout</code>Reflects to the layout attribute.
stackedbooleanas <code>stacked</code>Reflects to the stacked attribute.
radiusnumberas <code>radius</code>Reflects to the radius attribute.
barSizenumber | nullas <code>bar-size</code>Reflects to the bar-size attribute.
maxBarSizenumber | nullas <code>max-bar-size</code>Reflects to the max-bar-size attribute.
gridbooleanas <code>grid</code>Reflects to the grid attribute.
valueAxisbooleanas <code>value-axis</code>Reflects to the value-axis attribute.
barebooleanas <code>bare</code>Reflects to the bare attribute.
minnumber | nullas <code>min</code>Reflects to the min attribute.
maxnumber | nullas <code>max</code>Reflects to the max attribute.
marginstring | nullas <code>margin</code>Reflects to the margin attribute.
tickMarginnumberas <code>tick-margin</code>Reflects to the tick-margin attribute.
formatstring | nullas <code>format</code>Reflects to the format attribute.
localestring | nullas <code>locale</code>Reflects to the locale attribute.
Events1
Events
NameDescription
hui-chart-focusThe 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
Slots
NameDescription
(default)One <script type="application/json"> holding series and rows. Replacing it, or its text, redraws the chart.
CSS custom properties2
CSS custom properties
NameDefaultDescription
--hui-chart-1 … --hui-chart-5the preset's violetSeries 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
::part() hooks
NameDescription
figureThe wrapper carrying role="figure" and the label.
plotThe <svg>. Hidden from assistive technology; the table stands in for it.
gridThe gridlines group.
axis-xThe labels under the plot.
axis-yThe labels beside the plot.
seriesOne group per series, with data-key.
markOne bar, with data-series-index and data-index.
tableThe visually hidden data table.
tooltipThe tooltip: a manual popover, placed beside the active category.
legendThe legend row, when legend is set.
announcerThe 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.

One series, capped width
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.

Stacked, bare
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.

Horizontal, with a value axis
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.

A sparkline in a list row
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 figure named by label, 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 CanvasText and 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: reduce they appear at full height, and no redraw after a swap animates.

Keyboard

Keyboard
KeysAction
TabFocuses the plot, one stop per chart, and shows the tooltip at the category the keyboard was last on.
Arrow Right, Arrow DownThe next category.
Arrow Left, Arrow UpThe previous category.
Home, EndThe first and the last category.
EscapeHides 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 gets data-invalid, and one console warning says why.
  • The charts are a third file, home-ui-charts.js, not part of home-ui.js: a page that draws a chart loads it too.