Responsive Styling in React
The Basics
How do we change our site to look good on all devices and screen sizes? There are three main
techniques:
- Use flex and relative dimensions
- Rendering different content for different screen sizes
- Changing the style of content based on the screen size
For techniques 2-3, we need to know the size of the user's screen. In our application,
we use the screen's width to determine if the user is on a mobile device, tablet, narrow desktop,
or desktop. These options are represented by the WindowTypes enum:
| export enum WindowTypes {
Mobile = 'MOBILE',
Tablet = 'TABLET',
NarrowDesktop = 'NARROW',
Desktop = 'DESKTOP',
}
|
and by our constant breakpoints:
| export const BREAKPOINT_DESKTOP = 1300;
export const BREAKPOINT_TABLET = 1025;
export const BREAKPOINT_MOBILE = 680;
|
where window types are defined as follows:
| const windowType: WindowTypes =
width < BREAKPOINT_MOBILE
? WindowTypes.Mobile
: width < BREAKPOINT_TABLET
? WindowTypes.Tablet
: width < BREAKPOINT_DESKTOP
? WindowTypes.NarrowDesktop
: WindowTypes.Desktop;
|
1. Use flex and relative dimensions
Flex
The CSS display: flex property will be your best friend. A flex container alters its content's
height, width, and/or position to fit the available space. I could explain it, but
this guide is pretty much all you'll
need and something I 100% recommend always having on hand.
I recommend using flex over other layout solutions like AntD's
grid because it's more flexible and a lot simpler in the
code, but be aware other solutions exist if flex isn't working for what you're working on.
Relative Dimensions
Use standard dimensions like px whenever it looks reasonable to keep things simple, but relative
dimensions can help you scale content to look appropriate for the screen. In particular, be aware
that you can use:
| Unit |
Description |
vw |
1vw = 1% of the browser window's width |
vh |
1vh = 1% of the browser window's height |
vmin |
1vmin = 1% of the browser window's smaller dimension (of width and height) |
vmax |
1vmax = 1% of the browser window's larger dimension (of width and height) |
% |
Percentage of the parent element's respective dimension |
2. Rendering different content for different screen sizes: useWindowDimensions
To demonstrate how to render different content for different window types, let's build a component
that renders what window type the user is on. We'll show a h1 on desktop, h2 on narrow desktop,
h3 on tablet, and h4 on mobile.
First, we can get the user's window type with the useWindowDimensions
hook, located under src/components/windowDimensions.
To use the hook, simply add the following line to your component:
| const { windowType } = useWindowDimensions();
|
Note
The useWindowDimensions hook also gives us access to the window's exact width and height, but
in most cases the window type will be sufficient.
| const { width, height, windowType } = useWindowDimensions();
|
Second, we can change what to render in our React component with a switch statement:
1
2
3
4
5
6
7
8
9
10
11
12 | {(() => {
switch (windowType) {
case WindowTypes.Mobile:
return <h4>You're on mobile!</h4>
case WindowTypes.Tablet:
return <h3>You're on tablet!</h3>
case WindowTypes.NarrowDesktop:
return <h2>You're on narrow desktop!</h2>
case WindowTypes.Desktop:
return <h1>You're on desktop!</h1>
}
})()}
|
so our whole component would look something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | const WebPage: React.FC = () => {
// get the window type
const { windowType } = useWindowDimensions();
return (
<>
{/* content that renders on all screens */}
<h1>Welcome to the site that tells you what window type you're on!</h1>
{/* content that renders based on window type */}
{(() => {
switch (windowType) {
case WindowTypes.Mobile:
return <h4>You're on mobile!</h4>
case WindowTypes.Tablet:
return <h3>You're on tablet!</h3>
case WindowTypes.NarrowDesktop:
return <h2>You're on narrow desktop!</h2>
case WindowTypes.Desktop:
return <h1>You're on desktop!</h1>
}
})()}
</>
);
}
|
Note
Switch statements are just one way to conditionally render content in React based on windowType.
See here for other ways that will allow
you to do different things, such as only showing a component on desktop.
This was a pretty basic example, but in general we'll do this sort of responsive rendering to show
different content or use different layouts for different window types.
3. Changing the style of content based on the screen size: CSS @media queries
When we're just changing the style of components instead of what components are rendering, we
use the CSS @media rule. In general, always try to do this over using useWindowDimensions
(we'll explain why later).
To demonstrate, let's build a page that renders small text (10px) on mobile and medium text
(16px) on everything else.
First, let's build our page for everything except mobile.
| const StyledParagraph = styled.p`
font-size: 16px;
`;
const WebPage: React.FC = () => {
return (
<>
<StyledParagraph>Welcome to our amazing webpage!</StyledParagraph>
</>
);
}
|
Note
We're using styled-components in this example and in our
project, but @media rules work whenever you're using CSS.
Now, to have our StyledParagraph be 10px on mobile, we just have to add an @media rule in the
CSS of our styled component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | const StyledParagraph = styled.p`
font-size: 16px;
/* on any devices with a width less than BREAKPOINT_MOBILE... */
@media (max-width: ${BREAKPOINT_MOBILE}px) {
/* ...apply the following styles */
font-size: 10px;
}
`;
const WebPage: React.FC = () => {
return (
<>
<StyledParagraph>Welcome to our amazing webpage!</StyledParagraph>
</>
);
}
|
And that's it! There's a lot more you can do with CSS media queries, which you can explore
here.
It is possible to use our useWindowDimensions hook instead of CSS media queries to do what we
just did above by using props in our styled component:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | interface StyledParagraphProps {
readonly isMobile: boolean;
}
const StyledParagraph = styled.p`
font-size: ${({ isMobile }: StyledParagraphProps) => (isMobile ? '10' : '16')}px;
`;
const WebPage: React.FC = () => {
// get the window type
const { windowType } = useWindowDimensions();
const isMobile = windowType === WindowTypes.Mobile;
return (
<>
<StyledParagraph isMobile={isMobile}>Welcome to our amazing webpage!</StyledParagraph>
</>
);
}
|
or
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | interface StyledParagraphProps {
readonly fontSize: string;
}
const StyledParagraph = styled.p`
font-size: ${({ fontSize }: StyledParagraphProps) => fontSize};
`;
const WebPage: React.FC = () => {
// get the window type
const { windowType } = useWindowDimensions();
const fontSize = (windowType === WindowTypes.Mobile) ? '10px' : '16px';
return (
<>
<StyledParagraph fontSize={fontSize}>Welcome to our amazing webpage!</StyledParagraph>
</>
);
}
|
etc. However, we prefer to use media queries because passing props around can get pretty messy. From
the examples above, we can see that it's a lot more readable and quicker to use media queries.
Further, following React best practices of having
smart and dumb components,
we only want to use the useWindowDimensions hook in our smart components (in our project, our
containers). This means that we'd have to pass down a windowType or isMobile or similar prop to
every component that we want to have it, which is pretty ridiculous if a component four levels down
is the only one that needs it. Just to drive home the point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 | // ComponentFour.tsx
interface StyledParagraphProps {
readonly isMobile: boolean;
}
const StyledParagraph = styled.p`
font-size: ${({ isMobile }: StyledParagraphProps) => (isMobile ? '10' : '16')}px;
`;
const ComponentFour: React.FC<StyledParagraphProps> = ({ isMobile }) => {
return (
<StyledParagraph isMobile={isMobile}>The thing that needs to be styled</StyledParagraph>
);
}
// ComponentThree.tsx
interface ComponentThreeProps {
readonly isMobile: boolean;
}
const ComponentThree: React.FC<ComponentThreeProps> = ({ isMobile }) => {
return (
<>
<p>Component 3 content</p>
<ComponentFour isMobile={isMobile} />
</>
);
}
// ComponentTwo.tsx
interface ComponentTwoProps {
readonly isMobile: boolean;
}
const ComponentTwo: React.FC<ComponentTwoProps> = ({ isMobile }) => {
return (
<>
<p>Component 2 content</p>
<ComponentThree isMobile={isMobile} />
</>
);
}
// ComponentOne.tsx
interface ComponentOneProps {
readonly isMobile: boolean;
}
const ComponentOne: React.FC<ComponentOneProps> = ({ isMobile }) => {
return (
<>
<p>Component 1 content</p>
<ComponentTwo isMobile={isMobile} />
</>
);
}
// WebPage.tsx
const WebPage: React.FC = () => {
// get the window type
const { windowType } = useWindowDimensions();
const isMobile = windowType === WindowTypes.Mobile;
return (
<>
<h1>Welcome to our amazing webpage!</h1>
<ComponentOne isMobile={isMobile} />
</>
);
}
|
versus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 | // ComponentFour.tsx
const StyledParagraph = styled.p`
font-size: 16px;
@media (max-width: ${BREAKPOINT_MOBILE}px)
font-size: 10px;
}
`;
const ComponentFour: React.FC = () => {
return (
<StyledParagraph>The thing that needs to be styled</StyledParagraph>
);
}
// ComponentThree.tsx
const ComponentThree: React.FC = () => {
return (
<>
<p>Component 3 content</p>
<ComponentFour />
</>
);
}
// ComponentTwo.tsx
const ComponentTwo: React.FC = () => {
return (
<>
<p>Component 2 content</p>
<ComponentThree />
</>
);
}
// ComponentOne.tsx
const ComponentOne: React.FC = () => {
return (
<>
<p>Component 1 content</p>
<ComponentTwo />
</>
);
}
// WebPage.tsx
const WebPage: React.FC = () => {
return (
<>
<h1>Welcome to our amazing webpage!</h1>
<ComponentOne />
</>
);
}
|
which leaves space for the props that matter.
A more complicated example: Login Page
So far our examples have been pretty basic. Our login container shows a more interesting way of
using the techniques we've described so far. (You can play with resizing the page
here).
On desktops, we show our standard design:

