Introduction
The styling of your website or web application is an important part of creating a good user experience. You cannot just ship HTML and JavaScript and expect the app to look decent. When it comes to styling a React app, there are plenty of options to choose from.
-
the CSS way:
a. good old fashioned CSS (inline,<style>
tag or linked stylesheets)
b. using modern build tools for the adventurous - CSS Modules -
the JavaScript way:
a. inline styling using the Object-based syntax provided out of the box with React
b. choose from a plethora (yes, that many) of CSS-in-JS libraries.
The CSS-in-JS scenario has been a controversial one at times but no serious developer can deny that they are worth exploring, that's why we will explore the basic APIs and usage of styled-components (one of the most widely used of the available options) by creating a trivial app that displays a list of "Random People" and some info about them. This is just enough to get you started with styled-components by highlighting some of the common real-world use cases.
We will not be going through every bit of the app in detail but you can view the live version of it on Codesandbox and fork the repo on Github. We will be focusing solely on the implementation related to styled-components throughout the app.
Note - The reader is expected to have prior experience of working with React. You can go through Introduction to React for a quick refresher.
Using styled-components
We will be using Codesandbox for bootstrapping the React app which uses the create-react-app under the hood. Once the setup is done we can get started by installing the npm package for styled-components.
$ npm i styled-components
No extra build steps or configurations are needed for styled-components to work. This makes getting started with the library very easy.
Styling in styled-components follows a syntax identical to CSS. It varies slightly for things like keyframes and media queries. We will explore them in details separately. The CSS like syntax uses ES6 Tagged Template Literals. The style rules we write as such are used to generate a CSS stylesheet with unique class names for each set of rules, and attaches those classes to their respective DOM nodes of the components. The generated stylesheet is injected at the end of the <head>
section of the HTML document during runtime by default.
styled
Main.js
import styled from 'styled-components';
const Main = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
align-content: flex-start;
`;
export default Main;
styled
is the default export of the styled-components. All the DOM elements are present as methods on the styled
object. In the snippet above, we are creating the Main
component which acts as the parent container for all the components other than our Header
. Here, we use styled.div
to create a styled component that renders a <div>
. The CSS syntax inside the backticks utilizes common flexbox-based layout code. This is the simplest way to create a styled component. If you go through the source code in the repo, you will come across multiple usages of this API like styled.img
, styled.button
, etc.
Loader.styled-as-function.js
/* src/components/Loader.js */
.
.
.
/* line 6 */
const Loader = ({ className }) => (<div className={className}><Loading/></div>)
.
.
.
/* line 41 - 46 */
export default styled(Loader)`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;
styled
can also be invoked as a function (factory call) itself to wrap an existing React component as a styled component. In the above example, we create the Loader
as a simple stateless React component and then call the styled
factory function with Loader
as an argument to wrap it with the necessary styles. Actually, styled.div
is also just an alias for styled('div')
so it can be used with HTML tags directly as well.
One important thing to note when using the styled as a factory function to wrap a React component is to manually set the className
attribute on the DOM elements to this.props.className
or else the style rules will not take effect.
Keyframes, pseudo-elements and pseudo-selectors
Loader.keyframes-and-pseudo-elements.js
/* src/components/Loader.js */
/* line 1 - 39 */
import React from 'react';
import styled, { keyframes } from 'styled-components';
import { getColor } from '../utils/theme'
const Loader = ({ className }) => (<div className={className}><Loading/></div>)
const loading = keyframes`
0% {
transform: rotate(0);
} 100% {
transform: rotate(360deg);
}
`;
const Loading = styled.div`
color: transparent;
min-height: 2rem;
pointer-events: none;
position: relative;
&::after {
content: "";
animation: ${loading} .5s infinite linear;
border: .1rem solid ${getColor('primary')};
border-radius: 50%;
border-right-color: transparent;
border-top-color: transparent;
display: block;
z-index: 1;
left: 50%;
position: absolute;
top: 50%;
height: 1.6rem;
width: 1.6rem
margin-left: -.8rem;
margin-top: -.8rem;
}
`;
.
.
.
We are creating a simple animated Loader
component using keyframes and pseudo-element. styled-components export a keyframes
helper function as a named export that is used to create animation keyframes. It also generates keyframes rules with unique names, just like the class names so that we don't have any global name clashes on the generated stylesheet. The API is similar to that of styled.tagname
and the generated keyframes object is then used in the animation property for the style rules, as you can see on line 29 in the gist.
The getColor
utility function is just used to extract color values from the theme. We will explore the details of that shortly when we talk about theming in styled-components.
For the pseudo-elements, the syntax is more like what is used in SCSS. &
refers to the current selector scope and can also be used for nested rules just like in SCSS. The rules under ::after
defines the style for the after pseudo-element of the Loader <div>
. Remember, just like in CSS, content
property needs to set (even if to an empty string ''
) for the pseudo-element to be valid.
Similarly, pseudo-selectors like :hover
, :active
, etc. can also be used in styled-components. In the User
component style rules, you can see the transition
rules are similar to their CSS equivalents.
index.pseudo-selector.js
/* index.js */
/* line 11 - 31 */
.
.
.
`* {
border: none;
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
width: 100%;
}
a,
a:hover,
a:active,
a:visited {
text-decoration: none;
color: inherit;
}`;
.
.
.
User.pseudo-selector.js
/* src/User.js */
.
.
.
/* line 40 - 57 */
export default styled(User)`
width: 280px;
margin: 10px;
padding: 10px;
border: solid 2px ${getColor('secondary')};
border-radius: 70px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
transition: all 0.25s ease-in;
position: relative;
background: ${getColor('secondary')};
&:hover {
transform: scale(1.05);
}
`;
Using props and the css helper
Interpolated functions can be used inside in the tagged template literals. This function is passed all the props available to the component. styled-components pass on all their props and hence the props can be used to customize or alter style rules dynamically. In this Button
component, we use the css
helper function to create an absolutely positioned button but only if it has a floating
prop. The css
helper function allows the generation of valid CSS using template literals within interpolations, meaning it is used to generate CSS using ``
when inside ${}
wrapped inside another ``
(so meta).
Button.js
/* src/components/Button.js */
import styled, { css } from 'styled-components';
import { getFontSize, getColor } from '../utils/theme';
const Button = styled.button`
font-size: ${getFontSize('smFont')};
color: ${getColor('primary')};
background: ${getColor('light')};
border-radius: 5px;
width: 150px;
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
align-self: center;
height: 30px;
border: solid 1px ${getColor('primary')};
${props =>
props.floating &&
css`
position: absolute;
bottom: 10px;
left: 50%;
transform: translate(-50%, 0);
`};
`;
export default Button;
User.js
/* src/components/User.js */
.
.
.
/* line 16 - 30 */
const maleBorder = props =>
props.gender === 'male' ? `solid 4px ${props.theme.color.primary}` : '0';
const femaleBorder = props =>
props.gender === 'female' ? `solid 4px ${props.theme.color.primary}` : '0';
const Avatar = styled.img`
border-radius: 50%;
width: 60px;
height: 60px;
position: relative;
border-bottom: ${femaleBorder};
border-left: ${femaleBorder};
border-right: ${maleBorder};
border-top: ${maleBorder};
`;
.
.
.
In the Avatar
styled-component, props.gender
is used to customize the border of the <img>
tag. For female users there will be border on the left and bottom side but not on the top and right side and vice versa for the male users. The props.theme.color.primary
is the primary color of our theme, the code gist in theming section sheds more light on it.
extending styles
Sometimes, changing styles based on props is not enough and we may need to generate multiple variants of a single component. We can easily do that by calling the extend
method on that component.
UserProfile.extend.js
/* src/components/UserProfile.js */
.
.
.
/* line 74 - 83 */
const Value = styled.span`
font-size: ${getFontSize('tn')};
color: ${getColor('dark')};
padding: 5px;
`;
const Label = Value.extend`
font-weight: bold;
color: ${getColor('primary')};
`;
.
.
.
Here, we need two <span>
elements with similar styles, so we extend Value
(an existing styled.span
) to create Label
which has a bolder font and different font color.
Media queries
Modern sites need to be responsive and for that we need media queries. We will use media queries to adapt our UserProfile
and its constituent components for different screen sizes.
UserProfile.js
/* src/components/UserProfile.js */
.
.
.
/* line 44 */
const breakPoint = '600px';
.
.
.
/* line 53 - 65 */
const Details = styled.div`
width: 350px;
height: 200px;
display: row;
margin: 10px;
jusify-content: center;
align-items: flex-start;
color: black;
@media (max-width: ${breakPoint}) {
width: 280px;
}
`;
.
.
.
/* line 85 - 99 */
export default styled(UserProfile)`
padding: 10px;
margin: 40px 0;
display: flex;
position: relative;
background: ${getColor('secondary')};
border-radius: 5px;
box-sizing: border-box;
@media (max-width: ${breakPoint}) {
flex-direction: column;
justify-content: center;
align-items: center;
}
`;
When the screen width is lower than 600px
, we change the flex-direction
and other alignments rules of the UserProfile
component and lower the width of the Details
component from 350px
to 280px
.
Global styles
Sometimes, we need to add global styles for use cases like negating the default link anchor <a>
styles or styling the <body>
element. The styled-components library provides a helper function as named export for that too - injectGlobal
can be used to add global styles using the now familiar tagged template literal syntax.
index.global-styles.js
/* src/index.js */
.
.
.
/* line 10 - 32 */
injectGlobal`
* {
border: none;
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
width: 100%;
}
a,
a:hover,
a:active,
a:visited {
text-decoration: none;
color: inherit;
}
`;
.
.
.
Theming
The styled-components have theming as a first-class feature because it is very common for web apps to have a uniform theme (like colors, typography, etc.). It exports a ThemeProvider
wrapper component that accepts a theme
props. It accepts an object holding values for theming purposes, which in our case here are colors and font sizes. It injects this theme
props to all the styled components using the context API under the hood. If you need the theme
props outside of the context of any styled components, there is also a higher-order component withTheme
(just like withRouter
in react-router-dom) available for that.
In our theme.js file, we create a theme
object with various color
and fontSize
values. We also create a function invertTheme
. The ThemeProvider
can also be passed a function for something like contextual theming. The function receives the theme
object as a parameter from a parent ThemeProvider
i.e. any other ThemeProvider
that is higher up the tree. We also have two curried functions to help us extract the color and font size values from the theme
props. These two are the functions that have been popping up in the style rules of other components. These are simple functions that accept the props argument and return the desired value extracted from it.
App.js
/* src/App.js */
import styled, { ThemeProvider } from 'styled-components';
.
.
.
import { getUsers } from './utils/apiCalls';
import { invertTheme, getColor } from './utils/theme';
class AppContainer extends Component {
state = {
users: [],
themeInverted: false,
};
invertTheme = () => {
this.setState(state => ({
themeInverted: !state.themeInverted,
}));
};
.
.
.
render() {
const { users, themeInverted } = this.state;
return themeInverted ? (
<ThemeProvider theme={invertTheme}>
<AppPresentional
users={users}
className={this.props.className}
invertTheme={this.invertTheme}
/>
</ThemeProvider>
) : (
<AppPresentional
users={users}
className={this.props.className}
invertTheme={this.invertTheme}
/>
);
}
}
const AppPresentional = ({ className, invertTheme, users }) => (
<div className={className}>
<Header invertTheme={invertTheme}>
<p>Random People</p>
<Button onClick={invertTheme}>Invert Theme</Button>
</Header>
.
.
.
</div>
);
export default styled(AppContainer)`
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
min-height: 100vh;
background: ${getColor('light')}
font-family: 'Roboto', sans-serif;
`;
index.js
/* src/index.js */
.
.
.
const rootElement = document.getElementById('root');
ReactDOM.render(
<ThemeProvider theme={theme}>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>,
rootElement,
);
theme.js
/* src/utils/theme.js */
export const theme = {
color: {
primary: '#5755d9',
secondary: '#f1f1fc',
dark: '#454d5d',
light: '#f8f9fa',
},
fontSize: {
hg: '32px',
md: '24px',
sm: '18px',
tn: '14px',
},
};
export const invertTheme = ({
color: { primary, secondary, dark, light },
...rest
}) => ({
color: {
primary: secondary,
secondary: primary,
dark: light,
light: dark,
},
...rest,
});
export const getFontSize = size => props => props.theme.fontSize[size];
export const getColor = color => props => props.theme.color[color];
In the index.js file we wrap the App
component with ThemeProvider
and pass it the theme
object. This serves as our default theme. As ThemeProvider
components can be nested and be used for contextual theming, we rerender a ThemeProvider
wrapping the AppPresentional
(the App component is divided into two components, container and presentational) if the this.state.themeInverted
is true. This time we pass the invertTheme
function to it which receives the default theme
object as props from the ThemeProvider
higher up in the tree and returns a new object with color values swapped around.
Conclusion
styled-components is one of the best solutions available for styling a React application. We have only discussed the most common of the possible use cases but there is much more to the library which you can explore in their documentation. styled-components have great server-rendering support as well. Now you know how to get going, give it a try.