ChartFramebeta
Responsive SVG chart shell: it measures its width, applies plot margins, and hands the plot geometry to a child snippet. The building block under every cartesian chart.
Playground
<script lang="ts">
import { ChartFrame } from '@urbicon-ui/blocks';
const demo = [12, 18, 14, 22, 19, 26, 24];
</script>
<ChartFrame
height={200}
>
{#snippet children({ innerWidth, innerHeight })}
{@const max = Math.max(...demo)}
{@const pts = demo.map((v, i) => ({
x: (i / (demo.length - 1)) * innerWidth,
y: innerHeight - (v / max) * innerHeight
}))}
<!-- value axis + baseline make the margins visible -->
<line x1="0" y1="0" x2="0" y2={innerHeight} class="stroke-border-subtle" />
<line
x1="0"
y1={innerHeight}
x2={innerWidth}
y2={innerHeight}
class="stroke-border-default"
/>
<polyline
points={pts.map((p) => `${p.x},${p.y}`).join(' ')}
fill="none"
class="stroke-primary"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
{#each pts as p (p.x)}
<circle cx={p.x} cy={p.y} r="3.5" class="fill-primary" />
{/each}
{/snippet}
</ChartFrame>01 When to use
ChartFrame is the low-level shell every cartesian chart in the family is built on. It owns the
parts that are tedious to get right (responsive width via a ResizeObserver, plot margins, the viewBox, and the accessible role="img" wrapper) and hands you the measured plot geometry so you only draw the marks.
Reach for it only when the chart you need isn't already in the family. For the common cases prefer the ready-made charts — they handle scales, axes, legends and tooltips for you:
LineChart/AreaChart— trends over a continuous axisBarChart— categorical comparisonsDonutChart— part-to-wholeSparkline— a tiny inline trend with no axes
Use ChartFrame directly for a bespoke mark type the family doesn't cover (a candlestick, a radial plot, a custom annotation layer). You keep the responsive, accessible shell and own only the SVG inside it.
02 Examples
Custom marks
children snippet receives the plot geometry (width, height, innerWidth, innerHeight, margin) and draws inside the plot area: (0,0) is the top-left after margins, so map onto innerWidth/innerHeight and never offset by margin yourself. Style strokes and fills with the design-token utilities (stroke-primary, stroke-border-default) so theming and dark mode follow.<script lang="ts">
import { ChartFrame } from '@urbicon-ui/blocks';
// Your own domain data — ChartFrame stays data-agnostic.
const data = [12, 18, 14, 22, 19, 26, 24];
</script>
<!--
ChartFrame measures its width and hands the drawable plot geometry to the
`children` snippet. You map your data onto `innerWidth`/`innerHeight` and draw
raw SVG marks — here a baseline, a trend line and data points.
-->
<ChartFrame height={200} ariaLabel="Weekly values" class="max-w-md">
{#snippet children({ innerWidth, innerHeight })}
{@const max = Math.max(...data)}
{@const points = data.map((v, i) => ({
x: (i / (data.length - 1)) * innerWidth,
y: innerHeight - (v / max) * innerHeight
}))}
<line x1="0" y1={innerHeight} x2={innerWidth} y2={innerHeight} class="stroke-border-default" />
<polyline
points={points.map((p) => `${p.x},${p.y}`).join(' ')}
fill="none"
class="stroke-primary"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
{#each points as p (p.x)}
<circle cx={p.x} cy={p.y} r="3.5" class="fill-primary" />
{/each}
{/snippet}
</ChartFrame>
Legend & accessible fallback
ariaLabel names the chart, the legend snippet renders HTML below the SVG, and the fallback snippet is rendered visually hidden for screen readers: a data table here, so the underlying numbers stay reachable.| Day | Revenue (k€) |
|---|---|
| Mon | 12 |
| Tue | 19 |
| Wed | 15 |
| Thu | 24 |
| Fri | 21 |
<script lang="ts">
import { ChartFrame } from '@urbicon-ui/blocks';
const data = [
{ label: 'Mon', value: 12 },
{ label: 'Tue', value: 19 },
{ label: 'Wed', value: 15 },
{ label: 'Thu', value: 24 },
{ label: 'Fri', value: 21 }
];
const max = Math.max(...data.map((d) => d.value));
</script>
<!--
`ariaLabel` names the chart image, `legend` renders HTML below the SVG, and
`fallback` is visually hidden but read by screen readers — here a data table
so the numbers are never lost to assistive tech.
-->
<ChartFrame height={220} ariaLabel="Daily revenue, Monday to Friday" class="max-w-md">
{#snippet children({ innerWidth, innerHeight })}
{@const gap = 8}
{@const bw = (innerWidth - gap * (data.length - 1)) / data.length}
{#each data as d, i (d.label)}
{@const h = (d.value / max) * innerHeight}
<rect
x={i * (bw + gap)}
y={innerHeight - h}
width={bw}
height={h}
rx="3"
class="fill-primary"
/>
{/each}
{/snippet}
{#snippet legend()}
<div class="text-text-secondary mt-2 flex items-center gap-1.5 text-xs">
<span class="bg-primary inline-block size-2.5 rounded-[2px]"></span>
Revenue (k€)
</div>
{/snippet}
{#snippet fallback()}
<table>
<caption>Daily revenue, Monday to Friday</caption>
<thead>
<tr><th>Day</th><th>Revenue (k€)</th></tr>
</thead>
<tbody>
{#each data as d (d.label)}
<tr><td>{d.label}</td><td>{d.value}</td></tr>
{/each}
</tbody>
</table>
{/snippet}
</ChartFrame>
Fixed width (SSR-stable)
width for responsive measurement (the common case). Set it to opt out: the SVG renders at exactly that width on the server with no layout shift, which suits emails, PDFs, or snapshot tests.<ChartFrame width={480} height={200} ariaLabel="Static chart">
{#snippet children({ innerWidth, innerHeight })}
<!-- drawn at a deterministic 480×200, no ResizeObserver -->
{/snippet}
</ChartFrame>03 Accessibility
One named image
The SVG carries role="img" with your ariaLabel, so assistive tech
announces the chart as a single named image rather than reading out every path and number.
Always pass one; without it the chart is an unlabelled image, fine only when an adjacent
caption already conveys the same information.
Supply a fallback for the detail
Because role="img" hides the SVG internals, supply a fallback snippet — rendered visually hidden — when the detail matters. A data <table> is the most robust choice.
The legend is real HTML
The legend snippet is ordinary HTML below the SVG, so its text is selectable and
in the accessibility tree without extra work.
API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
ariaLabel | string | — | Accessible label for the chart image (role="img"). | |
children | Snippet<[ChartPlot]> | — | Renders the SVG plot content; receives the ChartPlot geometry. | |
class | string | — | Extra classes merged onto the <figure> wrapper. | |
fallback | Snippet | — | Screen-reader fallback (e.g. a data table) rendered visually hidden. | |
height | number | 240 | Fixed SVG height in px. | |
legend | Snippet | — | Optional legend rendered (as HTML) below the SVG. | |
margin | ChartMargin | — | Plot margins; merged over the frame defaults. | |
preset | string | — | Apply a named preset registered on <BlocksProvider>. | |
slotClasses | ChartFrameSlotClasses | — | Per-slot class overrides. | |
unstyled | boolean | — | Remove all default tv classes. | |
width | number | — | Fixed width in px. Omit for responsive width measured from the container (the common case); set it only for SSR-stable, non-responsive output. | |
...HTMLAttributes<HTMLElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children') |
04 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
ChartFrameProps | interface | props | 0 | — | |
ChartFrameSlotClasses | type | helper | 1 | Per-slot class overrides for <ChartFrame>. | |
ChartMargin | interface | helper | 1 | Plot margins (px). Any omitted side falls back to the frame default. | |
ChartPlot | interface | helper | 0 | Plot geometry passed to a ChartFrame children snippet. The snippet renders
inside a <g> translated to the plot's top-left corner, so marks use local
coordinates in [0, innerWidth] × [0, innerHeight]. | |
ChartFrameSlot | type | helper | 0 | Slots <ChartFrame> paints — the wrapper and the SVG it measures. |
Installation
Import
import { ChartFrame } from '@urbicon-ui/blocks';