Skip to main content
Photography of Alvaro Montoro being a doofus
Alvaro Montoro

Family Dodo

Building a coordinate system with HTML and CSS

css html webdev

I developed a dynamic cartesian coordinate system. It may be a bit basic, but I find it interesting because you can plug in many different functions and display the points correctly. All while having some interesting features:

  • Customizable: pick the colors, size, ranges, center of the system, etc.
  • Responsive: 100% of the parent width by default, but it can have relative/absolute sizes.
  • Easy-to-use: straightforward translation of functions to CSS.
  • Lightweight: 35 lines of CSS and a line of HTML for each point to be displayed. No JavaScript.

Yes, you read right: it doesn't use JavaScript to do the function calculations. Instead, it's all in CSS with the calc() method.


Article contents:

  1. Introduction
  2. The Code
  3. Variables
  4. Examples and Demos
  5. Accessibility
  6. What's next?

Introduction

A weekend in September —Yes, I've had the article written since then but never published it—, I woke up early, and taking advantage that everyone else in the house was still sleeping, I decided to play with HTML and CSS. Looking at Twitter, I found this tweet by Lea Verou:

She uses calc() to generate a range of values following a linear function. It is an interesting approach that uses calc() combined with custom properties (I've also seen it done by Ana Tudor and Temani Afif, among others). I wanted to try something like this too. Maybe create an example using a quadratic function. Then one thing led to another, and the demo started growing.

First, I built a basic graph of a cartesian system with a quadratic function with just HTML and CSS. All the values were hardcoded, so it was too specific. Then, I tried to make it more and more generic to match as many functions as possible: linear, quadratics, cubic, etc., and it worked. It scaled pretty well to the new operations. And it wasn't a complex demo either.

The Code

The whole code for the cartesian system fits in fewer than 40 lines of CSS code! And that's taking into account the use of CSS custom properties that made the whole thing a bit lengthy (but more customizable, so it's a give-and-take situation.)

Here is the CSS code:

