Planning the responsive behavior of a component can be tricky, particularly when the component layout is highly affected by its content.
For example, let's consider a table component. You may decide to use two different layouts: one optimized for smaller screens (state-a layout), the other for bigger screens (state-b layout).
You would then need to decide the breakpoint for the change of layout and set it in CSS using a media query.
However, the same table component could have two columns or twenty.
If your table has a low number of columns, you may decide to change the layout at a small breakpoint:
For a table with more columns or richer content, instead, you may want to change it later to make sure it does not look too crowded on small screens:
Ideally, you should find a breakpoint that works for both of them (and all the other tables you have on your website). You could use a .table
class to define the style of the state-a layout, and use a media query to overwrite this style for the state-b layout:
<table class="table">
<!-- table content -->
</table>
<style>
.table {
/* state-a layout style */
}
@media (min-width: 600px) {
.table {
/* state-b layout style */
}
}
</style>
This solution may not be ideal because the breakpoint you choose is a compromise: you could end up with some tables that look too crowded and others too sparse. Even if you find a solution that seems to work with today's tables, it could easily break with tomorrow's.
Class Modifiers #
A possible alternative would be to define responsive class modifiers (classes that share the same style but target different breakpoints) to have the option of triggering the layout change at different breakpoints.
If we consider an example with two breakpoints (small and medium), you would have:
.table {
/* state-a layout style */
}
/* small breakpoint */
@media (min-width: 600px) {
.table--state-b\@sm {
/* state-b layout style */
}
}
/* medium breakpoint */
@media (min-width: 1000px) {
.table--state-b\@md {
/* state-b layout style */
}
}
then you can apply those modifiers to different <table>
s based on their content:
<!-- 👇 switch layout at a small breakpoint -->
<table class="table table--state-b@sm"></table>
<!-- 👇 switch layout at a medium breakpoint -->
<table class="table table--state-b@md"></table>
The code defined in the .table--state-b@sm
class is the same as the one in the .table--state-b@md
. Remember, those two classes are used to create the same layout; it's only applied at different breakpoints.
This approach has two main disadvantages. The first one is the maintainability of your code: if you need to make changes to the state-b layout, you would need to update two different classes (.table--state-b@sm
and .table--state-b@md
). This is something you can solve using a CSS pre-processor (e.g., using SASS mixins).
The second issue is having the CSS code for state-b repeated multiple times in the final CSS (twice if you have two modifiers, but it could be more if you need additional variations!).
Repeat this for all the components and the different media queries, and this could cause a significant increase in your CSS file size.
Solution #
While working on the Table category of the Components Library at CodyHouse, we ended up using a different approach; we defined a class for the state-b layout:
.table {
/* state-a layout style */
}
.table--state-b {
/* state-b layout style */
}
We then defined a --table-layout
CSS custom property inside the .table
class and modified its value using class modifiers:
.table {
--table-layout: state-a;
}
@media (min-width: 600px) {
.table--state-b\@sm {
--table-layout: state-b;
}
}
@media (min-width: 1000px) {
.table--state-b\@md {
--table-layout: state-b;
}
}
Note that the class modifiers are now used to change the value of a CSS custom property; there's no repetition of layout style.
Using JavaScript, we can check whether to add or remove the .table--state-b
class based on the value of this CSS custom property. This will apply the proper layout style!
var layout = getComputedStyle(table).getPropertyValue('--table-layout');
table.classList.toggle('table--state-b', layout == 'state-b');
This technique allows us to use a single class (.table--state-b
) for the layout style, regardless of the media query. Adding a new variation only requires setting the value of a single CSS custom property. No code repetition involved!
In this example, we've been working with tables, but you can apply this technique to any component whose responsiveness is highly affected by its content.
Downsides? #
This approach requires JavaScript to work, but that should not be an issue: if JS is off, we serve the state-a version of the table, which is perfectly accessible.
What about CSS custom properties support? Using CSS variables is probably the cleanest and most self-explanatory way. But if you need to support older browsers (e.g., IE 11 and below) you can use a ::before
pseudo-element and change its content using the class modifiers:
.table::before {
display: none;
content: 'state-a';
}
@media (min-width: 600px) {
.table--state-b\@sm::before {
content: 'state-b';
}
}
@media (min-width: 1000px) {
.table--state-b\@md::before {
content: 'state-b';
}
}
In JS, you can check the value of the ::before
content rather than a custom property. Same result, different browser support! You can decide based on your needs.
That's it. I hope you find this approach useful. If you use a different technique, I would love to hear it!
Feedbacks/suggestions? Get in touch on Twitter.