ChartFrame
Responsive SVG chart shell — measures its width, applies plot margins, and hands the drawable 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 and margin. Map your data onto innerWidth/innerHeight and draw raw SVG, styling strokes and fills with the design-token utilities (stroke-primary, stroke-border-default) so theming and dark mode flow automatically.<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, useful for 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.
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.
Always pass an ariaLabel
Always pass an ariaLabel. Without it the chart is an unlabelled image — fine
only when an adjacent caption already conveys the same information.
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. | |
slotClasses | ChartSlotClasses | — | 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 | — | |
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]. | |
ChartSlotClasses | type | helper | 1 | Per-slot class overrides for the charts family. | |
ChartSlot | type | helper | 0 | Union of chart slot names (kept in sync with chartVariants). |
Installation
Import
import { ChartFrame } from '@urbicon-ui/blocks';