Without Chart.js and Without D3.js
Chart.js adds 200 KB to your JavaScript bundle. D3.js is a learning curve of its own. For most dashboard widgets in Hyva themes, Alpine.js and native SVG are enough, complete with tooltips, animations, and full accessibility.
Table of Contents
- 1. Why Alpine.js Instead of Chart.js for Simple Charts
- 2. Understanding the SVG Coordinate System and Viewbox
- 3. Bar Chart with Alpine.js and SVG Rect
- 4. Line Chart with SVG Polyline and Bezier Curves
- 5. Donut Chart with SVG Stroke Dashoffset
- 6. Tooltips with Alpine and SVG Foreignobject
- 7. CSS Animations for Chart Reveal
- 8. Accessibility: ARIA Table as a Chart Alternative
- 9. Alpine SVG Charts vs. Chart.js Compared
- 10. Summary
- 11. FAQ
1. Why Alpine.js Instead of Chart.js for Simple Charts
Chart.js is an excellent library for applications that need complex, interactive visualizations. For a dashboard widget with five bars or a donut chart showing a product's stock level, however, Chart.js is massively oversized. Its uncompressed size is around 200 KB, the rendering process uses Canvas instead of SVG (which makes scaling on high resolution displays harder), and accessibility is a known problem: Canvas content is invisible to screen readers without additional aria descriptions.
SVG, on the other hand, is native to the browser, vector based, scalable to any size without loss of sharpness, and fully part of the DOM, meaning Alpine.js can interact directly with SVG elements just like with any other HTML element. A rect element with :height="calculateHeight(value)" is a valid Alpine binding. A polyline with :points="dataToPoints(data)" works the same way. No Canvas API, no separate render cycle: Alpine's reactive system takes care of re-rendering automatically when the data changes. For any chart that does not need a specific Canvas feature, Alpine.js with SVG is the superior solution on Hyva sites.
2. Understanding the SVG Coordinate System and Viewbox
The SVG coordinate system starts at (0,0) in the top left corner and grows to the right (x) and downward (y). That is the most common stumbling block when drawing charts: bars grow from bottom to top in visual space, but in the SVG coordinate system they must be drawn upward from a fixed Y value. A bar with a value of 80% at a total height of 200px has an SVG height of 160px (200 * 0.8), but its Y starting point is at 40px (200 - 160). The formula: y = viewboxHeight - (value / maxValue) * viewboxHeight.
The viewBox attribute (viewBox="0 0 600 300") defines the internal coordinate system independently of the actual display size. The SVG element itself can have width="100%" and adapt responsively to its container; the viewBox automatically scales the content along with it. For calculations in Alpine.js you always work with viewBox coordinates, never with the container's actual pixels. That keeps the logic independent of the actual screen size.
// Alpine.js bar chart: SVG coordinate calculations
function barChart(data) {
const W = 600, H = 300, PADDING = 40;
const chartH = H - PADDING * 2;
const chartW = W - PADDING * 2;
return {
data,
hoveredIndex: -1,
get maxValue() {
return Math.max(...this.data.map(d => d.value));
},
// Calculate bar dimensions in SVG coordinates
barRect(item, index) {
const barW = (chartW / this.data.length) * 0.6;
const gap = chartW / this.data.length;
const barH = (item.value / this.maxValue) * chartH;
return {
x: PADDING + index * gap + (gap - barW) / 2,
y: PADDING + chartH - barH,
width: barW,
height: barH
};
},
// Y-axis labels (5 ticks)
get yTicks() {
return Array.from({ length: 6 }, (_, i) => {
const val = (this.maxValue / 5) * i;
const y = PADDING + chartH - (val / this.maxValue) * chartH;
return { val: Math.round(val), y };
});
}
};
}
3. Bar Chart with Alpine.js and SVG Rect
An SVG bar chart is made up of a few basic elements: <rect> elements for the bars, <text> elements for labels, and <line> elements for grid lines. Alpine.js iterates over the data points with x-for and calculates the SVG coordinates for each data point in real time. When the data changes, for example because the user changes a date range filter, Alpine updates all rect attributes automatically.
Hover highlight behavior is straightforward to implement in Alpine: @mouseenter="hoveredIndex = index" and @mouseleave="hoveredIndex = -1" control the bar's color via :fill="hoveredIndex === index ? '#0d9488' : '#14b8a6'". Important for accessibility: every <rect> element gets tabindex="0", role="graphics-symbol", and aria-label="Category: 450 euros". This lets keyboard users navigate through the bars and hear the values via screen reader.
4. Line Chart with SVG Polyline and Bezier Curves
For a line chart, Alpine.js calculates an SVG coordinate list from the data points and sets it as the points attribute of a <polyline>. The conversion: x = PADDING + (index / (data.length - 1)) * chartWidth, y = PADDING + chartHeight - (value / maxValue) * chartHeight. The result is a jagged line connecting the data points. For a smoothed curve, use <path> with bezier curves: the control points are calculated as one third of the distance between adjacent points.
The area below the line (area chart) is drawn with a <path> using the same coordinates plus a return path along the X axis, filled with a linear SVG gradient from teal to transparent. The <linearGradient> element sits inside a <defs> block within the SVG. Alpine only binds the dynamic attributes; the gradient itself is static SVG. This separation matters: static SVG structure goes in the HTML markup, dynamic values come in as Alpine bindings.
// Smooth bezier curve path for line chart
function pointsToPath(points) {
if (points.length < 2) return '';
let d = `M ${points[0].x} ${points[0].y}`;
for (let i = 0; i < points.length - 1; i++) {
const curr = points[i];
const next = points[i + 1];
const cpX = (curr.x + next.x) / 2;
// Cubic bezier: control points at horizontal midpoint
d += ` C ${cpX} ${curr.y}, ${cpX} ${next.y}, ${next.x} ${next.y}`;
}
return d;
}
// Area path: line + return along x-axis
function pointsToAreaPath(points, baseY) {
const line = pointsToPath(points);
const last = points[points.length - 1];
const first = points[0];
return `${line} L ${last.x} ${baseY} L ${first.x} ${baseY} Z`;
}
5. Donut Chart with SVG Stroke Dashoffset
A donut chart consists of a <circle> element whose circumference is displayed as arcs using stroke-dasharray and stroke-dashoffset. The trick: stroke-dasharray is set to the circle's circumference (2 * π * r), and stroke-dashoffset determines how much of the circle is visible. An offset of 0 shows 100%, an offset equal to the circumference shows 0%. For multiple segments, each circle is rotated by the cumulative offset of the previous segments via transform="rotate(-90 cx cy)" plus an additional offset.
Alpine.js calculates the dasharray and dashoffset values for each segment in a getter: get segments() { ... }. When the data changes, for example because a filter is applied, Alpine updates the SVG attributes automatically. A CSS transition on stroke-dashoffset produces a smooth animation on data change without any JavaScript animation logic. The donut chart's center shows the total value or the percentage of the selected segment with <text>, driven by Alpine's reactive state.
6. Tooltips with Alpine and SVG Foreignobject
Tooltips in SVG charts can be implemented either as a native SVG <g> element with <rect> and <text> elements, or as HTML via <foreignObject>. The latter is more flexible because you can use regular HTML markup with Tailwind classes. The <foreignObject> element is positioned absolutely within the SVG and displays HTML content. Alpine binds the position via :x and :y to state that gets updated on @mouseenter of the bars or data points.
The tooltip position needs to account for the SVG viewport: near the edges the tooltip must flip to the other side of the cursor. Alpine calculates this with a simple check: if the data point's X value is greater than half the viewBox width, the tooltip appears on the left instead of the right. The HTML variant via <foreignObject> also allows native Tailwind classes for shadows, border radius, and text formatting, all of which would be considerably more effort with native SVG rendering.
7. CSS Animations for Chart Reveal
Bars that grow from 0 to their final height when the page loads make dashboards visually more appealing and draw the user's eye to the important values. In SVG this can be achieved with CSS animations on transform: scaleY() combined with transform-origin: bottom center, but SVG and HTML have different transform origins, which leads to browser inconsistencies. The more robust method: bars start with height="0" and are animated to their final height via a CSS transition. Alpine sets the initial values on mount and the final values after a requestAnimationFrame delay, which ensures the CSS transition actually takes effect.
For line charts, the reveal animation is more elegant with the stroke-dashoffset trick: the line is drawn as if someone were tracing it by hand. To do this, set stroke-dasharray to the path's total length (found with path.getTotalLength()) and animate stroke-dashoffset from that total length down to 0. This produces a smooth drawing effect that uses only CSS and requires no JavaScript animation loop.
// Animate line chart on mount using stroke-dashoffset trick
animateLine() {
this.$nextTick(() => {
const path = this.$refs.linePath;
if (!path) return;
const length = path.getTotalLength();
// Set initial state: invisible line
path.style.strokeDasharray = length;
path.style.strokeDashoffset = length;
path.style.transition = 'none';
// Force browser to apply initial state before animating
path.getBoundingClientRect();
// Start animation on next frame
requestAnimationFrame(() => {
path.style.transition = 'stroke-dashoffset 1.2s cubic-bezier(0.4, 0, 0.2, 1)';
path.style.strokeDashoffset = '0';
});
});
}
8. Accessibility: ARIA Table as a Chart Alternative
An SVG chart is only accessible to screen reader users if the data it visualizes is also available as text. The best solution is a visually hidden data table containing the same data the chart visualizes. This table is removed from the visual flow with class="sr-only" but remains in the DOM and in the accessibility tree. The SVG element itself gets role="img" and aria-labelledby, pointing to a heading outside the SVG that describes the chart.
For interactive charts, where hover tooltips display values, those values also need to be accessible to keyboard users. Every interactive data point (a circle on the line, a bar) gets tabindex="0" and aria-describedby, pointing to a hidden element with the full value. @focus handlers show the same tooltip as @mouseenter, so keyboard users get the same information as mouse users. This is not an optional extra: without this implementation the chart fails WCAG 2.1 success criterion 1.1.1 (non-text content).
9. Alpine SVG Charts vs. Chart.js Compared
Chart.js and D3.js are the industry standards for complex data visualizations. For simple dashboard widgets on Hyva sites, however, that overhead is not justified. The comparison below shows where Alpine.js with SVG is the better choice and where Chart.js still makes sense.
| Criterion | Chart.js / D3.js | Alpine.js + SVG | Alpine's Advantage |
|---|---|---|---|
| Bundle size | 200-500 KB | 0 KB extra | Alpine.js already loaded |
| Scalability | Canvas (pixel dependent) | SVG (vector based) | Sharp on retina displays |
| Accessibility | Canvas: blind to screen readers | SVG: fully in the DOM | ARIA directly on SVG elements |
| Tailwind styling | Only via config object | Native SVG attributes | Direct Tailwind color system |
| Complex charts | Complete (scatter, bubble...) | Bars, lines, donut | Chart.js for niche cases |
The line between Alpine.js SVG and Chart.js comes down to complexity: scatter plots, bubble charts, statistical visualizations with axis scaling, logarithms, or regression belong in Chart.js or D3.js. Bars, lines, donuts, and simple sparklines belong in Alpine.js with SVG. For Hyva shops, where dashboard widgets display revenue, orders, and conversion rate, Alpine.js is sufficient in 95% of cases.
Mironsoft
Alpine.js Dashboard · Hyva Development · Data Visualization
Dashboard widgets without an external chart library?
We build lightweight data visualizations with Alpine.js and SVG for Hyva shops: faster loading, accessible, and fully integrated into your Tailwind design.
Chart Analysis
Review of existing chart libraries and a migration path to Alpine SVG
Widget Development
Bars, lines, donut with tooltips, animations, and ARIA accessibility
Performance
Chart.js removal, bundle reduction, and Core Web Vitals optimization
10. Summary
Visualizing data with Alpine.js and SVG is feasible for all common chart types and in many cases better than Chart.js: 0 KB of extra bundle weight, vector based rendering with no pixel problems on retina displays, ARIA attributes directly on SVG elements, and full control over appearance with Tailwind colors. Calculating SVG coordinates in Alpine getters is the central pattern: once you understand it, you can implement any chart type with it.
The most important principle: accessibility is not an afterthought. A hidden sr-only table with the same data, ARIA labels on interactive elements, and keyboard navigation through data points make the difference between a chart that only communicates visually and one that includes all users. Alpine.js makes this easier than any Canvas based library, because SVG lives entirely in the DOM and ARIA attributes can be bound directly.
Alpine.js SVG Charts: The Key Points at a Glance
SVG Coordinates
Y axis grows downward. Bar Y: viewboxHeight - (value/max)*chartH. ViewBox defines the coordinate system independently of the display size.
Donut Chart
stroke-dasharray = 2*π*r. stroke-dashoffset controls the visible share. Segments are positioned by rotating cumulative offsets.
Animation
Lines with stroke-dashoffset from total length to 0. Bars with a CSS transition on height. No JavaScript animation loop needed.
Accessibility
sr-only data table, role="img" on the SVG, aria-label on interactive elements, tabindex="0" for keyboard navigation through data points.