.css-ccs {
  --c: 10;
  --cx: 5;
  --cy: 5;
  --dsize: 10;
  --dcolor: #369;
  --size: 100%;
  position: relative;
  width: var(--size);
  height: 0;
  padding-top: var(--size);
  box-sizing: border-box;
  aspect-ratio: 1/1;
  background-image:
    linear-gradient(#0000 calc(var(--cy) * 100% / var(--c) - 1px), #0008 0 calc(var(--cy) * 100% / var(--c) + 1px), transparent 0),
    linear-gradient(to right, #0000 calc(var(--cx) * 100% / var(--c) - 1px), #0008 0 calc(var(--cx) * 100% / var(--c) + 1px), transparent 0),
    repeating-linear-gradient(#0002 0 0.5px, #0000 0 calc(100% / var(--c) - 0.5px), #0002 0 calc(100% / var(--c))),
    repeating-linear-gradient(to right, #0002 0 0.5px, #0000 0 calc(100% / var(--c) - 0.5px), #0002 0 calc(100% / var(--c)));
}

.css-ccs.no-overflow {
  overflow: hidden;
}

.css-css .dot {
  --translationUnit: 100% / var(--c);
  --translationX: var(--translationUnit) * var(--cx);
  --translationY: var(--translationUnit) * var(--cy);
  /* Vertical is "flipped" in CSS: higher values go down! Do negative! */
  --y: calc(var(--translationY) - var(--translationUnit) * var(--function));
  width: calc(var(--dsize) * 1px);
  height: calc(var(--dsize) * 1px);
  background: var(--dcolor);
  border-radius: 50%;
  position: absolute;
  transform: translate(-50%, -50%);
  left: calc(var(--translationX) + var(--x) * var(--translationUnit));
  top: var(--y);
}

That .css-ccs code will generate the axes and the guidelines, while the .dot sets the stage for the function later.

As for the HTML, we will need an element with class css-ccs (CSS Cartesian Coordinates System) that will contain other elements of class dot that will set the x value in the style. Something like this:

<div class="css-ccs">
  <div class="dot" style="--x: -3;"></div>
  <div class="dot" style="--x: 0;"></div>
  <div class="dot" style="--x: 1;"></div>
  <div class="dot" style="--x: 2;"></div>
  <!-- ... -->
</div>

Ideally, I would have loved to have something like this:

<figure>
  <figcaption>Graph Title</figcaption>
  <output data-x="-3"></output>
  <output data-x="0"></output>
  <output data-x="1"></output>
  <output data-x="2"></output>
</figure>

And while I will eventually change the tags, not all of the changes are possible. data-x would be the best way for indicating the x value: a data attribute for a piece of data instead of a "styling variable." Unfortunately, CSS doesn't currently support reading the data attributes and passing the value to a custom property. So the custom property will do for now.

Finally, we need to plug in the function to the graph. It will go for the .dot class and be (kind of) straightforward: we will define a --function custom property in the dot with a calc() with the function. That's it.

The catch? CSS doesn't have powers (or many other types of operations for that matter), so to do something like x2, we will need to multiply x by x (x*x). We have x in the --x custom property, so we will need to do this:

.my-chart .dot {
  /**
   * x^2 = x * x = var(--x) * var(--x)
   */
  --function: calc(var(--x) * var(--x));
}

A bit lengthy and verbose, but still simple and not too difficult to read.

Variables

We can customize the graph, passing some CSS variables to the chart itself (the container) and individually to each dot.

These are the variables for the container:

Name Default Description
--c 10 Number. Size of the diagram (in total "cells"). It must be a positive number.
--cx 5 Number. Position of the x-axis in the diagram. It must be an integer. It should be positive and less or equal to --c.
--cy 5 Number. Position of the y-axis in the diagram. It must be an integer. It should be positive and less or equal to --c.
--dsize 10 Number. Size of the dots in pixels. It must be positive and greater than zero.
--dcolor #369 Color. Color of the dots (any color format).
--size 100% Length. Width of the coordinate system. It can be absolute or relative to the parent.

Thanks to the CSS cascade, we can redefine some of those values individually for each dot. Additionally, we have to pass the x value for the dots:

Name Default Description
--dsize 10 Number. Size of the dots in pixels. It must be positive and greater than zero.
--dcolor #369 Color. Color of the dots (any color format).
--x N/A Number. Required. Value of the x to be passed to the function and represented in the graph.

Examples and Demos

Here there are some examples of plotted graphs with CSS using this method:

Linear function: x - 3

#axis_x-3 .dot {
  /**
   * f(x) = x - 3
   *        x = var(--x)
   */
  --function: calc(var(--x) - 3);
}

Quadratic function: x2 - 5

#axis_x2-5 .dot {
  /**
   * f(x) = x^2 - 5
   *        x^2 = var(--x) * var(--x)
   */
  --function: calc(var(--x) * var(--x) - 5);
}

Cubic function: 0.4x3 - 5.25x - 4

#axis_04x3_525x_4 .dot {
  /**
   * f(x) = 0.4x^3 - 5.25x - 4
   *        0.4x^3 = var(--x) * var(--x) * var(--x)
   *         5.25x = 5.25 * var(--x)
   */
  --function: calc(0.4 * var(--x) * var(--x) * var(--x) - 5.25 * var(--x) - 4);
}

Reciprocal square function: 1 / x2

#axis_1_x2 .dot {
  /**
   * f(x) = 1 / x^2
   *        x^2 = var(--x) * var(--x)
   */
  --function: calc(1 / (var(--x) * var(--x)));
}

Accessibility

The graphs may look nice, but they present some accessibility challenges. The whole chart is empty, so we should at least add an aria-label with a description of the content. Also, a role would be nice. Adding a role of "img" or "figure" would help announce the graph correctly to assistive technologies.

Even better, we could use the <figure>/<figcaption> tags. But then, we would need to do some CSS gymnastics because the background will take 100% of the container, and some overlap may happen.

As for the data points, right now, they are <div>, but more semantic tags would better describe what these elements are. (For example, <output>, <data>, or <samp>.)

<div class="css-ccs" 
     role="img" 
     aria-label="Cartesian coordinate system representing the function ... with points at ...">
  <!-- ... -->
</div>

Apart from that, the points don't have any information, and it would be nice to have some data on hover/focus. If/When we do that, we'll need to take into account the accessibility of both states, so there are good alternatives for assistive technology users.

What next?

While the CSS works fine and it is a nice experiment, it lacks many key features. Some of them can be added or simulated with HTML and CSS, but others require JavaScript. Still, our little coordinates systems are lightweight and straightforward.

CSS's limitations also restrict the graphs. CSS math function list is growing (min, max, clamp, etc.), and we can simulate some common operations (as Ana Tudor shows in her articles, but there are still many math calculations that cannot be done just with CSS, and that will limit us.

Probably not something to keep building into a library, but it could be helpful for drawing small examples and doesn't require a heavy external library (I'll take 40 short lines of CSS vs. a 100KB bundle of JS). Plus it was fun to develop and figure out.

Article originally published on