Bar Grow
DataA bar chart whose bars grow from the baseline one after another when it scrolls into view; hovering a bar shows its value.
A small line chart that draws itself from left to right when it scrolls into view; the last point lands and pulses. The SVG is rendered on the server from the data, the line is uncovered by a clip that sweeps from its left edge to its right, and the accessible name summarises the series. Composes Reveal; no script of its own.
sample data
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
JavaScript of its own
0 B
Uses Reveal and the shared runtime (1.9 kB raw, once per site). With those included: 3.0 kB raw.
CSS 8.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 55 · Firefox 54 · Safari 13.1; needs clip-path.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import Sparkline from '@moonarc/core/Sparkline';
---
<Sparkline data={[12, 18, 15, 22, 30, 28, 41]} label="Stars per week" class="text-signal" />
<Sparkline data={downloads} width={240} height={48} fill dot={false} duration={1200} />Owns the file, no dependency. The registry item also installs Scroll Reveal, the shared runtime and the base tokens.
npx shadcn@latest add https://moonarc.dev/r/sparkline.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win. If you paste it, also copy Reveal.astro and runtime.ts, and add the JS gate to your head.
---
/**
* Sparkline — a small line chart that draws itself left to right when it
* scrolls into view, the last point pulsing once drawn. The SVG is rendered
* on the server from the data; the line is uncovered by a clip-path inset
* whose right edge travels from the left of the chart to past its right, so
* it draws at any size. Composes Reveal for the trigger; zero script of its
* own. The label summarises the series.
*/
import type { HTMLAttributes, HTMLTag } from 'astro/types';
import Reveal from './Reveal.astro';
interface Props extends HTMLAttributes<'span'> {
/** The series. */
data: number[];
/** Element to render. */
as?: HTMLTag;
/** Size in px — also the viewBox, so the drawing scales with it; it shrinks to fit a narrower container. */
width?: number;
height?: number;
/** Line colour. */
color?: string;
/** Fill the area under the line. */
fill?: boolean;
/** Mark the last point. */
dot?: boolean;
/** Draw duration in ms. Unset: the preset's --ma-duration-slow. */
duration?: number;
/** Accessible name; the summary (min, max, last) is appended. */
label?: string;
/** Delay in ms before drawing. */
delay?: number;
/** Play once and stay. */
once?: boolean;
/** Fraction visible to trigger. */
threshold?: number;
}
const { data, as = 'span', width = 120, height = 32, color = 'var(--chart-1, currentColor)', fill = false, dot = true, duration, label, delay, once, threshold, class: className, style, ...rest } = Astro.props;
const series = data.length ? data : [0];
const n = series.length;
const min = Math.min(...series);
const max = Math.max(...series);
const span = max - min || 1;
const pad = 3;
const x = (i: number) => pad + (n > 1 ? (i / (n - 1)) * (width - 2 * pad) : (width - 2 * pad) / 2);
const y = (v: number) => height - pad - ((v - min) / span) * (height - 2 * pad);
const d = series.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
const area = `${d} L${x(n - 1).toFixed(1)},${height} L${x(0).toFixed(1)},${height} Z`;
const last = series[n - 1]!;
const name = `${label ? `${label}: ` : ''}${n} points, min ${min}, max ${max}, last ${last}`;
const vars = [`--ma-spark-color:${color}`, duration !== undefined && `--ma-spark-dur:${duration}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<Reveal as={as} from="none" delay={delay} once={once} threshold={threshold} class={['ma-spark', className].filter(Boolean).join(' ')} role="img" aria-label={name} style={vars} {...rest}>
<svg class="ma-spark__svg" viewBox={`0 0 ${width} ${height}`} width={width} height={height} aria-hidden="true">
{fill && <path class="ma-spark__area" d={area} />}
<path class="ma-spark__line" d={d} vector-effect="non-scaling-stroke" />
{dot && (
<>
<circle class="ma-spark__ring" cx={x(n - 1).toFixed(1)} cy={y(last).toFixed(1)} r="3" />
<circle class="ma-spark__dot" cx={x(n - 1).toFixed(1)} cy={y(last).toFixed(1)} r="2.5" />
</>
)}
</svg>
</Reveal>
<style is:global>
@layer components {
:where(.ma-spark) {
display: inline-block;
max-width: 100%;
line-height: 0;
vertical-align: middle;
}
:where(.ma-spark__svg) {
display: block;
max-width: 100%;
height: auto;
overflow: visible;
color: var(--ma-spark-color, currentColor);
}
:where(.ma-spark__line) {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
/* A wipe, where this used to be a pathLength="1" dash: the stroke is non-scaling, so its dashes are measured on
screen while pathLength is measured in the viewBox, and a chart drawn four times its viewBox came out as a
quarter dash, a gap, a dash. The inset reaches 1em past every edge so the round caps and joins are not cut. */
clip-path: inset(-1em);
transition: clip-path var(--ma-spark-dur, var(--ma-duration-slow)) var(--ma-ease-out) var(--ma-r-delay, 0ms);
}
:where(.ma-spark__area) {
fill: currentColor;
opacity: 0.12;
transition: opacity var(--ma-duration) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-spark-dur, var(--ma-duration-slow)));
}
:where(.ma-spark__dot) {
fill: currentColor;
transition: opacity var(--ma-duration-fast) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-spark-dur, var(--ma-duration-slow)));
}
/* hidden until its animation starts, so nothing shows while the line is still drawing */
:where(.ma-spark__ring) {
fill: currentColor;
opacity: 0;
transform-box: fill-box;
transform-origin: 50% 50%;
animation: ma-spark-pulse var(--ma-dur-ambient) var(--ma-ease-out) calc(var(--ma-r-delay, 0ms) + var(--ma-spark-dur, var(--ma-duration-slow))) infinite;
}
/* Under the JS gate the line starts undrawn; Reveal's .ma-in lets it draw, then the dot lands and pulses. */
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__line) {
clip-path: inset(-1em calc(100% + 1em) -1em -1em);
transition: none;
}
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__area),
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__dot) {
opacity: 0;
transition: none;
}
:where([data-ma-js] .ma-spark:not(.ma-in) .ma-spark__ring) {
animation: none;
opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-spark__line),
:where(.ma-spark__area),
:where(.ma-spark__dot) {
transition: none;
}
:where(.ma-spark__ring) {
animation: none;
opacity: 0;
}
}
/* Print: drawn, with its area and its last point. A chart printed before it scrolled into view was empty. */
@media print {
:where(.ma-spark .ma-spark__line) {
clip-path: none;
transition: none;
}
:where(.ma-spark .ma-spark__area) {
opacity: 0.12;
transition: none;
}
:where(.ma-spark .ma-spark__dot) {
opacity: 1;
transition: none;
}
:where(.ma-spark .ma-spark__ring) {
animation: none;
opacity: 0;
}
}
}
@keyframes ma-spark-pulse {
from {
scale: 1;
opacity: 0.5;
}
to {
scale: 3;
opacity: 0;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
data | number[] | none | The series, in order. |
as | HTMLTag | 'span' | Element to render. |
width | number | 120 | Width in px, also the viewBox; the drawing scales with it and shrinks to fit a narrower container. |
height | number | 32 | See width. |
color | string | 'var(--chart-1, currentColor)' | Line colour; the area and the dot follow it. The first chart colour of the theme contract when a theme defines one, else the text colour. |
fill | boolean | false | Tint the area under the line once it is drawn. |
dot | boolean | true | Mark the last point and pulse it. |
duration | number | preset (700) | Draw duration in ms. Unset, the preset's --ma-duration-slow. |
label | string | none | Accessible name; "n points, min, max, last" is appended. |
delay | number | 0 | Delay before drawing. |
once | boolean | true | Play once and stay. |
threshold | number | 0.2 | Fraction visible to trigger. |
The line is drawn at once and the last point does not pulse.
ClientRouterInherits Reveal: rebinds after every navigation.
Replaces: Sparkline (Preline charts) · react-sparklines · ApexCharts sparklines. See the migration table.
A bar chart whose bars grow from the baseline one after another when it scrolls into view; hovering a bar shows its value.
Counts a number up from zero to its value when it scrolls into view, with thousands separators, decimals, prefix and suffix.
A bar that fills to its value when it scrolls into view, with the number counting up beside it, or a ring with the number in the middle.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown