{"id":5257,"date":"2025-02-28T10:43:59","date_gmt":"2025-02-28T15:43:59","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=5257"},"modified":"2025-02-28T10:44:00","modified_gmt":"2025-02-28T15:44:00","slug":"custom-property-fallbacks","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/custom-property-fallbacks\/","title":{"rendered":"Custom Property Fallbacks"},"content":{"rendered":"\n<p>Look at this CSS and take a guess what color our text will be. No tricks, this is the only relevant code:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-pseudo\">:root<\/span> {\n  <span class=\"hljs-attribute\">--color<\/span>: green;\n  <span class=\"hljs-attribute\">--color<\/span>: notacolor;\n  <span class=\"hljs-attribute\">color<\/span>: red;\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">var<\/span>(--color, blue);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Perhaps surprisingly, the answer is the default text color, usually black. Let\u2019s figure out why that\u2019s the case and how to write a fallback that works.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><code>var()<\/code> Fallbacks<\/h2>\n\n\n\n<p>The value <code>blue<\/code> seems a likely candidate to be our color, doesn&#8217;t it? After all, we often call the 2nd parameter of <code>var()<\/code> its &#8220;fallback&#8221; value. However, this type of fallback is only used in the following circumstances:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The custom property isn\u2019t defined.<\/li>\n\n\n\n<li>The custom property&#8217;s value is the <code>initial<\/code> keyword.<\/li>\n\n\n\n<li>The custom property is<a href=\"https:\/\/www.w3.org\/TR\/css-variables\/#animation-tainted\"> animation-tainted<\/a> and is used in an animation property.<\/li>\n<\/ul>\n\n\n\n<p><em>and<\/em>&#8230;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The custom property isn\u2019t registered with <code>@property<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>Since a registered custom property <em>must<\/em> be given an initial value, it\u2019ll always have a valid value and will <em>never<\/em> use its fallback.<\/p>\n\n\n\n<p>Our custom property is defined, not animated tainted, and its value isn\u2019t a keyword, so we can throw away <code>blue<\/code> and keep looking.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Fallback Declarations<\/h2>\n\n\n\n<p>Usually in CSS, we can rely on the cascade to fallback to a previous valid value, leading to a common pattern where we declare the property twice:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">overflow<\/span>: <span class=\"hljs-selector-tag\">hidden<\/span>;\n<span class=\"hljs-selector-tag\">overflow<\/span>: <span class=\"hljs-selector-tag\">clip<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Browsers that don\u2019t support the <code>clip<\/code> keyword will discard the entire declaration and use <code>hidden<\/code> instead.<\/p>\n\n\n\n<p>Unfortunately, custom properties don\u2019t work like this.<\/p>\n\n\n\n<p>When parsing a custom property or its matching <code>var()<\/code> function, the browser doesn\u2019t know if it\u2019s valid until it comes time to compute its value. So instead they are treated as <em>always<\/em> valid, and any previous declarations get discarded.<\/p>\n\n\n\n<p class=\"learn-more\">If you want to prevent a custom property from being overwritten, you\u2019ll have to<a href=\"https:\/\/css-tricks.com\/the-surprising-behavior-of-important-in-css-custom-property-values\/\"> mark it as important<\/a>.<\/p>\n\n\n\n<p>That means <code>--color: green;<\/code> gets discarded immediately upon discovering <code>--color: notacolor;<\/code>, and <code>color: red;<\/code> is discarded when we get to <code>color: var(--notacolor, blue);<\/code>.<\/p>\n\n\n\n<p>In the end, our CSS computes to:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">color<\/span>: <span class=\"hljs-selector-tag\">notacolor<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Unsurprisingly, this isn\u2019t valid, and that\u2019s why we get black as our color.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_wBvzxab\" src=\"\/\/codepen.io\/anon\/embed\/wBvzxab?height=450&amp;theme-id=47434&amp;slug-hash=wBvzxab&amp;default-tab=css,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed wBvzxab\" title=\"CodePen Embed wBvzxab\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">What We Can Do Instead<\/h2>\n\n\n\n<p>That all sounds bad but we actually have several options for writing fallbacks that work.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>@property<\/code><\/h3>\n\n\n\n<p>As mentioned before, if a registered custom property is invalid, it\u2019ll always use its initial-value, which means we can use that as our fallback:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@property<\/span> --color {\n  <span class=\"hljs-selector-tag\">syntax<\/span>: '&lt;<span class=\"hljs-selector-tag\">color<\/span>&gt;';\n  <span class=\"hljs-selector-tag\">inherits<\/span>: <span class=\"hljs-selector-tag\">true<\/span>;\n  <span class=\"hljs-selector-tag\">initial-value<\/span>: <span class=\"hljs-selector-tag\">purple<\/span>;\n}\n\n<span class=\"hljs-selector-pseudo\">:root<\/span> {\n  <span class=\"hljs-attribute\">--color<\/span>: notacolor;\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">var<\/span>(--color); \n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Exactly what we want, though:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We can only define a single fallback for the entire document (which can be good enough depending on how your custom properties are organised).<\/li>\n\n\n\n<li>The intent isn\u2019t very obvious, registering a property is a roundabout way of setting a fallback.<\/li>\n<\/ul>\n\n\n\n<p>If that\u2019s not a problem for you and\/or you\u2019re registering your properties anyway, this is a great option.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>@supports<\/code><\/h3>\n\n\n\n<p>Using <code>@supports<\/code> lets us check our value is valid before declaring it and that gives us even more flexibility in how we define our fallbacks. Let\u2019s look at two ways to use it:<\/p>\n\n\n\n<p>Set a safe value first, and then inside an <code>@supports<\/code> block we can redeclare the property:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-pseudo\">:root<\/span> {\n  <span class=\"hljs-attribute\">--color<\/span>: red;\n}\n\n<span class=\"hljs-keyword\">@supports<\/span> (<span class=\"hljs-attribute\">color:<\/span> notacolor) {\n  <span class=\"hljs-selector-pseudo\">:root<\/span> {\n    <span class=\"hljs-attribute\">--color<\/span>: notacolor;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then we can just use it anywhere we like, without having to think about the fallback:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">p<\/span> {\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">var<\/span>(--color);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We can set it and forget it, confident that when our value isn\u2019t supported we\u2019ll still have our previous declaration to fall back on. 9 out of 10 times this is what I reach for.<\/p>\n\n\n\n<p>Alternatively, let\u2019s skip setting a safe value, and only define the property inside <code>@supports<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@supports<\/span> (<span class=\"hljs-attribute\">color:<\/span> notacolor) {\n  <span class=\"hljs-selector-pseudo\">:root<\/span> {\n    <span class=\"hljs-attribute\">--color<\/span>: notacolor;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Where\u2019s our fallback? Well, since our property only gets declared when it\u2019s supported, we can use the 2nd parameter of <code>var()<\/code> to write our fallback inline:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">p<\/span> {\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">var<\/span>(--color, red);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you find yourself wanting to use different fallbacks for the same custom property, this could be a better option.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>The Future<\/strong><\/h2>\n\n\n\n<p>Eventually, we\u2019ll have even more options for dealing with invalid values.<\/p>\n\n\n\n<p>New CSS goodies like like the keyword <code>revert-rule<\/code> and the <code>first-valid()<\/code> function will let us do away with <code>@supports<\/code> and write our fallbacks wherever we want:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-pseudo\">:root<\/span> {\n  <span class=\"hljs-comment\">\/* Multiple inline fallbacks when declaring the property *\/<\/span>\n  <span class=\"hljs-attribute\">--color<\/span>: <span class=\"hljs-built_in\">first-valid<\/span>(notacolor, maybeacolor, red);\n}\n\n<span class=\"hljs-selector-tag\">p<\/span> {\n  <span class=\"hljs-comment\">\/* Fallback to a different rule when using the variable *\/<\/span>\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">first-valid<\/span>(var(--color), revert-rule);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>You can follow their progress on GitHub:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/github.com\/w3c\/csswg-drafts\/issues\/10443\">Discussion<\/a> resulting in the <code>revert-rule<\/code> keyword resolution.<\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/w3c\/csswg-drafts\/issues\/5055\">Discussion<\/a> resulting in <code>first-valid()<\/code> resolution.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Further Learning<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/frontendmasters.com\/courses\/css-variables\/?utm_source=boost&amp;utm_medium=blog&amp;utm_campaign=boost\">Dynamic CSS with Custom Properties<\/a> &#8211; A course from Lea Verou that covers fallbacks and much more.<\/li>\n\n\n\n<li><a href=\"https:\/\/matthiasott.com\/notes\/css-custom-properties-fail-without-fallback\">CSS Cus\u00adtom Prop\u00ader\u00adties Fail With\u00adout Fallback<\/a> &#8211; Matthias Ott wrote about this back in 2020.<\/li>\n\n\n\n<li><a href=\"https:\/\/css-tricks.com\/a-complete-guide-to-custom-properties\/\">A Complete Guide to Custom Properties<\/a>.<\/li>\n\n\n\n<li><a href=\"https:\/\/bsky.app\/profile\/joshwcomeau.com\/post\/3li3r6i7dac2w\">Josh Comeau recently shared<\/a> how he uses <code>@supports<\/code> and custom properties to work with <code>linear()<\/code>.<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post starts with quite a tricky little quiz about what a color value resolves to be.<\/p>\n","protected":false},"author":23,"featured_media":5268,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[7],"class_list":["post-5257","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-css"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/pexels-photo-6692441.jpeg?fit=1880%2C1255&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5257","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/23"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=5257"}],"version-history":[{"count":8,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5257\/revisions"}],"predecessor-version":[{"id":5272,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5257\/revisions\/5272"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/5268"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=5257"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=5257"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=5257"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}