惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
L
LINUX DO - 热门话题
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
爱范儿
爱范儿
D
DataBreaches.Net
Simon Willison's Weblog
Simon Willison's Weblog
S
Secure Thoughts
S
SegmentFault 最新的问题
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
P
Proofpoint News Feed
The Hacker News
The Hacker News
T
ThreatConnect
N
News and Events Feed by Topic
T
Threatpost
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园 - Franky
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
F
Future of Privacy Forum
F
Full Disclosure
Cyberwarzone
Cyberwarzone
J
Java Code Geeks
李成银的技术随笔
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
H
Hacker News: Front Page
人人都是产品经理
人人都是产品经理
博客园_首页
Scott Helme
Scott Helme
Google DeepMind News
Google DeepMind News
美团技术团队
Malwarebytes
Malwarebytes
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
T
The Exploit Database - CXSecurity.com
G
GRAHAM CLULEY
Recent Announcements
Recent Announcements
C
CXSECURITY Database RSS Feed - CXSecurity.com

CSS-Tricks

Revealing Text With CSS letter-spacing | CSS-Tricks Technical Writing in the AI Age | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks Cross-Document View Transitions: Scaling Across Hundreds of Elements | CSS-Tricks The State of CSS Centering in 2026 | CSS-Tricks Stack Overflow: When We Stop Asking | CSS-Tricks Cross-Document View Transitions: The Gotchas Nobody Mentions | CSS-Tricks What’s !important #11: 3D Voxel Scenes, Flying Focus, CSS Syntaxes, and More | CSS-Tricks Computing and Displaying Discounted Prices in CSS | CSS-Tricks rotateX() | CSS-Tricks rotateY() | CSS-Tricks rotateZ() | CSS-Tricks rotate() | CSS-Tricks Soon We Can Finally Banish JavaScript to the ShadowRealm | CSS-Tricks Using CSS corner-shape For Folded Corners | CSS-Tricks A Scrollytelling Gift for Mum on Mother’s Day 2026 | CSS-Tricks Google’s Prompt API | CSS-Tricks Making Zigzag CSS Layouts With a Grid + Transform Trick | CSS-Tricks Fixed-Height Cards: More Fragile Than They Look | CSS-Tricks What’s !important #10: HTML-in-Canvas, Hex Maps, E-ink Optimization, and More | CSS-Tricks The Importance of Native Randomness in CSS | CSS-Tricks contrast() | CSS-Tricks contrast-color() | CSS-Tricks Let’s Use the Nonexistent ::nth-letter Selector Now | CSS-Tricks Quick Hit #126 Recreating Apple’s Vision Pro Animation in CSS | CSS-Tricks Quick Hit #125 Enhancing Astro With a Markdown Component | CSS-Tricks Quick Hit #124 Markdown + Astro = ❤️ | CSS-Tricks Quick Hit #123 What’s !important #9: clip-path Jigsaws, View Transitions Toolkit, Name-only Containers, and More | CSS-Tricks A Well-Designed JavaScript Module System is Your First Architecture Decision | CSS-Tricks hypot() | CSS-Tricks The Radio State Machine | CSS-Tricks 7 View Transitions Recipes to Try | CSS-Tricks Quick Hit #122 Quick Hit #121 Selecting a Date Range in CSS | CSS-Tricks saturate() | CSS-Tricks justify-self | CSS-Tricks Quick Hit #120 Alternatives to the !important Keyword | CSS-Tricks Quick Hit #119 New CSS Multi-Column Layout Features in Chrome | CSS-Tricks Quick Hit #118 Making Complex CSS Shapes Using shape() | CSS-Tricks Quick Hit #117 Front-End Fools: Top 10 April Fools’ UI Pranks of All Time | CSS-Tricks Sniffing Out the CSS Olfactive API | CSS-Tricks What’s !important #8: Light/Dark Favicons, @mixin, object-view-box, and More | CSS-Tricks Quick Hit #116 Form Automation Tips for Happier User and Clients | CSS-Tricks Quick Hit #115 Generative UI Notes | CSS-Tricks Quick Hit #114 Quick Hit #113 Experimenting With Scroll-Driven corner-shape Animations | CSS-Tricks Quick Hit #112 JavaScript for Everyone: Destructuring | CSS-Tricks Quick Hit #111 Quick Hit #110 What’s !important #7: random(), Folded Corners, Anchored Container Queries, and More | CSS-Tricks 4 Reasons That Make Tailwind Great for Building Layouts | CSS-Tricks Quick Hit #109 Quick Hit #108 Abusing Customizable Selects | CSS-Tricks Quick Hit #107 The Value of z-index | CSS-Tricks Quick Hit #106 The Different Ways to Select <html> in CSS Quick Hit #105 Popover API or Dialog API: Which to Choose? Quick Hit #104 What’s !important #6: :heading, border-shape, Truncating Text From the Middle, and More Yet Another Way to Center an (Absolute) Element An Exploit ... in CSS?! Quick Hit #103 A Complete Guide to Bookmarklets Quick Hit #102 Loading Smarter: SVG vs. Raster Loaders in Modern Web Design Potentially Coming to a Browser :near() You Quick Hit #101 Distinguishing "Components" and "Utilities" in Tailwind Quick Hit #100 Spiral Scrollytelling in CSS With sibling-index() Interop 2026 Quick Hit #99 What’s !important #5: Lazy-loading iframes, Repeating corner-shape Backgrounds, and More Quick Hit #98 Making a Responsive Pyramidal Grid With Modern CSS Approximating contrast-color() With Other CSS Features Quick Hit #97 Trying to Make the Perfect Pie Chart in CSS Quick Hit #96 Quick Hit #95 CSS Bar Charts Using Modern Functions Quick Hit #94 No Hassle Visual Code Theming: Publishing an Extension Quick Hit #93
How to Make a Line Chart With CSS
CSS-Tricks · 2020-03-11 · via CSS-Tricks

