CSS.
rants by Bast on Monday February 17th, 2025
I have a love-hate relationship with css.
There's a lot to css. It's an example of an almost-perfect design language, built so that you can label and value all possible display properties of items in a document, or even just in a user interface. Many frameworks hijack it with their own properties, or have something quite close.
That's not to say there isn't a lot missing. Nesting is only becoming available now, despite being in demand for literal decades. Many powerful features, like arbitrary numbers of pseudo elements or injected wrappers, are completely absent. Implementation-driven decision making left us without much-desired features, like auto
animation.
But nothing quite gets me like the idiosyncracies.
I was implementing tooltips for my blog today.
I've been experimenting using the abbr
element for this. It's not exactly what I'm looking for, and I'll probably not being using it going forwards (especially after this experience) but it led me down the usual rabbit hole of pseudo-elements like ::after and ::before, z-indexing, absolute vs relative positioning…
And like normal, I stumbled upon something odd. Something wasn't behaving as expected.
I was generating a tooltip via content: attr(title)
. It was given a maximum width, and a display of block
, which traditionally causes it to fit as much content horizontally as possible.
Except… it didn't.
Despite the content wanting more space (a normal, non-layout-removed width of 244.7px), and despite the lack of any constraint on width
(once I removed the max-width property), and despite the default (and then later, explictly specified auto
for right
)… the text wrapped at the earliest available spot.
What?
Try it yourself:
<style>
abbr {
/* Disable auto small caps */
font-variant: unset;
text-decoration: dotted underline;
/* Make positional anchor for tooltip */
position: relative;
z-index: 0;
}
abbr::before {
display: block;
content: " ";
position: absolute;
top: -0.25em;
right: -0.5em;
left: -0.5em;
bottom: -0.25em;
background-color: rgba(255, 0, 0, 0.2);
z-index: -1;
}
abbr::after {
display: block;
visibility: hidden;
position: absolute;
top: 100%;
left: -12px;
content: attr(title);
max-width: min(calc(768px * 0.6), 60vw);
overflow: hidden;
padding: 0;
line-height: 0px;
border-radius: 0;
transition: all 0.8s;
backdrop-filter: blur(4px);
background-color: transparent;
color: transparent;
}
abbr:hover::after {
visibility: visible;
padding: 4px 6px;
line-height: 1em;
border-radius: 3px;
background-color: rgba(10, 10, 10, 0.9);
color: rgb(240, 240, 240);
transition: all 0.3s;
}
</style>
<abbr title="Content Management System">CMS</abbr>
The behavior is.. rather buggy. We are animating all properties here with a shorthand (useful for development, I'll specify all of them when deploying) but we're getting all sorts of fun as a result.
What should be animated is:
- visibility: visibility animates late, it remains in it's prior state until the animation time expires, then it jumps to the final state
- padding: padding should animate linearly by default across the values
- line-height: this one is weird. I have chosen line-height: inherit here. This should animate mostly as expected (because it's inheriting a fixed, non-auto value. Animating
auto
is messy. It's technically spec-forbidden, but supported in chrome as of recently). Firefox is currently behaving as if line-height is snapping to zero as soon as the transition begins. This is.. not how that is supposed to work, and line-height animates normally in other situations.
Fascinatingly, even setting it to a hard pixel value still results in it all collapsing onto one line…
Hey look. I found a firefox bug. line-height: 0
fails to transition. line-height: 0px
works. Ditto for using inherit
on the other end instead of 1em.
I'll report that later I promise…
Back to the actual problem at hand, text wrapping. Why does the element not expand to fit it's content?
Regardless, this design is to be scrapped for now. As nice as a custom ::after element is, I need to support things like text selection, or click instead of just hover for activation. A closer element to that is <details>:
<details>
<summary>CMS</summary>
Content Management System
</details>
CMS
Content Management SystemHonestly, I can't remember why I was implementing this.
It's fine. I wanted expandable inline text for something, so I'm sure it'll come up again when I get around to writing whatever blog post got me started on this rabbit hole.
These two neat styles give me the behavior I want:
<style>
details {
display: inline-block;
}
details summary {
display: inline
}
</style>
<details>
<summary>CMS</summary>
Content Management System
</details>
CMS
Content Management SystemAdd a little extra to make it pretty:
<style>
details {
display: inline;
background-color: rgba(255, 255, 255, 0.15);
}
details summary {
display: inline;
text-decoration: dotted underline;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 0.25rem;
}
</style>
<p>
A
<details>
<summary>CMS</summary>
Content Management System
</details>
is a powerful and required part of a lot of website structures, used to delegate management and curation of content to non-technical teams, while also providing force-multiplier effects to technical developments, lorem ipsum dolor sit amet.
</p>
A
CMS
Content Management System
Ah. Hmm.
Well. I suppose I have a love hate relationship with HTML, too.
For the curious, the problem here is that <p> elements cannot contain block level elements, and <details> is apparently a block level element. This causes the browser to auto-close and auto-open <p> tags around it, breaking content flow.
The actual "rendered"/post-parse code looks like this in firefox:
<p>
A
</p><details>
<summary>CMS</summary>
Content Management System
</details>
is a powerful and required part of a lot of website structures, used to delegate management and curation of content to non-technical teams, while also providing force-multiplier effects to technical developments, lorem ipsum dolor sit amet.
<p></p>
Which is.. suboptimal.
Theoretically I could hack around this, simulating <p> tags with styled divs or custom elements, but I don't think that's a good use of my time or blog space.
Maybe I can hack something with checkboxes.
At this point I'm feeling sympathy for all my fellow developers who react directly to react to solve things like this. With react, everything is simpler. You make a component, it renders as a div. When clicked, it renders another div. Everything is pre-coded, logical, straightforwards, and absent of the depths of the actual web platform behind it. You can even avoid the complexity of stylesheets by embedding your CSS in your JS too.
Unfortunately, by doing so, you eschew all the accessibility and reliability work that has gone into this cursed web platform for the past 30 years. In a huge portion of react applications, I literally cannot right click links. Who would want to right click a link?
Well… I had some trouble with Element, the matrix chat client. As part of their login process, they send you off to your homeserver via an authorization link. An authorization link that only opens in your default browser.
An authorization link you cannot right click or copy in any way.
An authorization link.. that only works once.
And so I was left with the incredibly frustrating experience of "click here to log in," the link opening in a browser session I absolutely wasn't going to log into my github within, and copy-pasting the link not working. And no "copy link" option.
This was not generally seen as a problem. Why would someone not log in with their default browser?
Matrix as a chat client needs to grab the technical crowd first. Because the technical crowd are the ones that will run homeservers in the first place. The ones who try out new, weird, funky open source chat applications that do federation and put up with the technical nits of early-release and untested software because that's what they like to do.
And, well, technical users have strange, technical environments.
/ramble
An <a> tag has a lot to it. React does not implement any of it by default, and you are certainly going to miss some. On the other hand, HTML and CSS are full of nightmarish, conflicting, complicated cruft. What do?
Your best.
How does a screenreader read the page? Who knows. Since I've paid some attention, hopefully significantly better than your average SPA. How does a screenreader read checkbox-toggled content? Honestly, not sure. Going to have to test that. TODO
<style>
bs-defn {
display: none;
}
[bs-defn-label] {
text-decoration: dotted underline;
}
input[type=checkbox][bs-defn-toggle] {
display: none;
}
input[type=checkbox][bs-defn-toggle]:checked + bs-defn {
display: inline;
}
</style>
<p>
A
<label for="cms-defn" bs-defn-label>CMS</label><input type="checkbox" id="cms-defn" bs-defn-toggle>
<bs-defn>( Content Management System )</bs-defn>
is a powerful and required part of a lot of website structures, used to delegate management and curation of content to non-technical teams, while also providing force-multiplier effects to technical developments, lorem ipsum dolor sit amet.
</p>
A
Styled up for animation:
<style>
bs-defn {
display: inline;
position: absolute;
visibility: hidden;
clip-path: xywh(0px 0px 0px 100%);
transition: clip-path 1s, position 0s 1s allow-discrete, visibility 1s;
}
[bs-defn-label] {
text-decoration: dotted underline;
}
input[type=checkbox][bs-defn-toggle] {
display: none;
}
input[type=checkbox][bs-defn-toggle]:checked + bs-defn {
position: static;
visibility: visible;
clip-path: xywh(0px 0px 100% 100%);
transition: clip-path 1s, position 0s 0s allow-discrete, visibility 1s;
}
</style>
<p>
A
<label for="cms-defn" bs-defn-label>CMS</label><input type="checkbox" id="cms-defn" bs-defn-toggle>
<bs-defn>( Content Management System )</bs-defn>
is a powerful and required part of a lot of website structures, used to delegate management and curation of content to non-technical teams, while also providing force-multiplier effects to technical developments, lorem ipsum dolor sit amet.
</p>
A
There is, also, unfortunately no option to "typewriter" the relevant text out. But we can abuse clip-path! Finally, something that isn't disabled on inline elements. The jank took me a while to work around (firefox apparently doesn't support transitioning between display: inline-block and display: inline, which is required if we want a width-based shrinkage. So we basically cannot have that. Additionally, transitioning between display: none and display: inline breaks things (it scales out nicely, but scaling in is instant). I'm not sure why that is, but fiddling for half an hour didn't figure it out and it was directly related to display, so we're working around that by abusing position type toggling, which otherwise works sanely.
This gives us the effect you see above: instant space snapping (if we wanted it more clean, javascript is required) and then a visual scale-out of the content (for the pretty).
The actual implementation I will commit will be faster and probably have a non-default animation curve. I also believe that you want an animation to be slower fading than appearing, so I'll add that nonuniformity to the delay.
PS: if you're wondering why it janks onto an extra line in chrome… chrome apparently doesnt' like animating position:
. Even toggling it on and off in the devtools is janky. It's an edge case: absolutely positioned elements are block elements, even if marked otherwise (because an inline absolute doesn't make sense), but this is apparently not attached to the animation.
…but don't worry
We can attach it manually. Evil laugh.
transition: clip-path 1s, position 0s 1s allow-discrete, display 0s 1s allow-discrete, visibility 1s;
"But bast" you might say. You're not changing the display value! How can you animate something that you are not changing?
Indeed. I am not changing it. But, lo, behold!:
A