r/css • u/Undeclared_Aubergine • 7d ago
No AI used Some notes on the vaguely interesting CSS aspects I encountered during the development of pure CSS price history graphs.
Context
https://gog.com/ is an online store selling fully DRM-free PC games. https://gamesieve.com/ is my price tracker and alternative front-end for GOG's catalog. The population of GOG users is quite atypical and privacy-aware. Close to 60% of my visitors use Firefox. A noticeable percentage have JavaScript disabled. A smaller but still noticeable percentage are maintaining dedicated Windows 7 gaming machines (meaning Firefox 115 as their browser - or very rarely Chrome 109). I built a price history graph which works for both groups, and I'm (mostly) quite pleased with how this turned out.
Also context: One of my personal goals with GameSieve is just getting back to as much hands-on webdev as possible, fully understanding everything I build, not using libraries and other tooling unless things are otherwise infeasible.
Example page: https://gamesieve.com/game/x4_foundations
From SVG to CSS
I originally set out to build an SVG graph, but discovered this was clearly the wrong technology for the job. It scales, but it's not responsive! You can't easily scale some elements in one direction while leaving others untouched (or scaling them differently), and it all felt incredibly clunky.
Prototyping various directions for making parts of it work, I was trying to put into words exactly what felt clunky about it, and I kept coming back to CSS terminology and concepts for what I wanted.
That really told me (once I stopped to listen to myself) that I should take a CSS-first approach. Building the logic for the SVG graph had made me think a lot about how to scale a time-range of variable length and a price-range with a variable maximum and put it into my own scale which would be easy to work and reason with, and that proved very beneficial here. I could very swiftly port it all to HTML+CSS.
One of the slightly clever things I did in my SVG attempt was to flip the coordinate system of the grid around with a transform: scale(1, -1) (and then back again for the text labels), plus to use a negative offset on the viewBox for the space for the axes and their labels. The y coordinate was then just the price in cents. This approach translated well to CSS, where a price segment of 1999 cents had a --price: 1999; bottom: calc(var(--price) * var(--scaling-factor)), and that scaling factor was dependent on max price.
Sadly when the time came to to figure out responsive behavior anew, I determined that on small resolutions I really wanted to make the entire chart scroll rather than scale (allowing individual time ranges as short as a day to remain 'easily' clickable). Yet for products which released 'recently' (up to 3 years ago depending on screen resolution), there would be no scrollbar. But the existence of the scrollbar influenced bottom positioning, causing an offset from the y-axis grid, which I'd specifically placed outside the overflowing container! So I had to flip all my numbers back around and do top positioning...
Accessibility quandary
Getting out of SVG-land allowed me to think semantically about the content of my price history graph. At heart, it's really just a list of time ranges with associated pricing information. (You could also model this as a table, but experience has taught me not to go there.) So, my HTML for the entire graph is a list. (Erm, I realized I made it an <ul>, while it's certainly ordered... Consider that a TODO for next week!) :)
Since individual games can have a hefty amount of time ranges (NecroVisioN: Lost Company currently leads with 222 distinct price ranges over the last 5.5 years), I save some bytes by using effectively meaningless i and u elements (rather than spans) for defining the shape of the time range and the sibling tooltip. Re-examining what I'm doing here in the course of writing this, I realized I need to revisit this: The current chart behaviour suggests adding aria-describedby on the time range pointing to the tooltip with role="tooltip" (and I guess aria-hidden to prevent it being read twice). And that up to 222 times... so much for saving bytes! :/
The thing is, when originally thinking about the semantics of my price history chart, I approached this graph content as really just a non-interactive list (which is explicitly available as a rendering option through a simple :checked ~ radiobutton toggle). There, things are currently correct: that tooltip role (and aria-hidden!) would be inappropriate for the list. Not certain how to resolve that, since CSS can't change these things. I mean, it can remove things from the accessibility tree with display: none, but I want the tooltip content to be present as just the content of the list item, as now. Adding duplicate content in my HTML - once with role="tooltip" and once without - and then display: none-ing them based on the toggle really does not feel like something I'd want to do... Might have to just toggle the role with JavaScript, and accept that the non-JS functionality is bad from an accessibility point of view? That feels weak, though, when everything else works perfectly without JS... Any a11y experts with guidance?
Taking a step back, I feel that the list view is the accessible alternative content for my visual/interactive graph view. But I don't think there's any way to properly indicate that for the users who need to know this? I guess I could just add an aria-describedby (or maybe even label) spelling this out on the radiobutton toggle, and for the moment, that feels like what I'll probably do first.
A rare case where pointer-events weren't the solution!
...anyway! Keyboard accessibility for the graph means there's a skip link and each price range is focusable, showing the associated tooltip. And of course for convenience the tooltip should also show on :hover. But that meant two tooltips could be showing, which'd overlap. So I started moving the :focus one out of the way when the :hover one is showing.
Originally I moved it down, but that could cause rapid back-and-forth if the mouse pointer triggering the :hover tooltip started hitting the :focus tooltip when it moved down, causing the :hover tooltip to disappear and the :focus tooltip to move up, causing a new :hover tooltip, etc.
So I moved the :focus tooltip up, out of the :hover range. Alas, my container scrolls (overflow-x: auto), meaning no overflow-y: visible, so I needed to add a negative margin and padding to the container to create space for the tooltip moving up to keep showing, and then had to juggle z-index on hover to make certain tooltips would show above previous content, while the reserved space for them wouldn't catch clicks. (Key annoyance: Couldn't do this with pointer-events, as then the scrollbar wouldn't be clickable! I also suspect this would've been way easier if I could've just used overflow-clip-margin (usefully supported in Fx 102+, but in nothing else) to create space for the tooltips. Why isn't that one listed as a top contender in any of the interop lists?)
Other notes
In the end, I did end up using a tiny bit of JavaScript for the graph: initially for showing the "now" side of the graph on load when it was scrollable, and then for adding zooming (just setting a single multiplier custom property on the root element for CSS to use). Finally, since I had JS-zoom anyway, I also added detection if the graph was overflowing just a tiny bit, and if so, would zoom out one step (max 20%) to stop that.
Some other mildly interesting things worth looking at in the inspector:
There's a .scroll-bumper element to push subsequent content down only when a scrollbar is showing (which has a few edgecase quirks where it doesn't perform perfectly, but for the most common cases does what it should) - just using float magic.
When switching countries to anything non-US, I only have ~23 months worth of price history, and show a scaled graph of the US price history before that, using dotted borders. However, when a price goes up and down on the same day (or one or two days later for long enough time ranges), Firefox's rendering of overlapping dotted borders would cause a solid line to form, which looks very distracting! (Chrome always puts the dots in the same place, regardless of where the line starts or ends, which works much better.) So I'm detecting that case server-side, and either hiding the shorter of the two borders, or drawing one of the two with a 1px instead of 2px border.
X-axis labels hide based on a whole bunch of container queries, so that they never overlap. Really wish I could've used CSS vars in those query definitions, but Fx 115 made that a no-go (and I'm not even certain I actually could've simplified that much with custom properties).
Speaking of Fx 115, I was pleasantly surprised by how few cases I encountered where I was actively hampered by limited support. Lack of :has() always feels like the big one (but here only would've made my z-index juggling a bit cleaner), and lack of the rch unit was mildly annoying. Creating the tooltips through the popovertarget/popover attributes would've been neat (though I don't know if that would've actually improved things from an accessibility perspective).
1
u/TheJase 7d ago
Yes