Line,  bar, and pie charts are the bread and butter of dashboards and are the basic components of any data visualization toolkit. Sure, you can use SVG or a JavaScript chart library like Chart.js or a complex tool like D3 to create those charts, but what if you don’t want to load yet another library into your already performance-challenged website?

There are plenty of articles out there for creating CSS-only bar charts, column charts, and pie charts, but if you just want a basic line chart, you’re out of luck. While CSS can “draw lines” with borders and the like, there is no clear method for drawing a line from one point to another on an X and Y coordinate plane. 

Well, there is a way! If all you need is a simple line chart, there’s no need to load in a huge JavaScript library or even reach for SVG. You can make everything you need with just CSS and a couple of custom properties in your HTML. Word of warning, though. It does involve a bit of trigonometry. If that didn’t scare you off, then roll up your shirt sleeves, and let’s get started!

Here’s a peek at where we’re headed:

Let’s start with the baseline

If you are creating a line chart by hand (as in, literally drawing lines on a piece of graph paper), you would start by creating the points, then connecting those points to make the lines. If you break the process down like that, you can recreate any basic line chart in CSS. 

Let’s say we have an array of data to display points on an X and Y coordinate system, where days of the week fall along the X-axis and the numeric values represent points on the Y-axis.

[
  { value: 25, dimension: "Monday" },
  { value: 60, dimension: "Tuesday" },
  { value: 45, dimension: "Wednesday" },
  { value: 50, dimension: "Thursday" },
  { value: 40, dimension: "Friday" }
]

Let’s create an unordered list to hold our data points and apply some styles to it. Here’s our HTML:

<figure class="css-chart" style="--widget-size: 200px;">
  <ul class="line-chart">
    <li>
      <div class="data-point" data-value="25"></div>
    </li>
    <li>
      <div class="data-point" data-value="60"></div>
    </li>
    <li>
      <div class="data-point" data-value="45"></div>
    </li>
    <li>
      <div class="data-point" data-value="50"></div>
    </li>
    <li>
      <div class="data-point" data-value="40"></div>
    </li>
  </ul>
</figure>