We have less whitespace on narrow desktop:

The blocks stack on tablet:

And we have a much simpler design for mobile:

Let's look at the code to see how it works (narrowing in on the code that matters for styling).
First, for WindowTypes.Desktop, WindowTypes.NarrowDesktop, and WindowTypes.Tablet, we
render the same blocks and have mostly the same styling, but we use flex to make the grey and
green boxes appear reasonably on the screen. We also use relative dimensions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 | const InputGreetingContainer = styled.div`
width: 100vw; /* use relative dimensions to make the boxes as wide as the screen */
/* make the boxes as tall as the parent container (the screen minus the nav bar), or 575px
(whichever is taller). this way, if the screen is short, we can scroll down to see the rest
of the content instead of making the content look too short. */
min-height: 575px;
height: 100%;
display: flex; /* use flex to responsively position the boxes */
/* by using wrap, the green box will be below the grey on tablet,
ensuring the boxes never get too skinny */
flex-wrap: wrap;
/* center the boxes in the screen and parent container */
align-items: center;
justify-content: space-around;
align-content: center;
/* define the gap between the boxes */
gap: 20px;
`;
const Login: React.FC = () => {
return (
<InputGreetingContainer>
{/* grey box */}
<InputContainer>
<Title>{LOGIN_TITLE}</Title>
<Line />
<LoginForm
formInstance={loginForm}
onFinish={onFinish}
windowType={windowType}
/>
{ForgotPasswordFooter}
</InputContainer>
{/* green box */}
<GreetingContainer
header={LOGIN_HEADER}
body={LOGIN_BODY}
/>
</InputGreetingContainer>
);
}
|
We also add a media rule to InputGreetingContainer to make sure the boxes are big enough on
tablets (since the paragraphs are not as wide, they'll need to have more lines, making them taller
than on desktops).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | export const InputGreetingContainer = styled.div`
width: 100vw;
min-height: 575px;
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
align-content: center;
gap: 20px;
/* make sure the boxes are tall enough to fit all content on tablets */
@media (max-width: ${BREAKPOINT_TABLET}px) {
min-height: 900px;
}
`;
|
Finally, for mobile we'll use useWindowDimensions to not render the boxes at all:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 | export const InputGreetingContainer = styled.div`
width: 100vw;
min-height: 575px;
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
align-content: center;
gap: 20px;
/* make sure the boxes are tall enough to fit all content on tablets */
@media (max-width: ${BREAKPOINT_TABLET}px) {
min-height: 900px;
}
`;
const Login: React.FC = () => {
const { windowType } = useWindowDimensions();
return (
<>
{(() => {
switch (windowType) {
case WindowTypes.Mobile:
return (
<MobileLoginPageContainer>
<PageHeader pageTitle={LOGIN_TITLE} isMobile={true} />
<LoginForm
formInstance={loginForm}
onFinish={onFinish}
windowType={windowType}
/>
{ForgotPasswordFooter}
</MobileLoginPageContainer>
);
case WindowTypes.Tablet:
case WindowTypes.NarrowDesktop:
case WindowTypes.Desktop:
return (
<PageLayout>
<InputGreetingContainer>
<InputContainer>
<Title>{LOGIN_TITLE}</Title>
<Line />
<LoginForm
formInstance={loginForm}
onFinish={onFinish}
windowType={windowType}
/>
{ForgotPasswordFooter}
</InputContainer>
<GreetingContainer
header={LOGIN_HEADER}
body={LOGIN_BODY}
/>
</InputGreetingContainer>
</PageLayout>
);
}
})()}
</>
);
};
|
Congrats! You've made it to the end 