Published at
Updated at
Reading time
3min
This post is part of my Today I learned series in which I share all my web development learnings.

Custom properties and CSS parsing are always good for surprises like !important behaving slightly differently or properties being "invalid at computed value time".

Today I discovered yet another surprise โ€” custom properties don't work in combination with the url() function. ๐Ÿ˜ฒ

.something {
  /* this doesn't work */
  --image: "https://jo.com/image.jpg";
  background: url(var(--image));
}

Roman Dvornov describes the details quite well in a GitHub issue, but let me give you a condensed explanation.

The two modes of url()

Your url()-containing CSS will be parsed differently, depending on how you use url(). There's an old and a newer way:

  • Old: url(https://jo.com/image.jpg)
  • Newer: url('https://jo.com/image.jpg') or url("https://jo.com/image.jpg")

The problem of the legacy url() token

And these missing quotes of the old way might seem like a tiny detail, but they affect how your CSS is parsed.

Without quotes, the url() syntax looks like a CSS function, but it isn't. CSS parsers will treat it as a single token, a so-called url-token.

.something {
  background: url(https://ja.com/image.jpg);
  //          \---------------------------/
  //            without quotes this โ˜๏ธ is
  //               a single CSS token
}

And this entire token from url( to the closing ) enforces parentheses, whitespace characters, single quotes (') and double quotes (") to be escaped with a backslash.

If you're curious, here's how url() is parsed in this scenario.

Parsing algorithm for a url-token

url() with a quoted string, on the other hand, is a normal and flexible CSS function notation, which is parsed part by part and works as expected.

But now, guess what happens when you want to use a custom property in combination with url()?

.something {
  /* this doesn't work */
  --image: "https://jo.com/image.jpg";
  background: url(var(--image));
  //              โ˜๏ธ "No quotes? Cool, that's a url-token!"
  //              ๐Ÿ˜ข "Too bad though, `(` isn't allowed in here..."
  //              โŒ "I'll throw everything away!"
}

Because there are no quotes, this declaration is parsed as a url-token. And unfortunately, the ( in var(--image) isn't escaped, so the parser throws an error and invalidates the entire CSS declaration.

And this legacy url-token parsing is why you can't use custom variables inside of url().

The solution to work around the legacy url()

How can your work around the url-token problem then?

First, you can restructure your code and move the url() function into the custom property declaration itself. The following work just fine! ๐ŸŽ‰

.something {
  --image: url(https://jo.com/image.jpg);
  background: var(--image);
}

Additionally, the CSS spec maintainers added a new alias to remove the url-token behavior. The src() notation behaves the same way as url() but without this weird legacy url-token logic.

.something {
  /* this works! (theoretically) */
  --image: "https://jo.com/image.jpg";
  background: src(var(--image));
}

Unfortunately, no browser supports src() (I couldn't find browser support information but tested current Chrome, Safari and Firefox) yet, so it's time for us to wait. ๐Ÿคทโ€โ™‚๏ธ

CSS parsing and custom properties โ€” always good for a surprise!

If you want to read more about this topic, here are some resources:

If you enjoyed this article...

Join 5.5k readers and learn something new every week with Web Weekly.

Web Weekly โ€” Your friendly Web Dev newsletter
Reply to this post and share your thoughts via good old email.
Stefan standing in the park in front of a green background

About Stefan Judis

Frontend nerd with over ten years of experience, freelance dev, "Today I Learned" blogger, conference speaker, and Open Source maintainer.

Related Topics

Related Articles