A couple notes to glean here. First is that we’re wrapping everything in a <figure> element, which is a nice semantic HTML way of saying this is self-contained content, which also provides us the optional benefit of using a <figcaption>, should we need it. Secondly, notice that we’re storing the values in a data attribute we’re calling data-value that’s contained in its own div inside a list item in the unordered list. Why are we using a separate div instead of putting the class and attribute on the list items themselves? It’ll help us later when we get to drawing lines.

Lastly, note that we have an inlined custom property on the parent <figure>  element that we’re calling --widget-size. We’ll use that in the CSS, which is going to look like this:

/* The parent element */
.css-chart {
  /* The chart borders */
  border-bottom: 1px solid;
  border-left: 1px solid;
  /* The height, which is initially defined in the HTML */
  height: var(--widget-size);
  /* A little breathing room should there be others items around the chart */
  margin: 1em;
  /* Remove any padding so we have as much space to work with inside the element */
  padding: 0;
  position: relative;
  /* The chart width, as defined in the HTML */
  width: var(--widget-size);
}


/* The unordered list holding the data points, no list styling and no spacing */
.line-chart {
  list-style: none;
  margin: 0;
  padding: 0;
}


/* Each point on the chart, each a 12px circle with a light border */
.data-point {
  background-color: white;
  border: 2px solid lightblue;
  border-radius: 50%;
  height: 12px;
  position: absolute;
  width: 12px;
}

The above HTML and CSS will give us this not-so-exciting starting point:

Well, it sort of looks like a chart.

Rendering data points

That doesn’t look like much yet. We need a way to draw each data point at its respective X and Y coordinate on our soon-to-be chart. In our CSS, we’ve set the .data-point class to use absolute positioning and we set a fixed width and height on its parent .css-chart container with a custom property. We can use that to calculate our X and Y positions.

Our custom property sets the chart height at 200px and, in our values array, the largest value is 60. If we set that data point as the highest point on the chart’s Y axis at 200px, then we can use the ratio of any value in our data set to 60 and multiply that by 200 to get the Y coordinate of all of our points. So our largest value of 60 will have a Y value that can be calculated like this:

(60 / 60) * 200 = 200px 

And our smallest value of 25 will end up with a Y value calculated the same way:

(25 / 60) * 200 = 83.33333333333334px

Getting the Y coordinate for each data point is easier. If we are spacing the points equally across the graph, then we can divide the width of the chart (200px) by the number of values in our data array (5) to get 40px. That means the first value will have an X coordinate of 40px (to leave a margin for a left axis if we want one), and the last value will have an X coordinate of 200px.

You just got mathed! 🤓

For now, let’s add inline styles to each of the divs in the list items. Our new HTML becomes this, where the inline styles contain the calculated positioning for each point.

<figure class="css-chart">
  <ul class="line-chart">
    <li>
      <div class="data-point" data-value="25" style="bottom: 83.33333333333334px; left: 40px;"></div>
    </li>
    <li>
      <div class="data-point" data-value="60" style="bottom: 200px; left: 80px;"></div>
    </li>
    <li>
      <div class="data-point" data-value="45" style="bottom: 150px; left: 120px;"></div>
    </li>
    <li>
      <div class="data-point" data-value="50" style="bottom: 166.66666666666669pxpx; left: 160px;"></div>
    </li>
    <li>
      <div class="data-point" data-value="40" style="bottom: 133.33333333333331px; left: 200px;"></div>
    </li>
  </ul>
</figure>
We have a chart!

Hey, that looks a lot better! But even though you can see where this is going, you still can’t really call this a line graph. No problem. We only need to use a little more math to finish our game of connect-the-dots. Take a look at the picture of our rendered data points again. Can you see the triangles that connect them? If not, maybe this next picture will help:

Why is that important? Shhh, the answer’s coming up next.

Rendering line segments

See the triangles now? And they’re not just any old triangles. They’re the best kind of triangles (for our purposes anyway) because they are right triangles! When we calculated the Y coordinates of our data points earlier, we were also calculating the length of one leg of our right triangle (i.e. the “run” if you think of it as a stair step). If we calculate the difference in the X coordinate from one point to the next, that will tell us the length of another side of our right triangle (i.e. the “rise” of a stair step). And with those two pieces of information, we can calculate the length of the magical hypotenuse which, as it turns out, is exactly what we need to draw to the screen in order to connect our dots and make a real line chart.

