Front end Developer
Modernising our Mixtape Design System
Using modern CSS and Javascript methodologies to rebuild the PreviousNext Mixtape Design System has been beneficial and fun. Here's how we did it!
We first developed Mixtape in 2019 and it's been a solid frontend foundation for several projects now, as well as a testing ground for new technologies. It’s Drupal agnostic, and in addition to its CSS it provides both Twig templates with Vanilla Js interaction as well as React components–all designed with accessibility, performance and flexibility at its core.
A recent round of refactoring on this website has been a fun way to both improve our codebase and explore a lot of the new features that are available in the frontend world.
CSS improvements
Custom CSS properties
We’ve had these custom CSS properties in Mixtape from the beginning with the help of PostCSS. We initially placed everything on the :root
element, which is no longer necessary. Refactoring these meant removing the component-specific properties like --input-border-color
and overriding the global --border-color
on the input selector.
Design Tokens
As a shareable codebase, it needs to be blisteringly easy to override the defaults. Finding a balance between OOTB benefits and flexibility has taken a few iterations, often removing defaults entirely.
Where Design Tokens has really shone (well more specifically the PostCSS plugin Jack built) is being able to override the global custom properties on a project level without duplicating the custom properties. For example, importing all Mixtape's custom props and then overriding a few project-level ones, means the browser is loading both sets. But with Design Tokens, they can be overridden during PostCSS processing and only one set lands in the browser.
CSS grid
Another improvement we made a while ago, but that is still worth mentioning, is replacing the flexbox-based grid system with CSS Grids.
Taking it a little further, you can create a “stacked” grid (or grid pile) to replace most usage of position absolute. Simply stack everything into one grid cell and align as desired. This worked well for the content card images with a ‘tag’ overlay and the hero banner when it has a background image.
CSS Layers
Another huge flexibility win came with CSS Layers. Having everything in Mixtape wrapped in a relevant @layer
means projects don’t even need to worry about CSS Specificity issues. Anything on the project level that is not wrapped in a @layer
is automatically higher than any CSS Mixtape provides. And if it is necessary to “insert” some CSS into Mixtape's level of specificity, finding and using the relevant CSS Layer is simple too.
Container Queries
Oh, the joy! We’re still experimenting here with where to use container queries, but the biggest change has been to the content card component. No longer do we need a special modifier class to, say, make a horizontal card out of the default vertical one. Simply place one card in a container big enough for it to be horizontal and it adjusts itself.
Fluid typography
Since we have container queries why not also have fluid typography, based not on viewport units but on container query units!? Using @supports
we can have typography that successfully scales (within our acceptable changes) when the container is larger. That horizontal display of our content card can easily support a larger heading size and this no longer needs to be manually set.
We’ve also set up a typographic scale, defaulting to a ratio of 1.25 (e.g. h2 is 1.25 times larger than h3, etc.), where the scale and base size (16px) can be overridden on any given project with design tokens.
Our margins also scale with the font size by returning to em
units, so h2s can have more breathing space than h3s, all wrapped up in a vertical-flow
class that only applies the margins to content that needs spacing, as opposed to global margins that components then need to remove.
Is, Not, Where and Has selectors
These are super handy little additions to the CSS selectors and have simplified a lot of Mixtape's CSS. We’ve had nesting for a while in Mixtape with PostCSS providing backwards compatibility, so being able to group elements with :is() like & :is(h1, h2, h3)
just feels better.
We also started using :not() a little while ago too, but found specificity issues which have been resolved by wrapping in :where(), so a whole selector could be something like & :where(:is(h1, h2, h3):not(h1.headline))
and this is much easier to override later on.
Finally :has() is Firefox in December, and Mixtape is ready. We’re still keeping the modifier class as well, but they’ll be easy to pull out when the time comes. We've also been using for small "progressive enhancement" cases without a fallback.
Logical Properties
Converting over to logical properties (e.g. margin-inline-end
instead of margin-right
) means that Language Translation is more easily facilitated. The entire layout (setup with CSS Grids and Flexbox) can reverse for Right to Left languages, flowing more naturally for different language directions without the need for manual definition with the [dir="rtl"]
selector.
HTML attributes
Inert attribute
The new inert attribute has allowed us to swap our usage of the hidden
attribute and have more control over the display of interactive “hide/show” elements with better CSS Transitions (fade-in, slide-down, etc.), without introducing keyboard traps. The inert
attribute natively prevents all user interactions for the element it’s present on as well as all of its children.
Popover and Anchor APIs
A new drop menu component has been on the list for Mixtape for a while, and with an upcoming project in need of one, we took the opportunity to test out the new Popover API combined with the Anchor API. The result is a nice, minimal Javascript solution. Polyfills are dynamically imported based on browser support so they don’t bloat where they aren’t needed.
This component can optionally be a horizontal set of options with the help of another container query. The choices can be all-at-once visible or exposed on click depending on the space available to it.
Javascript performance
A few changes were also made to improve the performance of Mixtape's ES modules.
Observer APIs
Any scroll event listeners were refactored to instead use the Intersection Observer API. Resize event listeners were replaced with the Resize Observer API. And any calls to getBoundingClientRect()
were also refactored into either a Resize or Intersection observer depending on their needs (i.e. if we just needed the elements dimensions we could use the Resize observer, but if we needed an elements position we used an Intersection Observer). This greatly reduced the browser's need to repaint, and so improved performance.
Event listeners
Reducing the amount of event listeners was another simple performance refactor. Where possible, instead of adding the same event listener to each item, moving them up to the wrapper element and using the event target to check for the correct item, meant just one event listener instead of 10 or 20 or 100!
Dynamic imports
One last change was the addition of two utility Lazy Loader classes. The first is again using an Intersection Observer to dynamically import a module (and any dependencies it imports) once the container element has been scrolled into the viewport (read more about optimising page loads). The second does the same thing but is based on a breakpoint using matchMedia(). This way javascript that is only needed for the footer of a page (like a “was this page helpful” widget) or javascript that is only needed on mobile (like responsive tables) isn't being loaded until it's actually needed, improving the initial page load time. Another option would be dynamic imports on events like button click or hover, but we’re still hashing out how beneficial that would be.
The end result is a much leaner codebase, a more performant frontend and a more flexible base for upcoming projects. You’ll be able to see it all in action in our upcoming PNX website rebuild.
What’s next?
This is potentially a never-ending question (sings NeverEnding Story theme) but at the top of my personal list will be exploring how subgrid can fit in, experimenting with how the new colour functions could simplify things (and waiting very patiently for color-contrast()), having a play with @scope and finally getting around to converting our Vanilla JS modules into Web Components.
Related Articles
Optimise Your Page Loads with Lazy Loading Javascript
How to optimise your progressively decoupled Drupal frontend with the new Intersection Observer API.
Goodbye Internet Explorer
Internet Explorer retires next month (June 15th). It’s well and truly time for you to stop supporting it on your website for any current and future development (you have already, I hope).
CKEditor5 scoped styles with PostCSS
The Drupal 10 update is moving to CKEditor 5. What’s different? It’s no longer an iframe! So how do we scope any custom styles we want to include?