At a recent meeting with the frontend developers we discussed the following question: "How should our components handle responsiveness?"
Here are the three main approaches:
- Components know how to be responsive, i.e. they can be responsive in isolation.
- Components are not responsive in isolation. Parent components control the responsiveness of their children.
- Some mixture of the two approaches above.
To really understand this question, I implemented a small portion of the homepage for gov.uk. My design makes simplifications where necessary so as to only focus on the responsiveness problem (ex. I do not have a magnifying glass icon in my search bar)
If you'd like to see my version click here.
My implementation has a single breakpoint at 769px, i.e. there are two main views a user can see. The layout for tablet devices and smaller or the layout for devices larger than tablets.
Now with a good understanding of what the final design should be, here are the rules/guidelines I set up for myself for the implementation:
- Use a design system to build the page. This means...
- Everything should fit to a grid (gov.uk is built on a 10px grid)
- Components that contain gov.uk data MUST be a composition of components from the design system. (i.e. creating one-off wrappers/components with styled-components is not allowed)[1]
- Applying custom CSS to a component is not strictly disallowed, but it is not encouraged (use props instead)
- Responsiveness must be done with CSS and not JS. (Using JS for responsiveness means the component cannot be rendered properly with SSR or static rendering)[2]
The design system I built is not a complete design system. I've only added the components that were required to build the page. Here is a list of the components with a short description of what each does.
Note: A lot of the inspiration for the props
that the components expose is inspired by the project rebass. This will be explained in more detail later.
A div that passes styles to it's children. For example color
, font-family
, etc.
The implementation of this component is inspired from this article about responsive layout patterns by Google. Read it if you haven't! It's short!
ColumnDrop
and Column
are used to have multiple columns next to each other in a row on larger screens, but vertically stacked columns on smaller screens.
A div
used for padding so components don't need to add padding themselves.
A div
used for margins so components don't need to add margins themselves.
I couldn't find a name for this pattern, but a super common pattern for web pages is to have a container of centered content whose width is the minimum between 100% of the viewport and a specified max-width (1020px for example, usually something that makes sense for desktops and prevents layouts from stretching too much on very large screens).
Component for typography. Using this component makes it easier to follow the defined typescale of the project.
Has the same props as Text
, but used for creating a menu with links inside.
A text input
Note: All of the code shown here is also inside of this repository, although you may need to look for the right commit.
Instead of jumping straight to the end result, we'll look at the code in stages. I started by implementing just the tablet and smaller layout.
// commit 265f47d0d953dd9dee682408a7073e12ca81c8b8
// message: refactor and move gov.uk stuff into components
// pages/index.js
<BaseStyles>
<WebpageLayout maxWidth='1020px' padding={[2.5, 2, 0, 2]}>
<Spacer bottom={3}>
<WelcomeSection />
</Spacer>
<PopularLinks />
</WebpageLayout>
</BaseStyles>
The code here is more or less straightforward. We're using WebpageLayout
as the main container for the content and inside we're rendering our two sections from gov.uk. First we have WelcomeSection
and then after PopularLinks
.
You might be asking yourself, "What is bottom
? Why does padding
accept an array? Why are we passing numbers to these props?"
Since Spacer
is only used for margins, the bottom
prop maps to the css rule margin-bottom
. Likewise, padding
maps to the css rule padding
. The reason why we pass numbers is because the components are aligned to our 10px grid. That means bottom={3}
will produce the rule margin-bottom: 30px;
and padding={[2.5, 2, 0, 2]}
will produce the rule padding: 25px 20px 0 20px;
Here's a gif of what we have so far:
Hmmm not so responsive. Let's take a look at the final design so we have an idea of the responsive behaviour we need to code.
After taking a close look at the gif above (or the screenshots shown in the beginning), here are the things that we can see are responsive:
- The following copy all appear larger on larger screens:
- Welcome to GOV.UK
- description after Welcome to GOV.UK
- Popular on GOV.UK
- link navigation after Popular on GOV.UK
- The Welcome section and the Popular Links section are stacked vertically on smaller screens and side-by-side on larger screens
- The Welcome section has no right margin on smaller screens and does have a right margin on larger screens
- The Popular Links section has only bottom padding on smaller screens and padding all around on larger screens
- The Popular Links section has a blue background on smaller screens and a black background on larger screens
First let's take a look at our non-responsive versions of WelcomeSection
and PopularLinks
.
// commit 265f47d0d953dd9dee682408a7073e12ca81c8b8
// message: refactor and move gov.uk stuff into components
// components/welcome-section.js
<Container className={className}>
<Spacer bottom={1.5}>
<Text size='size2' bold>Welcome to GOV.UK</Text>
</Spacer>
<Spacer bottom={2}>
<Text size='sizen1'>The best place to find government services and information</Text>
<Text size='sizen1' bold>Simpler, clearer, faster</Text>
</Spacer>
<Input placeholder='Search GOV.UK' />
</Container>
// commit 265f47d0d953dd9dee682408a7073e12ca81c8b8
// message: refactor and move gov.uk stuff into components
// components/popular-links.js
<Container className={className} bottom={2}>
<Spacer bottom={0.5}>
<Text size='sizen3'>Popular on GOV.UK</Text>
</Spacer>
<LinkMenu size='sizen2' bold>
<Link href='#'>Universal Jobmatch job search</Link>
<Link href='#'>Renew vehicle tax</Link>
<Link href='#'>Log in to student finance</Link>
<Link href='#'>Book your theory test</Link>
<Link href='#'>Personal tax account</Link>
</LinkMenu>
</Container>
Once again, these components are more or less straightforward. As a side note, the n in the values sizen1
, sizen2
, etc. stands for negative. So the size of the text is smaller than the base size which is represented with the value size0
.
Let's make these responsive!
commit 6b17f98802fa8b00f1c802c3bfe7887d94d43c1a
make text in welcome section responsive
--- a/components/welcome-section.js
+++ b/components/welcome-section.js
<Container className={className}>
<Spacer bottom={1.5}>
- <Text size='size2' bold>Welcome to GOV.UK</Text>
+ <Text breakpoint='tablet' size={['size2', 'size3']} bold>
+ Welcome to GOV.UK
+ </Text>
</Spacer>
<Spacer bottom={2}>
- <Text size='sizen1'>The best place to find government services and information</Text>
- <Text size='sizen1' bold>Simpler, clearer, faster</Text>
+ <Text breakpoint='tablet' size={['sizen1', 'size1']}>
+ The best place to find government services and information
+ </Text>
+ <Text breakpoint='tablet' size={['sizen1', 'size1']} bold>
+ Simpler, clearer, faster
+ </Text>
</Spacer>
<Input placeholder='Search GOV.UK' />
</Container>
commit 7ac7e53ee08f27b231711d1bd36ecd42bdbb2d78
make text in popular-links responsive
--- a/components/popular-links.js
+++ b/components/popular-links.js
<Container className={className} bottom={2}>
<Spacer bottom={0.5}>
- <Text size='sizen3'>Popular on GOV.UK</Text>
+ <Text breakpoint='tablet' size={['sizen3', 'sizen2']}>
+ Popular on GOV.UK
+ </Text>
</Spacer>
- <LinkMenu size='sizen2' bold>
+ <LinkMenu breakpoint='tablet' size={['sizen2', 'size0']} bold>
<Link href='#'>Universal Jobmatch job search</Link>
<Link href='#'>Renew vehicle tax</Link>
<Link href='#'>Log in to student finance</Link>
Try to soak in what's happening by just reading the diff. Though if it's confusing, here's a more detailed explanation of how the responsiveness works.
Our responsive components now accept a prop breakpoint
. In this case the value tablet
represents the media query @media screen and (min-width: 769px)
.
We've also changed size
to accept an array of strings. In this case, the first value represents the default size and the second value represents the size after the first breakpoint.
I.e. the combination of breakpoint='tablet'
and size={['size2', 'size3']}
would eventually produce some CSS that looks like this:
font-size: 32px;
line-height: 35px;
@media screen and (min-width: 769px) {
font-size: 48px;
line-height: 50px;
}
Fun fact, if we needed more breakpoints, all we have to do is add more breakpoints and more sizes! For example breakpoint={['phone', 'tablet']}
and size={['size1', 'size2', 'size3']}
would result in:
font-size: 16px;
line-height: 20px;
@media screen and (min-width: 421px) {
font-size: 32px;
line-height: 35px;
}
@media screen and (min-width: 769px) {
font-size: 48px;
line-height: 50px;
}
This concept of passing in a breakpoint and an array of values is the basis of how we're going to do responsiveness for any component. It's a super powerful concept! Unfortunately I'm not that inventive/clever and I stole this pattern from the project rebass.
Let's take a look at what we've got so far.
-
The following copy all appear larger on larger screens:Welcome to GOV.UKdescription after Welcome to GOV.UKPopular on GOV.UKlink navigation after Popular on GOV.UK
- The Welcome section and the Popular Links section are stacked vertically on smaller screens and side-by-side on larger screens
- The Welcome section has no right margin on smaller screens and does have a right margin on larger screens
- The Popular Links section has only bottom padding on smaller screens and padding all around on larger screens
- The Popular Links sections has a blue background on smaller screens and a black background on larger screens
As I mentioned above, this type of responsive layout is a well understood pattern. So I'll say it again, go read this article by Google!
Here's our homepage using the ColumnDrop
and Column
components.
commit 88cc4b61e7de912a2ab8d6a2e944901cf8c838a4
implement column drop
--- a/pages/index.js
+++ b/pages/index.js
<BaseStyles>
<WebpageLayout maxWidth='1020px' padding={[2.5, 2, 0, 2]}>
- <Spacer bottom={3}>
- <WelcomeSection />
- </Spacer>
- <PopularLinks />
+ <ColumnDrop breakpoint='tablet'>
+ <Column width='66.66%'>
+ <Spacer bottom={3}>
+ <WelcomeSection />
+ </Spacer>
+ </Column>
+ <Column width='33.33%'>
+ <PopularLinks />
+ </Column>
+ </ColumnDrop>
</WebpageLayout>
</BaseStyles>
Ok so there should be some familiar concepts going on here. It's our ColumnDrop
component that accepts a breakpoint
prop, but now it's the ColumnDrop's children who need to be responsive. That is, we've wrapped the WelcomeSection
and PopularLinks
components with the Column
components and the width of the columns will change depending on the screen size.
This is done with the prop width
inside of the Column
components. It's assumed that before the breakpoint we want width: 100%
and that after the breakpoint we'll use the value inside of the width
prop.
WARNING: This API changes in later commits, but I was too lazy to go back and clean this up. The main concepts don't really change though. You'll see the code later so hopefully it's not a big deal.
If we look at our first Column
component, this is the CSS that would be produced:
width: 100%;
@media screen and min-width(769px) {
width: 66%;
}
And here's how it looks:
-
The following copy all appear larger on larger screens:Welcome to GOV.UKdescription after Welcome to GOV.UKPopular on GOV.UKlink navigation after Popular on GOV.UK
-
The Welcome section and the Popular Links section are stacked vertically on smaller screens and side-by-side on larger screens - The Welcome section has no right margin on smaller screens and does have a right margin on larger screens
- The Popular Links section has only bottom padding on smaller screens and padding all around on larger screens
- The Popular Links sections has a blue background on smaller screens and a black background on larger screens
These last few responsive properties are a bit tricky to implement. I would make the argument that the last three items in our responsive TODO list are neither responsive properties of WelcomeSection
/PopularLinks
nor a responsive layout. Instead, they are responsive properties of the containers that wrap WelcomeSection
/PopularLinks
.
Does this mean we need a responsive Container
component? Well... we have this already! Column
is already acting as our wrapper and it's already responsive. We just need to pass in some props so that it can change other things like margin
, padding
, and background-color
based off the screen size.
commit 7605e975d017546339e2b6503134a7011ad0b070
use responsive properties in gov.uk layout
--- a/pages/index.js
+++ b/pages/index.js
<BaseStyles>
<WebpageLayout maxWidth='1020px' padding={[2.5, 2, 0, 2]}>
<ColumnDrop breakpoint='tablet'>
- <Column width='66.66%'>
+ <Column
+ size={['full', '66.66%']}
+ margin={[
+ [0],
+ [0, 3, 0, 0]
+ ]}
+ >
<Spacer bottom={3}>
<WelcomeSection />
</Spacer>
</Column>
- <Column width='33.33%'>
+ <Column
+ size={['full', 'remaining']}
+ padding={[
+ [0, 0, 2, 0],
+ [2]
+ ]}
+ margin={[
+ [0],
+ [0.5, 0, 0, 0]
+ ]}
+ responsiveCSS={['', 'background-color: black;']}
+ >
<PopularLinks />
</Column>
</ColumnDrop>
Before we break down what's going on with these props, it might be helpful to start by just looking at the CSS that would be produced. Let's take a look at the CSS for the second column:
width: 100%;
margin: 0;
padding: 0 0 20px 0;
@media screen and min-width(769px) {
flex: 1;
margin: 5px 0 0 0;
padding: 20px;
background-color: black;
}
Hopefully this is all coming together now, but just in case we'll talk about the props in more detail.
To start, you'll notice that I've made some changes to the width
prop. I've changed it so it accepts an array of values, this is so it is consistent with how all the other responsive props work. I've also renamed the prop from width
to size
. The reason for this is because I think that if a prop shares the same name as a CSS property i.e. width
, then it's behaviour should more or less be identical.
In this case, size
can accept values 'full'
or 'remaining'
which don't have a meaning in traditional CSS but in this case full
translates to width: 100%;
and remaining
to flex: 1;
.
The other responsive props we have are padding
, margin
, and responsiveCSS
. padding
and margin
should be understandable based off of what we've already seen.
But why have we put the implementation for changing the background-color
inside of responsiveCSS
?
The reason for that is because Column
is inherently supposed to be just a container/wrapper. For me, paddings and margins are the responsibility of a container. As well, we want all of our paddings and margins to conform to our 10px grid, so it's nice that we can pass numbers that represent the coefficients.
responsiveCSS
is supposed to be an escape hatch of sorts. There are inevitably going to be different style rules you want to apply at different screen sizes, but it might not make sense to have a prop for each one of these.
Imagine if our Column
component could accept props like backgroundColor
, boxShadow
, display
, etc. The list goes on and on. Basically you would keep adding new props everytime you needed to control a different CSS property. In my opinion it's easier to just provide this escape hatch and tell developers to use it with caution. And that 90% of the time the components in our design system should allow you to accomplish the responsive behaviour you need with just props.
You've seen this before, but let's take a look again at the gif of our final implementation :)
-
The following copy all appear larger on larger screens:Welcome to GOV.UKdescription after Welcome to GOV.UKPopular on GOV.UKlink navigation after Popular on GOV.UK
-
The Welcome section and the Popular Links section are stacked vertically on smaller screens and side-by-side on larger screens -
The Welcome section has no right margin on smaller screens and does have a right margin on larger screens -
The Popular Links section has only bottom padding on smaller screens and padding all around on larger screens -
The Popular Links sections has a blue background on smaller screens and a black background on larger screens
Awesome we did it! It's totally responsive! And we followed all of the rules/guidelines! That means that we have a design system that is comprehensive enough to allow you to implement this (partial) page!
Let's take a look again at the three approaches we said we could use:
- Components know how to be responsive, i.e. they can be responsive in isolation.
- Components are not responsive in isolation. Parent components control the responsiveness of their children.
- Some mixture of the two approaches above.
I would argue that our implementation is either approach number 1 or 3. It's certainly not approach number 2. We're using Text
as a responsive component both inside WelcomeSection
and PopularLinks
. If we wanted our implementation to be even more approach number 1ish, we could change background-color
between blue and black inside PopularLinks
.
But let's take a look at these more closely.
Refactoring our code to be more approach 1ish isn't so difficult.
--- i/pages/index.js
+++ w/pages/index.js
<BaseStyles>
<WebpageLayout maxWidth='1020px' padding={[2.5, 2, 0, 2]}>
<ColumnDrop breakpoint='tablet'>
<Column
size={['full', '66.66%']}
margin={[
[0],
[0, 3, 0, 0]
]}
>
<Spacer bottom={3}>
<WelcomeSection />
</Spacer>
</Column>
<Column
size={['full', 'remaining']}
- padding={[
- [0, 0, 2, 0],
- [2]
- ]}
margin={[
[0],
[0.5, 0, 0, 0]
]}
- responsiveCSS={['', 'background-color: black;']}
>
<PopularLinks />
</Column>
</ColumnDrop>
</WebpageLayout>
</BaseStyles>
--- i/components/popular-links.js
+++ w/components/popular-links.js
- <Container className={className}>
+ <Container
+ className={className}
+ breakpoint='tablet'
+ padding={[
+ [0, 0, 2, 0],
+ [2]
+ ]}
+ responsiveCSS={['', 'background-color: black;']}
+ >
<Spacer bottom={0.5}>
<Text breakpoint='tablet' size={['sizen3', 'sizen2']}>
Popular on GOV.UK
</Text>
</Spacer>
<LinkMenu breakpoint='tablet' size={['sizen2', 'size0']} bold>
<Link href='#'>Universal Jobmatch job search</Link>
<Link href='#'>Renew vehicle tax</Link>
<Link href='#'>Log in to student finance</Link>
<Link href='#'>Book your theory test</Link>
<Link href='#'>Personal tax account</Link>
</LinkMenu>
</Container>
This is fine. In fact, I don't have a responsive Container
component implemented, this is just how the usage would look. What this comes down to is, "is the background-color inherently a part of the components design?"
If the answer is yes, then maybe this is a good choice. If the answer is no, for example we plan on reusing this component but with a green background, then this component would need to be refactored.
We also needed to move the padding into PopularLinks
as well to get this to work, which might also not be ideal. Again, it just depends on how you interpret the responsibilities of the component.
The refactor here also isn't so much work. For demonstrative purposes I've chosen to show only WelcomeSection
.
--- i/pages/index.js
+++ w/pages/index.js
<BaseStyles>
<WebpageLayout maxWidth='1020px' padding={[2.5, 2, 0, 2]}>
<ColumnDrop breakpoint='tablet'>
<Column
size={['full', '66.66%']}
margin={[
[0],
[0, 3, 0, 0]
]}
>
<Spacer bottom={3}>
- <WelcomeSection />
+ <WelcomeSection
+ breakpoint='tablet'
+ titleSize={['size2', 'size3']}
+ descriptionSize={['sizen1', 'size1']}
+ />
</Spacer>
</Column>
<Column
size={['full', 'remaining']}
padding={[
[0, 0, 2, 0],
[2]
]}
margin={[
[0],
[0.5, 0, 0, 0]
]}
responsiveCSS={['', 'background-color: black;']}
>
<PopularLinks />
</Column>
</ColumnDrop>
</WebpageLayout>
</BaseStyles>
--- i/components/welcome-section.js
+++ w/components/welcome-section.js
-const WelcomeSection = ({ className }) => (
+const WelcomeSection = ({
+ className,
+ breakpoint,
+ titleSize,
+ descriptionSize
+}) => (
<Container className={className}>
<Spacer bottom={1.5}>
- <Text breakpoint='tablet' size={['size2', 'size3']} bold>
+ <Text breakpoint={breakpoint} size={titleSize} bold>
Welcome to GOV.UK
</Text>
</Spacer>
<Spacer bottom={2}>
- <Text breakpoint='tablet' size={['sizen1', 'size1']}>
+ <Text breakpoint={breakpoint} size={descriptionSize}>
The best place to find government services and information
</Text>
- <Text breakpoint='tablet' size={['sizen1', 'size1']} bold>
+ <Text breakpoint={breakpoint} size={descriptionSize} bold>
Simpler, clearer, faster
</Text>
</Spacer>
This wasn't so bad, but I would argue that this type of solution is not very scalable. Let's say we also needed to make the spacing between the title and the description responsive.
const WelcomeSection = ({
className,
breakpoint,
titleSize,
- descriptionSize
+ descriptionSize,
+ titleDescriptionSpacing
}) => (
<Container className={className}>
- <Spacer bottom={1.5}>
+ <Spacer breakpoint={breakpoint} bottom={titleDescriptionSpacing}>
<Text breakpoint={breakpoint} size={titleSize} bold>
Welcome to GOV.UK
</Text>
</Spacer>
<Spacer bottom={2}>
<Text breakpoint={breakpoint} size={descriptionSize}>
The best place to find government services and information
</Text>
<Text breakpoint={breakpoint} size={descriptionSize} bold>
Simpler, clearer, faster
</Text>
</Spacer>
<Input placeholder='Search GOV.UK' />
</Container>
)
We've added a third prop that needs to be controlled from the outside but it's starting to feel a bit weird. Do we really want to control the spacing between the title and the description from the outside like this? What if we were implementing a component that has a lot more responsive behaviour? You could imagine our props like growing and growing.
The other problem with controlling responsiveness from the outside is that it makes it a lot more difficult for designers to use the component. How would you explain to a designer, "There's no mobile version of this component. Well there is... because it depends on where it is rendered... but it's the parent who controls how it is rendered. Anyways if you want to preview the mobile version of this component you should render it with these props."
I watched a youtube video recently about how USA Today created their design system.
There's a slide in the presentation that explains how modules/components in their design system will give them better reusability.
- Smarter Modules:
- To do the same job in different places
- To do the same job across use cases
- Smarter Styles:
- To use across these modules and keep them cohesive
This is exactly what we're trying to do with our design system! Later in the presentation, the speaker mentions the documentation they've created for their modules. Specifically, here are the questions that the documentation must answer:
- What is it called?
- What is it made out of?
- What variants are needed?
- How does it scale?
- What style variables are in use?
So you could imagine answering some of these questions already for the components we created in our implementation of gov.uk. There is however one of these questions that becomes a lot more difficult to answer, "How does it scale?". The only reasonable way to answer this question is if the component itself knows how it should scale.
Making a design system that supports responsive components isn't trivial. But to answer the original question, "How should our components handle responsiveness?"
I would say that the design system should have atoms and layouts that make it easy to code responsive behaviour. From there we want to build components that know how to be responsive, but we can selectively choose what responsive behaviour the component is responsible for.
So if you made it this far thank you!
One thing I didn't mention in this post was the implementation of the components in the design system that can be responsive. The code in this repository isn't the prettiest and needs a bit of refactoring, overall though I'd say it's solidish. Anyways what we really care about are the APIs that the components expose more so than if the implementation is perfect (implementations may change in the future anyways).
If this stuff interests you I encourage you to try and implement the responsive components yourself first! This also gives you an opportunity to think about what API you want to expose and the different choices you have to make.
- Why is it bad to create one-off components? For me this is a sign that the design system is not working well. It means that the design system is not supporting something you want to do, so you have to create it yourself.
- I am defining "using JS for responsivess" as "using the
window
object to implement render logic depending on the screen size". This does not work in SSR nor static rendering because, in a server or in a static build, you do not have access to thewindow
object.