For example, let’s take the second and third points on the chart.

<!-- ... --> 


<li>
  <div class="data-point" data-value="60" style="bottom: 200px; left: 80px;"></div>
</li>
<li>
  <div class="data-point" data-value="45" style="bottom: 150px; left: 120px;"></div>
</li>


<!-- ... -->

The second data point has a Y value of 200 and the third data point has a Y value of 150, so the opposite side of the triangle connecting them has a length of 200 minus 150, or 50. It has an adjacent side that is 40 pixels long (the amount of spacing we put between each of our points).

That means the length of the hypotenuse is the square root of 50 squared plus 40 squared, or 64.03124237432849.

The hypotenuse is what we need to draw our line graph

Let’s create another div inside of each list item in the chart that will serve as the hypotenuse of a triangle drawn from that point. Then we’ll set an inline custom property on our new div that contains the length of that hypotenuse.

<!-- ... -->


<li>
  <div class="data-point" data-value="60"></div>
  <div class="line-segment" style="--hypotenuse: 64.03124237432849;"></div>
</li>


<!-- ... -->

While we’re at it, our line segments are going to need to know their proper X and Y coordinates, so let’s remove the inline styles from our .data-point elements and add CSS custom properties to their parent (the <li> element) instead. Let’s call these properties, creatively, --x and --y. Our data points don’t need to know about the hypotenuse (the length of our line segment), so we can add a CSS custom property for the length of the hypotenuse directly to our .line-segment. So now our HTML will look like this:

<!-- ... -->


<li style="--y: 200px; --x: 80px">
  <div class="data-point" data-value="60"></div>
  <div class="line-segment" style="--hypotenuse: 64.03124237432849;"></div>
</li>


<!-- ... -->

We’ll need to update our CSS to position the data points with those new custom properties and style up the new .line-segment div we added to the markup:

.data-point {
  /* Same as before */


  bottom: var(--y);
  left: var(--x);
}


.line-segment {
  background-color: blue;
  bottom: var(--y);
  height: 3px;
  left: var(--x);
  position: absolute;
  width: calc(var(--hypotenuse) * 1px);
}
We have all of the parts now. They just don’t fit together correctly yet.

Well, we have line segments now but this isn’t at all what we want. To get a functional line chart, we need to apply a transformation. But first, let’s fix a couple of things.

First off, our line segments line up with the bottom of our data points, but we want the origin of the line segments to be the center of the data point circles. We can fix that with a quick CSS change to our .data-point styles. We need to adjust their X and Y position to account for both the size of the data point and its border as well as the width of the line segment.

.data-point {
  /* ... */


  /* The data points have a radius of 8px and the line segment has a width of 3px, 
    so we split the difference to center the data points on the line segment origins */
  bottom: calc(var(--y) - 6.5px);
  left: calc(var(--x) - 9.5px);
}

Secondly, our line segments are being rendered on top of the data points instead of behind them. We can address that by putting the line segment first in our HTML:

<!-- ... -->


<li style="--y: 200px; --x: 80px">
    <div class="line-segment" style="--hypotenuse: 64.03124237432849;"></div>
    <div class="data-point" data-value="60"></div>
</li>


<!-- ... -->

Applying transforms, FTW

We’ve almost got it now. We just need to do one last bit of math. Specifically, we need to find the measure of the angle that faces the opposite side of our right triangle and then rotate our line segment by that same number of degrees.

