Why aren’t logical properties taking over everything? - Chris Coyier
Good question.
I think it’s mostly inertia.
Good question.
I think it’s mostly inertia.
I wrote recently about making the switch to logical properties over on The Session.
Initially I tried ripping the band-aid off and swapping out all the directional properties for logical properties. After all, support for logical properties is green across the board.
But then I got some reports of people seeing formating issues. These people were using Safari on devices that could no longer update their operating system. Because versions of Safari are tied to versions of the operating system, there was nothing they could do other than switch to using a different browser.
I’ve said it before and I’ll say it again, but as long as this situation continues, Safari is not an evergreen browser. (I also understand that problem lies with the OS architecture—it must be incredibly frustrating for the folks working on WebKit and/or Safari.)
So I needed to add fallbacks for older browsers that don’t support logical properties. Or, to put it another way, I needed to add logical properties as a progressive enhancement.
“No problem!” I thought. “The way that CSS works, I can just put the logical version right after the directional version.”
element {
margin-left: 1em;
margin-inline-start: 1em;
}
But that’s not true in this case. I’m not over-riding a value, I’m setting two different properties.
In a left-to-right language like English it’s true that margin-inline-start
will over-ride margin-left
. But in a right-to-left language, I’ve just set margin-left
and margin-inline-start
(which happens to be on the right).
This is a job for @supports
!
element {
margin-left: 1em;
}
@supports (margin-inline-start: 1em) {
element {
margin-left: unset;
margin-inline-start: 1em;
}
}
I’m doing two things inside the @supports
block. I’m applying the logical property I’ve just tested for. I’m also undoing the previously declared directional property.
A value of unset
is perfect for this:
The
unset
CSS keyword resets a property to its inherited value if the property naturally inherits from its parent, and to its initial value if not. In other words, it behaves like theinherit
keyword in the first case, when the property is an inherited property, and like theinitial
keyword in the second case, when the property is a non-inherited property.
Now I’ve got three CSS features working very nicely together:
@supports
(also known as feature queries),unset
keyword.For anyone using an up-to-date browser, none of this will make any difference. But for anyone who can’t update their Safari browser because they can’t update their operating system, because they don’t want to throw out their perfectly functional Apple device, they’ll continue to get the older directional properties:
I discovered that my Mom’s iPad was a 1st generation iPad Air. Apple stopped supporting that device in iOS 12, which means it was stuck with whatever version of Safari last shipped with iOS 12.
Well, now I’m really glad I wrote that post about logical properties!
We’re not there yet. So how do we get there?
Well, I don’t know for sure – but articles like this are very helpful as we try to work it out!
I was refactoring some CSS on The Session over the weekend. I thought it would be good to switch over to using logical properties exclusively. I did this partly to make the site more easily translatable into languages with different writing modes, but mostly as an exercise to help train me in thinking with logical properties by default.
All in all, it went pretty smoothly. You can kick the tyres by opening up dev tools on The Session and adding a writing-mode
declaration to the body
or html
element.
For the most part, the switchover was smooth. It mostly involved swapping out property names with left
, right
, top
, and bottom
for inline-start
, inline-end
, block-start
, and block-end
.
The border-radius
properties tripped me up a little. You have to use shorthand like border-start-end-radius
, not border-block-start-inline-end-radius
(that doesn’t exist). So you have to keep the order of the properties in mind:
border-{{block direction}}-{{inline-direction}}-radius
Speaking of shorthand, I also had to kiss some shorthand declarations goodbye. Let’s say I use this shorthand for something like margin
or padding
:
margin: 1em 1.5em 2em 0.5em;
Those values get applied to margin-top
, margin-right
, margin-bottom
, and margin-left
, not the logical equivalents (block-start
, inline-end
, block-end
, and inline-start
). So separate declarations are needed instead:
margin-block-start: 1em;
margin-inline-end: 1.5em;
margin-block-end: 2em;
margin-inline-start: 0.5em;
Same goes for shorthand like this:
margin: 1em 2em;
That needs to be written as two declarations:
margin-block: 1em;
margin-inline: 2em;
Now I’ve said it before and I’ll say it again: it feels really weird that you can’t use logical properties in media queries. Although as I said:
Now you could rightly argue that in this instance we’re talking about the physical dimensions of the viewport. So maybe width and height make more sense than inline and block.
But along comes the new kid on the block (or inline), container queries, ready to roll with container-type
values like inline-size
. I hope it’s just a matter of time until we can use logical properties in all our conditional queries.
The other place where there’s still a cognitive mismatch is in transforms and animations. We’ve got a translateX()
function but no translate-inline()
. We’ve got translateY()
but no translate-block()
.
On The Session I’m using some JavaScript to figure out the details of some animation effects. I’m using methods like getBoundingClientRect()
. It doesn’t return logical properties. So if I ever want to adjust my animations based on writing direction, I’ll need to fork my JavaScript code.
Oh, and one other thing: the aspect-ratio
property takes values in the form of width/height
, not inline/block
. That makes sense if you’re dealing with images, videos, or other embedded content but it makes it really tricky to use aspect-ratio
on elements that contain text. I mean, it works fine as long as the text is in a language using a top-to-bottom writing mode, but not for any other languages.
This is kind of a Utopia lite: pop in your minimum and maximum font sizes along with a modular scale and it spits out some custom properties for clamp()
declarations.
To complement her talk at Beyond Tellerrand, Stephanie goes through some of the powerful CSS features that enable intrinsic web design. These are all great tools for the declarative design approach I was talking about:
I like this high-level view of the state of CSS today. There are two main takeaways:
This is exactly the direction we should be going in! More and more power from the native web technologies (while still remaining learnable), with less and less reliance on tooling. For CSS, the tools have been like polyfills that we can now start to remove.
Alas, while the same should be true of JavaScript (there’s so much you can do in native JavaScript now), people seem to have tied their entire identities to the tooling they use.
They could learn a thing or two from the trajectory of CSS: treat your frameworks as cattle, not pets.
I’m a fan of logical properties in CSS. As I wrote in the responsive design course on web.dev, they’re crucial for internationalisation.
Alaa Abd El-Rahim has written articles on CSS tricks about building multi-directional layouts and controlling layout in a multi-directional website. Not having to write separate stylesheets—or even separate rules—for different writing modes is great!
More than that though, I think understanding logical properties is the best way to truly understand CSS layout tools like grid and flexbox.
It’s like when you’re learning a new language. At some point your brain goes from translating from your mother tongue into the other language, and instead starts thinking in that other language. Likewise with CSS, as some point you want to stop translating “left” and “right” into “inline-start” and “inline-end” and instead start thinking in terms of inline and block dimensions.
As is so often the case with CSS, I think new features like these are easier to pick up if you’re new to the language. I had to unlearn using floats for layout and instead learn flexbox and grid. Someone learning layout from scatch can go straight to flexbox and grid without having to ditch the cognitive baggage of floats. Similarly, it’s going to take time for me to shed the baggage of directional properties and truly grok logical properties, but someone new to CSS can go straight to logical properties without passing through the directional stage.
Except we’re not quite there yet.
In order for logical properties to replace directional properties, they need to be implemented everywhere. Right now you can’t use logical properties inside a media query, for example:
@media (min-inline-size: 40em)
That wont’ work. You have to use the old-fashioned syntax:
@media (min-width: 40em)
Now you could rightly argue that in this instance we’re talking about the physical dimensions of the viewport. So maybe width and height make more sense than inline and block.
But then take a look at how the syntax for container queries is going to work. First you declare the axis that you want to be contained using the syntax from logical properties:
main {
container-type: inline-size;
}
But then when you go to declare the actual container query, you have to use the corresponding directional property:
@container (min-width: 40em)
This won’t work:
@container (min-inline-size: 40em)
I kind of get why it won’t work: the syntax for container queries should match the syntax for media queries. But now the theory behind disallowing logical properties in media queries doesn’t hold up. When it comes to container queries, the physical layout of the viewport isn’t what matters.
I hope that both media queries and container queries will allow logical properties sooner rather than later. Until they fall in line, it’s impossible to make the jump fully to logical properties.
There are some other spots where logical properties haven’t been fully implemented yet, but I’m assuming that’s a matter of time. For example, in Firefox I can make a wide data table responsive by making its container side-swipeable on narrow screens:
.table-container {
max-inline-size: 100%;
overflow-inline: auto;
}
But overflow-inline
and overflow-block
aren’t supported in any other browsers. So I have to do this:
.table-container {
max-inline-size: 100%;
overflow-x: auto;
}
Frankly, mixing and matching logical properties with directional properties feels worse than not using logical properties at all. The inconsistency is icky. This feels old-fashioned but consistent:
.table-container {
max-width: 100%;
overflow-x: auto;
}
I don’t think there are any particular technical reasons why browsers haven’t implemented logical properties consistently. I suspect it’s more a matter of priorities. Fully implementing logical properties in a browser may seem like a nice-to-have bit of syntactic sugar while there are other more important web standard fish to fry.
But from the perspective of someone trying to use logical properties, the patchy rollout is frustrating.
I really like the idea of a shared convention for styling web components with custom properties—feels like BEM meets microformats.
Oh, this is smart! You can’t target pseudo-elements in JavaScript, but you can use custom properties as a proxy instead.
Type and space are linked, so if you’re going to have a fluid type calculator, it makes sense to have a fluid space calculator too. More great work from Trys and James!
Robin makes a good point here about using dark mode thinking as a way to uncover any assumptions you might have unwittingly baked into your design:
Given its recent popularity, you might believe dark mode is a fad. But from a design perspective, dark mode is exceptionally useful. That’s because a big part of design is about building relationships between colors. And so implementing dark mode essentially forced everyone on the team to think long, hard, and consistently about our front-end design components. In short, dark mode helped our design system not only look good, but make sense.
So even if you don’t actually implement dark mode, acting as though it’s there will give you a solid base to build in.
I did something similar with the back end of Huffduffer and The Session—from day one, I built them as though the interface would be available in multiple languages. I never implemented multi-language support, but just the awareness of it saved me from baking in any shortcuts or assumptions, and enforced a good model/view/controller separation.
For most front-end codebases, the design of your color system shows you where your radioactive styles are. It shows you how things are tied together, and what depends on what.
A reminder that the contens of custom properties don’t have to be valid property values:
From a syntax perspective, CSS variables are “extremely permissive”.
A spot-on summary of where we’ve ended up with web components.
Web Components had so much potential to empower HTML to do more, and make web development more accessible to non-programmers and easier for programmers.
But then…
Somewhere along the way, the space got flooded by JS frameworks aficionados, who revel in complex APIs, overengineered build processes and dependency graphs that look like the roots of a banyan tree.
Alas, that’s true. Lea wonders how this can be fixed:
I’m not sure if this is a design issue, or a documentation issue.
I worry that is a cultural issue.
Using a custom element from the directory often needs to be preceded by a ritual of npm flugelhorn, import clownshoes, build quux, all completely unapologetically because “here is my truckload of dependencies, yeah, what”.
This is a great bit of detective work by Amber! It’s the puzzling case of The Browser Dev Tools and the Missing Computed Values from Custom Properties.
Who do I know working on dev tools for Chrome, Firefox, or Safari that can help Amber find an answer to this mystery?
I made the website for the Clearleft podcast last week. The design is mostly lifted straight from the rest of the Clearleft website. The main difference is the masthead. If the browser window is wide enough, there’s a background image on the right hand side.
I mostly added that because I felt like the design was a bit imbalanced without something there. On the home page, it’s a picture of me. Kind of cheesy. But the image can be swapped out. On other pages, there are different photos. All it takes is a different class name on that masthead.
I thought about having the image be completely random (and I still might end up doing this). I’d need to use a bit of JavaScript to choose a class name at random from a list of possible values. Something like this:
var names = ['jeremy','katie','rich','helen','trys','chris'];
var name = names[Math.floor(Math.random() * names.length)];
document.querySelector('.masthead').classList.add(name);
(You could paste that into the dev tools console to see it in action on the podcast site.)
Then I read something completely unrelated. Cassie wrote a fantastic article on her site called Making lil’ me - part 1. In it, she describes how she made the mouse-triggered animation of her avatar in the footer of her home page.
It’s such a well-written technical article. She explains the logic of what she’s doing, and translates that logic into code. Then, after walking you through the native code, she shows how you could use the Greeksock library to achieve the same effect. That’s the way to do it! Instead of saying, “Here’s a library that will save you time—don’t worry about how it works!”, she’s saying “Here’s it works without a library; here’s how it works with a library; now you can make an informed choice about what to use.” It’s a very empowering approach.
Anyway, in the article, Cassie demonstrates how you can use custom properties as a bridge between JavaScript and CSS. JavaScript reads the mouse position and updates some custom properties accordingly. Those same custom properties are used in CSS for positioning. Voila! Now you’ve got the position of an element responding to mouse movements.
That’s what made me think of the code snippet I wrote above to update a class name from JavaScript. I automatically thought of updating a class name because, frankly, that’s how I’ve always done it. I’d say about 90% of the DOM scripting I’ve ever done involves toggling the presence of class values: accordions, fly-out menus, tool-tips, and other progressive disclosure patterns.
That’s fine. But really, I should try to avoid touching the DOM at all. It can have performance implications, possibly triggering unnecessary repaints and reflows.
Now with custom properties, there’s a direct line of communication between JavaScript and CSS. No need to use the HTML as a courier.
This made me realise that I need to be aware of automatically reaching for a solution just because that’s the way I’ve done something in the past. I should step back and think about the more efficient solutions that are possible now.
It also made me realise that “CSS variables” is a very limiting way of thinking about custom properties. The fact that they can be updated in real time—in CSS or JavaScript—makes them much more powerful than, say, Sass variables (which are more like constants).
But I too have been guilty of underselling them. I almost always refer to them as “CSS custom properties” …but a lot of their potential comes from the fact that they’re not confined to CSS. From now on, I’m going to try calling them custom properties, without any qualification.
I count at least three clever CSS techniques I didn’t know about.
Matthias has a good solution for dealing with the behaviour of CSS custom properties I wrote about: first set your custom properties with the fallback and then use feature queries (@supports
) to override those values.
When I wrote about programming CSS to perform Sass colour functions I said this about the brilliant Lea Verou:
As so often happens when I’m reading something written by Lea—or seeing her give a talk—light bulbs started popping over my head (my usual response to Lea’s knowledge bombs is either “I didn’t know you could do that!” or “I never thought of doing that!”).
Well, it happened again. This time I was reading her post about hybrid positioning with CSS variables and max()
. But the main topic of the post wasn’t the part that made go “Huh! I never knew that!”. Towards the end of her article she explained something about the way that browsers evaluate CSS custom properties:
The browser doesn’t know if your property value is valid until the variable is resolved, and by then it has already processed the cascade and has thrown away any potential fallbacks.
I’m used to being able to rely on the cascade. Let’s say I’m going to set a background colour on paragraphs:
p {
background-color: red;
background-color: color(display-p3 1 0 0);
}
First I’ve set a background colour using a good ol’ fashioned keyword, supported in browsers since day one. Then I declare the background colour using the new-fangled color()
function which is supported in very few browsers. That’s okay though. I can confidently rely on the cascade to fall back to the earlier declaration. Paragraphs will still have a red background colour.
But if I store the background colour in a custom property, I can no longer rely on the cascade.
:root {
--myvariable: color(display-p3 1 0 0);
}
p {
background-color: red;
background-color: var(--myvariable);
}
All I’ve done is swapped out the hard-coded color()
value for a custom property but now the browser behaves differently. Instead of getting a red background colour, I get the browser default value. As Lea explains:
…it will make the property invalid at computed value time.
When this happens, the computed value of the property is either the property’s inherited value or its initial value depending on whether the property is inherited or not, respectively, as if the property’s value had been specified as the
unset
keyword.
So if a browser doesn’t understand the color()
function, it’s as if I’ve said:
background-color: unset;
This took me by surprise. I’m so used to being able to rely on the cascade in CSS—it’s one of the most powerful and most useful features in this programming language. Could it be, I wondered, that the powers-that-be have violated the principle of least surprise in specifying this behaviour?
But a note in the spec explains further:
Note: The invalid at computed-value time concept exists because variables can’t “fail early” like other syntax errors can, so by the time the user agent realizes a property value is invalid, it’s already thrown away the other cascaded values.
Ah, right! So first of all browsers figure out the cascade and then they evaluate custom properties. If a custom property evaluates to gobbledygook, it’s too late to figure out what the cascade would’ve fallen back to.
Thinking about it, this makes total sense. Remember that CSS custom properties aren’t like Sass variables. They aren’t evaluated once and then set in stone. They’re more like let
than const
. They can be updated in real time. You can update them from JavaScript too. It’s entirely possible to update CSS custom properties rapidly in response to events like, say, the user scrolling or moving their mouse. If the browser had to recalculate the cascade every time a custom property didn’t evaluate correctly, I imagine it would be an enormous performance bottleneck.
So even though this behaviour surprised me at first, it makes sense on reflection.
I’ve probably done a terrible job explaining the behaviour here, so I’ve made a Codepen. Although that may also do an equally terrible job.
(Thanks to Amber for talking through this with me and encouraging me to blog about it. And thanks to Lea for expanding my mind. Again.)
Yet another clever technique from Lea. But I’m also bookmarking this one because of something she points out about custom properties:
The browser doesn’t know if your property value is valid until the variable is resolved, and by then it has already processed the cascade and has thrown away any potential fallbacks.
That explains an issue I was seeing recently! I couldn’t understand why an older browser wasn’t getting the fallback I had declared earlier in the CSS. Turns out that custom properties mess with that expectation.