Introduction
Nowadays, CSS is evolving to replace JavaScript for many functions that front-end developers have traditionally handled with JS code. This shift allows us to address old issues with new solutions and perspectives, and I would like to share an example of this with you.
The problem
We need to create a component with cards. Each card has five sections that are optional; however, if a section is empty, it should leave a blank space. Additionally, each card should have the same width and height, and each section should use the same space. We have also taken into account that the length of the texts can be different. The client wants the component to work as a slider if the number of slides exceeds 2 on mobile, 3 on tablet, and 5 on desktop too.
Given these requirements, Flexbox is not suitable because it couldn't ensure uniform height for all subsections, especially when some are absent. You can’t use min-height either because you don’t know how much length the text is going to have.
Therefore, we turn to CSS Grid. We could have removed the card wrapper using “display: contents;” and tried to use grid areas to place everything, which might work if we knew the number of items beforehand. However, we are using a CMS, so it means that the number of cards could vary, which isn't ideal. We could revert to using JavaScript to calculate each section's height for each card and apply the maximum value to each card, but this approach is cumbersome, especially when using a JS library like Slick Slider.
This creates conflicts with our JS code and Slick Slider's calculations for widths and heights, especially when resizing the screen. Additionally, we have to minimize cumulative layout shifts for performance reasons.
When we estimated this component, it seemed like a simple task, but quickly turned into a coding challenge.
Thankfully, we now have new tools in our belts. Subgrid and Container Units offer modern solutions to our problem. Both features are quite recent and mostly unknown for a great majority of FE developers.
What is Subgrid?
CSS Subgrid, released in 2023, is supported by the latest versions of all major browsers, including Safari since version 16. Subgrid allows you to create a nested grid within a parent one, effectively creating a two-dimensional layout inside another two-dimensional layout.
Let's apply this to our problem.
We have a container element with multiple nested cards. We set this container as a grid to control the global width and height of all the cards (first-level grid with any number of columns and only one row).
.card-collection {
display: grid;
grid-auto-flow: column;
gap: 0;
// Old property as fallback.
overflow: hidden;
// New property.
overflow: clip;
}
Now, we add the second-level grid logic by creating a subgrid for each card. This subgrid has one column and five rows (corresponding to each subsection within the card). The subgrid controls the size of each subsection for all cards simultaneously, even if they are empty!
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: auto / span 5;
row-gap: 10px;
}
By using Subgrid, we eliminate complex JS scripts and conflicts, allowing Slick Slider to manage its usual functions without interference.
Solving issues with Slick Slider and layout shifts
Slick Slider creates a scrollable container called "slick track", which adjusts its width based on the number of items on screen. We need a CSS solution that maintains consistent element widths regardless of visibility (even when the screen width changes).
As you can see, the non-visible cards don’t maintain the same width as the visible ones, and that’s a problem because it stretches the width and height of the visible areas in the other cards.
Unfortunately, “grid-auto-columns: 1fr;” doesn’t work because we don't want equal widths without being related to the container width (autofit and autofill didn’t work either). Using min-width is also not feasible because it is not calculating the percentage according to the container but the element itself (as you can see in the image below).
This is where Container Units come in.
Container Units come to the rescue!
Container Units, also released in 2023, allow you to set an element's width based on its parent container. This feature is reliable when percentages are insufficient or specific styles are needed based on the parent’s width.
There are plenty of new container units: cqw, cqh, cqi, cqb, cqmin, and cqmax. In our case, we focus on cqw (container width units) to define card widths based on the wrapper's width.
We add an external wrapper to our container, which uses the maximum available screen width. This wrapper is defined as:
.collection__container {
width: 100%;
max-width: 100%;
container-type: inline-size;
container-name: card-slider;
}
The browser uses this wrapper to calculate container units. We set card widths based on breakpoints:
- Mobile: 2 slides, each card should have 50cqw (100cqw / 2).
- Tablet: 3 slides, each card should have 33.33cqw (100cqw / 3).
- Desktop: 5 slides, each card should have 20cqw (100cqw / 5).
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: auto / span 5;
row-gap: 10px;
// 2 columns by default.
width: calc(100cqw / var(--mobile-slides-number, 2));
}
@media screen and (min-width: 768px) {
.card {
// 3 columns by default.
width: calc(100cqw / var(--tablet-slides-number, 3));
}
}
@media screen and (min-width: 1200px) {
.card {
// 5 columns by default.
width: calc(100cqw / var(--desktop-slides-number, 5));
}
}
For the gap, we add padding within the card, avoiding complex calculations and ensuring better Slick Slider performance. We also apply negative lateral margins to avoid extra padding on the first and last visible slides.
Note: Meassured in codepen page, so, in real enviroments could be even better.
Cool! We did it. Time for a beer! You can check the results here:
Fallback for older browsers
For older browser support like Safari 15, we use JavaScript to detect the browser version and apply a custom class to the body. We revert to using Flexbox, acknowledging some layout shifts until Slick Slider initializes. This is a temporary compromise, ensuring the component remains functional. Older browser versions will eventually be deprecated and unused, so this shouldn't hinder development.
Conclusions
By leveraging CSS Subgrid and Container Queries, we efficiently addressed a complex layout challenge without relying on heavy JavaScript solutions. These modern CSS features provide robust, maintainable solutions that enhance performance and reduce conflicts with libraries like Slick Slider. While we offer fallback solutions for older browsers, the future-proof approach ensures a seamless experience for users on up-to-date browsers. This methodology exemplifies the power of modern CSS in tackling traditional layout problems, setting a new standard for front-end development.