How do we do that? Trigonometry! You may recall the little mnemonic trick to remember how sine, cosine and tangent are calculated:

  • SOH (Sine = Opposite over Hypotenuse
  • CAH (Cosine = Adjacent over Hypotenuse)
  • TOA (Tangent = Opposite over Adjacent)

You can use any of them because we know the length of all three sides of our right triangle. I picked sine, so that that leaves us with this equation:

sin(x) = Opposite / Hypotenuse

The answer to that equation will tell us how to rotate each line segment to have it connect to the next data point. We can quickly do this in JavaScript using Math.asin(Opposite / Hypotenuse). It will give us the answer in radians though, so we’ll need to multiply the result by (180 / Math.PI).

Using the example of our second data point from earlier, we already worked out that the opposite side has a length of 50 and the hypotenuse has a length of 64.03124237432849, so we can re-write our equation like this:

sin(x) = 50 /  64.03124237432849 = 51.34019174590991

That’s the angle we’re looking for! We need to solve that equation for each of our data points and then pass the value as a CSS custom property on our .line-segment elements. That will give us HTML that looks like this:

<!-- ... -->


<li style="--y: 200px; --x: 80px">
  <div class="data-point" data-value="60"></div>
  <div class="line-segment" style="--hypotenuse: 64.03124237432849; --angle: 51.34019174590991;"></div>
</li>


<!-- ... -->

And here’s where we can apply those properties in the CSS:

.line-segment {
  /* ... */
  transform: rotate(calc(var(--angle) * 1deg));
  width: calc(var(--hypotenuse) * 1px);
}

Now when we render that, we have our line segments!

Well, the line segments are definitely rotated. We need one more step.

Wait, what? Our line segments are all over the place. What now? Oh, right. By default, transform: rotate() rotates around the center of the transformed element. We want the rotation to occur from the bottom-left corner to angle away from our current data point to the next one. That means we need to set one more CSS property on our .line-segment class.

.line-segment {
  /* ... */
  transform: rotate(calc(var(--angle) * 1deg));
  transform-origin: left bottom;
  width: calc(var(--hypotenuse) * 1px);
}

And, now when we render it, we finally get the CSS-only line graph we’ve been waiting for.

At last! A line graph!

Important note: When you calculate the value of the opposite side (the “rise”), make sure it’s calculated as the “Y position of the current data point” minus the “Y position of the next data point.” That will result in a negative value when the next data point is a larger value (higher up on the graph) than the current data point which will result in a negative rotation. That’s how we ensure the line slopes upwards.

When to use this kind of chart

This approach is great for a simple static site or for a dynamic site that uses server-side generated content. Of course, it can also be used on a site with client-side dynamically generated content, but then you are back to running JavaScript on the client. The CodePen at the top of this post shows an example of client-side dynamic generation of this line chart.

The CSS calc() function is highly useful, but it can’t calculate sine, cosine, and tangent for us. That means you’d have to either calculate your values by hand or write a quick function (client-side or server-side) to generate the needed values (X, Y, hypotenuse and angle) for our CSS custom properties.

I know some of you got through this and will feel like it’s not vanilla CSS if it requires a script to calculate the values — and that’s fair. The point is that all of the chart rendering is done in CSS. The data points and the lines that connect them are all done with HTML elements and CSS that works beautifully, even in a statically rendered environment with no JavaScript enabled. And perhaps more importantly, there’s no need to download yet another bloated library just to render a simple line graph on your page.

Potential improvements

As with anything, there’s always something we can do to take things to the next level. In this case, I think there are three areas where this approach could be improved.

Responsiveness

The approach I’ve outlined uses a fixed size for the chart dimensions, which is exactly what we don’t want in a responsive design. We can work around this limitation if we can run JavaScript on the client. Instead of hard-coding our chart size, we can set a CSS custom property (remember our --widget-size property?), base all of the calculations on it, and update that property when the container or window either initially displays or resizes using some form of a container query or a window resize listener.

Tooltips

We could add a ::before pseudo-element to  .data-point to display the data-value information it contains in a tooltip on hover over the data point. This is a nice-to-have sort of touch that helps turn our simple chart into a finished product.

Axis lines

Notice that the chart axises are unlabeled? We could distribute labels representing the highest value, zero, and any number of points between them on the axis.

Margins

I tried to keep the numbers as simple as possible for this article, but in the real world, you would probably want to include some margins in the chart so that data points don’t overlap the extreme edges of their container. That could be as simple as subtracting the width of a data point from the range of your y coordinates. For the X coordinates, you could similarly remove the width of a data point from the total width of the chart before dividing it up into equal regions.


And there you have it! We just took a good look at an approach to charting in CSS, and we didn’t even need a library or some other third-party dependency to make it work. 💥