What are CSS charts and which chart types can CSS draw?
Nothing here needs a charting library. Bars, slices and rings all come from properties like height, grid placement and gradients, written in CSS. No script has to download, parse or run before the shapes show up.
Which shapes come easily depends on what CSS already does to a box. Resizing it and filling it with a gradient get you most of the way.
- Bar and column charts use a grid or flex container with one child per value, sized by percentage width or height.
- A stacked bar puts several segments in one row, each sized as a share of the total.
- One element with a conic-gradient() and hard color stops makes a pie.
- Cut the center out with a mask or an overlaid radial-gradient() and the pie becomes a donut.
- A progress ring is just one colored stop on a conic-gradient(), with the remainder transparent.
Line and area charts are the weak spot. They need diagonal edges and data points, so the shapes come from clip-path polygons that are fiddly to maintain, or from a framework such as Charts.css, whose site lists both chart types.
A script stays optional. One tutorial on Hashbangcode adds JavaScript mainly to build the gradient string from a data array (its accessible version also generates labelled slice elements), and the drawing itself is still CSS.
Single values don’t always need tricks either. The progress element handles task completion, and the meter element handles a value inside a known range, like a gauge reading. Both carry built-in meaning for assistive technology.
How does a CSS chart turn data into shapes?
Each value sits in a custom property, and calc() converts it into a size. A bar gets a percentage height, a slice gets a percentage color stop. The browser draws whatever the number resolves to, so the value in the markup is the only input the chart needs.
Where to store the value
The quickest route is an inline style, something like style=”–value: 72″ on each bar. It works anywhere custom properties work.
A data attribute (data-value=”72″) reads more cleanly in the markup, but CSS can only turn it into a size through typed attr(). Then there’s the table cell, where the number is visible text in a td and the size comes from a custom property set on that same cell.
Keeping the number in the HTML also keeps it readable when the stylesheet fails to load. That is why Charts.css and Mads Stoumann’s DEV Community walkthrough both build charts on a table.
Typed attr() is the catch. Can I use lists it as shipped in Chrome and Edge from version 133 and in Firefox from version 155, while stable Safari releases do not have it yet (only Safari Technology Preview does).
For production, I’d stick with an inline custom property. It’s the safer carrier.
Converting the value into a size
CSS does no scaling on its own. Charts.css expects a –size between 0 and 1, written as calc(40 / 100) for a value of 40 against a maximum of 100 (Charts.css docs and README).
The author divides by the maximum, and that division is the chart’s axis.
/* bar: a value from 0 to 100 becomes a height */
.bar { height: calc(var(--value) * 1%); }
/* ring: the same value becomes a gradient stop */
.ring { background: conic-gradient(var(--c) calc(var(--p) * 1%), #0000 0); }
The ring rule follows the pattern in freeCodeCamp’s single-element pie tutorial, where one custom property, –p, drives the fill.
How to build a bar chart with CSS grid
A bar chart is a grid with one child per value and a height tied to a custom property. Here’s the build, step by step.
- Add one element per value inside a wrapper and store each number in a –value custom property, scaled to a 0 to 100 range.
- Set the wrapper to display: grid with grid-auto-flow: column and grid-auto-columns: 1fr, so every bar gets an equal track.
- Add align-items: end so bars grow upward from a shared baseline.
- Give the wrapper a fixed height. Percentage heights resolve against it.
- Set each bar’s height to calc(var(–value) * 1%), then add gap, border-radius and labels.
<div class="chart">
<div class="bar" style="--value: 72"></div>
<div class="bar" style="--value: 45"></div>
</div>
.chart { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
align-items: end; gap: 8px; height: 240px; }
.bar { height: calc(var(--value) * 1%); border-radius: 4px 4px 0 0; }
Skip step 4 and the bars vanish. Without a height on the parent, a percentage height falls back to auto, and an empty element has no content to size it.
Flexbox works too, with align-items: flex-end. Grid is the cleaner pick for most bar charts because gridlines and bars can share the same tracks.
Horizontal and stacked bars
For horizontal bars, swap height for width and let the rows stack with grid-auto-flow: row.
Stacked bars go in a single flex row, with each segment’s flex-basis set to calc(var(–value) * 1%). The segment values have to add up to 100, otherwise the bar won’t fill its track.
How to draw pie, donut and progress ring charts with conic-gradient
All of these start from the same gradient. A pie is one element with one conic-gradient() whose color stops meet at hard edges, a donut adds a hole, and a ring keeps just a single colored stop. A 20, 45 and 35 percent split takes three stops, the example Keith Clark uses in his single-element pie tutorial.
Pie charts
.pie {
width: 200px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(#e63946 0 20%, #457b9d 20% 65%, #2a9d8f 65% 100%);
}
Each stop is a color plus a start and an end position, and the gradient begins at 12 o’clock. The border-radius turns the square into a circle.
According to MDN Web Docs (2026), conic-gradient() has worked across browsers since November 2020.
MDN adds a warning. A gradient is a CSS image, and CSS images have no way to carry alternative text, so the data behind the pie must also exist as text in the document.
Segment count is the practical limit. Code Boxx calls the approach painful once a pie has many slices, and Keith Clark’s custom-property version needs enough stops written in advance to cover the largest pie it will ever show. So two independent tutorials land on the same ceiling.
Donut charts
The hole gets cut after the pie is drawn. You can put a radial-gradient() mask on the pie element, the method in both Mads Stoumann’s walkthrough and freeCodeCamp’s tutorial, or layer a radial-gradient() in the background color on top and size it to the ring thickness.
.donut { mask: radial-gradient(farthest-side, #0000 calc(100% - 36px), #000 0); }
A mask generator can write the mask-image value if the hole needs an unusual shape. One catch: a mask clips everything inside the element, labels included, so the center text belongs in a sibling element.
Progress rings
A ring is a pie with two stops. The first covers the value, the second is transparent.
The freeCodeCamp version feeds the value through calc(var(–p) * 1%) and adds pseudo-elements for rounded ends. Square ends need nothing extra.
How to animate CSS charts and keep them responsive
Bars animate with a plain transition or keyframe on height or width. Pies and rings need an @property registration first, so the browser can interpolate the custom property. For sizing, percentages, grid tracks and the container do the work instead of fixed pixels.
Animating with transition and @property
A transition only fires when a value changes, so a bar needs a starting state. The simpler route for bars is a CSS keyframes rule that begins each one at zero height and runs once on load, with no script involved.
Pies and rings behave differently. An unregistered custom property is read as a plain token stream, so the browser swaps it instantly instead of sweeping between values. Registering it with @property and a typed syntax such as a percentage fixes that.
MDN’s own example animates a registered –progress percentage inside a linear-gradient() to fill a progress bar over 2.5 seconds.
@property reached Baseline newly available status in July 2024, according to MDN Web Docs (2026). Older browsers skip the registration and jump between the two values instead of sweeping.
Wrap the animation in a prefers-reduced-motion: no-preference query, so people who ask for less movement get the finished chart immediately.
Making charts fit any container
Percentage widths and heights get you most of the way. Grid tracks in fr units let bars share space evenly, and circular charts need a fixed aspect ratio.
Flavio Copes points out that aspect-ratio: 1 plus a width is all a conic-gradient pie needs to stay circular.
Mads Stoumann’s DEV Community demos control responsiveness through the size of the chart’s container, wrapped in a custom element. The same CSS then shows fewer columns as the container shrinks (his demo uses small and extra-small tiers), without page-level breakpoints.
Container sizing leaves media queries for the cases that depend on the viewport, such as hiding secondary axis labels on a narrow screen.
How to make a CSS chart accessible
Keep the data as real text and give every bar or slice at least 3:1 contrast against its neighbors. The chart also needs a name for assistive technology. A chart built from empty divs exposes nothing to a screen reader, so the text equivalent comes first.
Choose the text equivalent
When the chart is built on a table, the table is the text equivalent. A caption, header cells with scope and the values inside the td elements give a screen reader everything the picture shows.
Skip role=”img” on that container. The role hides the children from assistive technology, which throws the table away.
A div-based chart is a different story. The shapes carry no text, so the container gets role=”img” and an ARIA label that states the finding, with the values printed nearby in a table or list.
<figure>
<div class="pie" role="img"
aria-label="Sales by region: North 20%, South 45%, West 35%"></div>
<figcaption>Sales by region</figcaption>
</figure>
“Pie chart” tells a listener nothing. The label should carry the numbers.
Meet the contrast threshold
WCAG 2.2 success criterion 1.4.11 sets a contrast ratio of at least 3:1 for graphical objects against adjacent colors (W3C).
W3C’s own failing example pairs a yellow fill (#FFF000) with white at 1.2:1.
The rule has an exit. Cornell’s web accessibility review guidance exempts pie slices when percentages or other supplemental information sit alongside them, so a value printed on each slice removes the requirement.
Without printed values, test every slice against the slice next to it, not only against the page background. An analyzer for color contrast does this in seconds.
Label bars and slices directly, and keep the legend order the same as the visual order. Varying pattern as well as hue helps too, for example with repeating-linear-gradient() fills.
Charts.css or hand-written CSS: which one fits?
Use Charts.css when a page needs several table-driven charts of the standard types, and write the CSS by hand for one or two charts or for any shape the framework does not ship. The framework pays off through repetition, not through any single chart.
What the framework adds
The license is MIT and there are no dependencies. Markup is a table with the charts-css class plus a type class such as column or bar.
Modifier classes handle axes, data spacing and direction, for example show-primary-axis, data-spacing-4 and reverse-data. Variants cover multi-dataset charts, percentage columns, stacked columns, pie and donut charts, and 3D effects (chartscss.org). A legend is just a list carrying the charts-css and legend classes (InfoQ, 2021).
Because the data sits in a real table, the framework inherits what accessible tables already provide: a caption, header cells and values that stay readable without the stylesheet.
Circular charts are covered too. The chartscss.org home page lists pie and donut charts, and its Pie page documents a table-based pie that takes –start and –end values in place of –size.
Size and upkeep, where the sources disagree
File size is reported two ways. The chartscss.org home page says 76kb with 7kb gzipped, and the GitHub README says 58.4kb minified and 5.9kb gzipped.
Neither page says whether the gap comes from release versions or from how the files were measured. The minified file of version 1.2.0, the current release from July 2025, weighs about 60kb and just under 6kb gzipped, so the README is the closer of the two.
The repository shows 272 commits, 30 open issues and 6 open pull requests (GitHub, 2026). Read the issue tracker for the chart type you need before committing to it.
The project docs make one performance claim. With CSS there is no rendering step, which gives an edge over JavaScript solutions.
Pros and cons
On the plus side, there are no dependencies and no script, the numbers stay readable as table text, and one class vocabulary keeps many charts consistent. The MIT license is a bonus.
The downsides are real, though. Only the standard chart types are covered, and the docs send complex charts with special behaviors to JavaScript tools. One td per data point makes the markup long for big series. And any look beyond the defaults still means writing your own custom properties and selectors.
For one or two charts, the hand-written patterns above are lighter than learning a class vocabulary. Once the same chart type repeats across many pages, the framework saves the repeated work.
How do CSS charts compare with SVG, canvas and JavaScript chart libraries?
CSS charts fit a handful of static charts in standard shapes. SVG is better for lines and curves. Large datasets belong on canvas, and anything users need to interact with usually ends up in a JavaScript library such as Chart.js, D3.js or Highcharts.
| Option | Drawn with | Screen reader text | Best fit |
|---|
| CSS charts | HTML elements and CSS | Built in when based on a table | A few static, standard charts |
| SVG | Vector shapes in the DOM | Added with titles and labels | Lines, curves, custom shapes |
| Canvas (Chart.js) | Pixels on a canvas element | None until ARIA or fallback content is added | Large datasets, heavy animation |
| Highcharts | Script-generated SVG | Varies, test with a screen reader | Many chart types with vendor support |
The Chart.js accessibility documentation states that canvas content is not accessible to screen readers, so the author adds ARIA attributes or fallback content.
Put that next to the earlier point about gradient backgrounds and a pattern appears. Canvas charts and gradient-drawn charts share the same flaw, because the drawing carries no text. Table-based CSS charts are the exception.
For a line chart, SVG is the better tool. A polyline takes a list of points, where CSS needs clip-path polygons.
Cost differs too. A CSS chart costs nothing, while Highcharts is free only for personal, non-commercial and educational use, and commercial use needs a paid license (Highcharts license terms).
The less obvious pick is often the right one. A page with three static charts does not need a charting library, because the download and the script execution buy nothing the CSS version lacks.
Where do CSS charts stop working?
CSS cannot find the largest value among sibling elements, so the maximum is written by hand or by a build step. That’s the first wall.
Dataset size is the next one. Older Lighthouse versions warned when the body held more than about 800 nodes and errored above about 1,400. Since Lighthouse 13 the check is an Optimize DOM size insight that flags a large DOM when style and layout work gets slow (Chrome for Developers). A 365-day column chart with a bar and a label per day adds 730 nodes before any wrapper element.
Interaction is limited as well. Hover states and basic tooltips work in CSS, but zoom, brushing and selection do not.
The last limit is any shape that depends on neighboring values, which the next section covers.
Line charts, area charts and running totals
A line segment needs the value of the point before it. CSS cannot read a sibling’s value, so the author writes the previous value into each point, or a script does.
Pie slices hit the same wall. In his DEV Community walkthrough, Mads Stoumann says he could not get CSS counters to produce the accumulated value and total behind his pie chart.
When two or more of these limits apply to the same chart, move it to SVG or a JavaScript library instead of stretching the CSS.
CSS Charts FAQ
Can CSS charts show tooltips on hover?
Yes, for simple labels. A :hover or :focus rule can reveal text from a data attribute through content: attr(data-label) on a pseudo-element. Add tabindex so keyboard users get the same label. Tooltips that follow the cursor or stay pinned need JavaScript.
How do you add a legend to a CSS chart?
Place a list beside the chart and give each item a colored marker that matches its bar or slice. Charts.css ships a legend list with the charts-css and legend classes. The legend also gives low-vision readers a text label for every color.
Can CSS charts display live data?
Yes, with a small script. CSS cannot fetch data, but JavaScript can write each new number into a custom property and the chart redraws itself. Keith Clark’s pie tutorial updates charts by calling setProperty() on the element’s style object.
Do CSS charts work in React, Vue and other frameworks?
Yes. A CSS chart is plain markup plus style rules, so any framework that renders HTML can output it. Charts.css is a stylesheet applied to table elements, which means it needs no wrapper component or special binding.
Which CSS Chart to Build First
Start with a bar chart. It takes one grid and one custom property per value, with no gradient arithmetic. Pie and ring charts come second, once the data binding is proven.
On real data, the order that holds up is a bar chart with a table or text equivalent, then a screen reader and contrast check. Pie or ring charts follow, and animation goes last.
Accessibility sits before animation because a failed contrast ratio forces new colors across every bar, while animation changes no text.
Test the first chart with the largest and smallest real values, not sample numbers, and with a screen reader running.
One dated condition would change the data-binding advice. As of October 2026, Chrome, Edge and Firefox 155+ support typed attr(), but stable Safari still lacks it. Once Safari ships it, a data attribute can replace the inline custom property.
When one chart passes both checks, several of them fit together in a CSS dashboard.