Providing CSS Fallbacks with Chakra UI
Chakra UI has a pretty comprehensive styling API, available across all of the prebuilt components. There are property-specific options that you can use (e.g. maxHeight="10rem"
) or you can break out to a format a little closer to the output CSS using the sx
prop. However, both of these effectively use JavaScript to power their syntax, mapping a given CSS property to a specific JavaScript function, and that means you can't define a style more than once. If you're approaching this from a JavaScript background, that seems sensible and obvious, but if you're a CSS developer you know what this means: no fallbacks.
What is a Fallback?
Because of the way CSS as a programming language is constantly evolving in small, incremental jumps[1], coupled with the fact that browsers are a particularly unique rendering engine that the user controls completely[2], if you want to use more recent functionality (that may not have universal support) then you need to provide some kind of fallback position for when that new functionality fails.
There are a few different tricks that CSS uses to make this possible, with features like @supports
being particularly useful, but the oldest and most robust method is to lean into the Cascade and provide fallback values. These effectively allow you to say:
Hey, do this thing over here, and once you've done that, try this other, newer thing. Oh, you can't cope with the new stuff? That's fine, we've already got the OG version locked. OR wow! You can totally handle that new thing, so let's overwrite the old way and see the bells and whistles! ✨
It's super useful and practically a requirement of using any CSS features that have come out in the past 2-3 years, thanks to the longevity of browser versions. In terms of how that looks, let's use the very example that inspired this post: dynamic viewport units.
I'm not going to go into any kind of detail about these (plenty of other people have already done so), but suffice to say that if you want to know the current height of a browser's viewport, we've historically had access to the vh
unit. More recently, this spawned a nebulous family of similar-but-different units, including the extremely useful dvh
unit. The tl;dr version is that vh
tells you the rough-and-ready, best-guess viewport height, whilst the dvh
unit (which stands for dynamic view height) will give you the actual available space, one all browser chrome has been accounted for (including temporary stuff like disappearing toolbars on mobile browsers). Again, others have explained this much better than I can, but what you need to know is that dvh
is just generally better than vh
, as it will cater for way more actual user scenarios, so we generally want to use it.
Of course, it's new for all browsers (most major Western browsers only gaining support in mid-to-late 2022) and not available in some (see Can I Use for the current state of affairs), so we can't rely on it completely, and the existing vh
unit offers a solid fallback option. That would look something like this:
.some-class {
max-height: 100vh;
max-height: 100dvh;
}
Now in browsers that don't support dvh, they'll get a good enough value of 100vh, whilst more modern options will get the most ideal outcome. Gotta love that tasty Cascade! 🤤
Getting Chakra to Play Nice
Based on the above, the "best" solution would be for Chakra to allow a pattern like this:
<Box
sx={{
maxHeight: "100vh",
maxHeight: "100dvh"
}}
> {...} </Box>
That fits the actual, underlying logic that CSS uses, it's easy to read, and it's instantly familiar to anyone who already knows how to use CSS fallback values. Unfortunately, as noted above, that's just not how JavaScript works, and if you try it you'll get an angry linting error complaining about redefining values 🙄
OK, so what options do we have?
The Emotion(al) Backdoor
Chakra's style APIs are built on top of the well-known Emotion CSS-in-JS library, and Emotion has a built-in way of providing multiple values to a single attribute: a property array.
This works by providing an array of values to the prop directly, and looks like this:
<Box
maxHeight={[['100vh', '100dvh']]}
> {...} </Box>
The output will render in the order provided, so whatever value you put in first will be the first in the Cascade, and therefore gets overwritten by anything that comes after. For "simple" fallback patterns where you want to overwrite the entire value, this is probably the best solution, and does work well.
ℹ Why the nested array? That's because Chakra automatically assumes that any array in a style prop is referring to media breakpoints. If you try to use a single array, then your first value will be accepted as the baseline style, but the second will be rendered within a media query and therefore only apply after your lowest defined breakpoint. This can be a little hard to catch, but it is obvious if you check the output CSS and both values aren't on the same class-based ruleset.
The problem here is that Chakra redefined all of the style props types, so if you're using TypeScript it won't be happy with you. Nested arrays are not a permitted value type, so whilst it will work, it won't pass validation tests[3].
There is another workaround using Emotion that you can use, which is to import the css
function directly from the underlying package, like so:
import { css } from '@emotion/react';
<Box
css={css`
max-height: "100vh",
max-height: "100dvh"
`}
> {...} </Box>
Again, this will technically work (or at least, it did when I tried), and it seems to sidestep the TypeScript issue, but it also sidesteps Chakra entirely, at which point I become a little sketched-out about using the solution. Given that the Chakra team have given no indication that you should use Emotion functions directly, I personally wouldn't recommend this. Relying on another dependency assumes that dependency will always be available. It's unlikely ‒ but possible ‒ that a future update to Chakra may remove the Emotion library, or replace it with a limited version that doesn't contain these exports. In my mind, if we need to use a hack, that hack shouldn't rely on additional dependencies.
Making Our Own Entrance
Okay, so if there isn't an "official" method for doing this[4] then we'll have to devise our own.
To that end, how can we manipulate the existing Chakra style APIs to do something similar? One way would be to effectively redefine the value, but in a way that Chakra (and our linters) don't expect: we could use a self-referential CSS rule. The hell is that? Good question.
Chakra, like most CSS-in-JS solutions, has a method for referencing the current element via a CSS selector. In this case, it's the &
symbol, which acts as a placeholder reference to the scoped class name of the component. This is useful in many situations, including boosting the given specificity of a rule, but for us, it also creates a meaningful way to provide multiple values to CSS functions. That looks something like this:
<Box
sx={{
maxHeight: "100vh",
"&": {
maxHeight: "100dvh"
}
}}
> {...} </Box>
So what's the benefit of doing this? Well, the above has no real benefit over using the nested array technique discussed above, but for more complex rules, it gives us a lot more flexibility. Let's say you have a max height, but you want to change that between two viewport breakpoints; you can use the nested array technique for that – by providing multiple arrays – but it's getting a little hard to read:
<Box
maxHeight={[['100vh', '100dvh'], ['80vh', '80dvh']]}
> {...} </Box>
Plus, if we go even more complex – using breakpoints, and dynamic values, and other tricks – then nested arrays begin to complain, even without TypeScript. And with TypeScript, it's a non-starter. Instead, that could be written as below:
<Box
sx={{
maxHeight: { base: "100vh", md: "80vh" },
"&": {
maxHeight: { base: "100dvh", md: "80dvh" }
}
}}
> {...} </Box>
It's more verbose, but easier to understand, and more flexible.
What About @supports
?
There is an alternative to the above, which if available is as useful: @support
statements. These are a native feature of CSS which allow us to query whether a browser supports a given syntax or piece of functionality, and then conditionally apply rules if it does. Support statements are arguably a safer and better option, which invert the logic and work more like traditional progressive enhancement, but they aren't always available. Most modern additions to CSS come with some way to query their availability, but even today it isn't guaranteed that there will be a viable method for doing so. Plus, the pessimist in me feels it's worth pointing out that this relies on a browser implementing both the feature and the support query for that feature to work, whereas with fallback values it only needs the former[5].
Still, if you can use them, they may be a useful option. In Chakra, that looks something like this:
<Box
sx={{
maxHeight: { base: "100vh", md: "80vh" },
"@supports(height: 100dvh)": {
maxHeight: { base: "100dvh", md: "80dvh" }
}
}}
> {...} </Box>
This technique has the added benefit of being fully based on vanilla CSS logic, rather than relying on Chakra-specific syntax like &
. As a result, I'd say this is the best way to achieve the desired outcome. However, not all CSS features have a neat @support
rule; some cannot be meaningfully checked for in this way. In those instances, the &
selector "hack" feels like a decent, Chakra-native fallback that shouldn't ever be removed. Hopefully 🤞