Sometimes the libraries that we use daily can seem a little bit like magic. By taking the time to understand the fundamental JavaScript features that make these libraries possible, we become better equipped to improve our use of those libraries, applying their functionality in unique and relevant ways.
In React, there are two libraries that work together to make the creation of presentational components very simple: styled-components and styled-system.
Here is an example of these libraries working together, courtesy of the styled-system documentation.
import styled from 'styled-components'
import { color } from 'styled-system'
const Box = styled.div`
${color}
`
This code created a React component named Box that receives the props color and bg.
<Box color="#fff" bg="tomato">
Tomato
</Box>
In this article I am going to cover:
- Generating React Components with Tagged Templates
- Implementing a simple version of Styled Components
- Diving into how styled-components and styled-systems actually operate together
Check out Rangle's Front End Hub. New content weekly!
Template Literals
In order to understand how styled-components and styled-systems work, it's best to first understand where these libraries get their power from: Template Literals.
The most common use-case of Template Literals is string concatenation.
// Template Literal
const string = `I am a template literal`;
// Template Literal with placeholder
const expressions = 'dynamic values';
const string = `I can contain ${expressions}`;
Template Literals are clean, they're effective and they have been so well adopted for string concatenation that some of us (e.g. me) don’t even care how this syntax works, or realize that its functionality can be expanded upon.
Template Literals and the preceding function
When we implement Template Literals, something unintuitive happens: The contents of the Template Literal (e.g. the text and placeholders) are passed to a function.
Which function? In the two examples above, a default function with the job of concatenating the text and placeholders into a single string.
const expressions = 'dynamic values';
const example = `I can contain ${expressions}`;
console.log(example); // >> I can contain dynamic values
But Template Literals are not confined to performing only string concatenations. JavaScript lets us use our own custom functions to do whatever we want with the text and placeholders within the Template Literal. This type of function is called a Tag and to use it, you simply reference the function name — the Tag — in front of the Template Literal. Doing this results in the creation of a Tagged Template.
For example, here is a simple function that does not accept any parameters and prints a static message to the console.
const printStaticMessage = () => {
console.log('My static message');
}
We can invoke this function in two ways: As a traditional function call and as a Tagged Template.
printStaticMessage(); // >>> My static message
printStaticMessage``; // >>> My static message
Notice that each invocation generated the same result. So, we can conclude that Tagged Templates are just an alternative way to invoke a function.
Using the Template Literal as Arguments for the Tag
A more useful Tagged Template would utilize the text and placeholders within the Template Literal. Let’s create a Tag that prints out its arguments.
const printArguments = (...args) => { console.log(...args); }
const var1 = "my";
const var2 = "message"
printArguments`This is ${var1} custom ${var2}!`;
// ["This is "," custom ","!"],
// "my",
// "message"
The first argument is an array of strings that represent the text in the Template Literal, separated into elements based on the location of the placeholders.
The remaining n arguments are strings with the value of each placeholder, ordered based on when they are defined in the Template Literal.
Knowing that these are the arguments that Tags receive, we can guess what the implementation of the default Template Literals concatenation function looks like:
const defaultFunction = (stringArray, ...values) => {
return stringArray.reduce((acc, str, i) => {
return values[i] ? acc + str + values[i] : acc + str;
}, '');
}
const var1 = "my";
const var2 = "message"
const example = defaultFunction`This is ${var1} custom ${var2}!`;
console.log(example); // >>> This is my custom message!
Passing functions into the Tagged Template
Since the Tag is simply receiving the Template Literal placeholders as argument variables, those variables can contain any JavaScript object, like a number, string or function. Here is an non-useful example where we pass a function in the Template Literal placeholder and execute it in the Tag.
const executeFirstPlaceholder = (textArray, placeholder) => {
placeholder();
}
executeFirstPlaceholder`${() => { console.log('first placeholder')}}`;
// >>> first placeholder
Returning functions from a Tagged Template
Like any other function in JavaScript, a Tagged Template can return objects, strings, numbers, and, of course, functions.
const multiply = (multiple) => (num) =>
parseInt(num[0]) * parseInt(multiple[0]);
const double = multiply`2`;
const result = double`4`;
console.log(result); // >>> 8
Making the leap to React
React’s “function components” are very simply JavaScript functions that can be rendered into the DOM. Here is an example of a Tagged Template returning a React function component.
const withGreeting = ([greeting]) =>
({ name }) => <h1>{greeting}, {name}!</h1>;
const Greet = withGreeting`Greetings`;
// Render component
<Greet name="Chris" />
// Renders in DOM
<h1>Greetings, Chris</h1>
This is the crux for how Styled Components generate React components.
Styled Components
Utilising tagged template literals (a recent addition to JavaScript) and the power of CSS, styled-components allows you to write actual CSS code to style your components. It also removes the mapping between components and styles — using components as a low-level styling construct could not be easier!
Styled Components use Tagged Templates to return React components.
In the following example, styled.h1 is used to create a simple React component containing a <h1> HTML tag, display using the CSS styles specified within the Template Literal
import styled from 'styled-components';
const Title = styled.h1`color: blue;`;
// Render
<Title>Regular Title</Title>
// Renders to DOM
<h1 style="color: blue;">Regular Title</h1>
The styled object contains keys named after common HTML tags — like H1, H2, and div. These keys reference a function that can be used as the Tag in a Tagged Template.
A simple implementation of styled.h1
Let’s try to make a simple implementation of styled.h1. At it's simplest, the styled.h1 function receives the CSS styles in the back-ticks and transforms them into a style object that it attaches to the underlying element (e.g. h1).
const styledH1 = ([styles]) => ({ children })=> {
const lines = styles
.split(';')
.map(line => line.trim())
.filter(line => line !== "")
// Function courtesy of mck89 on StackOverflow
const convertToCamelCase = (key) =>
key.replace(/-([a-z])/g, (x, up) => up.toUpperCase())
const style = lines.reduce((acc, line) => {
const lineParsed = line.split(':');
const key = convertToCamelCase(lineParsed[0]);
const val = lineParsed[1];
return { ...acc, [key]: val };
}, {});
return <h1 style={style}>{children}</h1>
}
const H1 = styledH1`
color: red;
font-size: 18px;
`;
// Render
<H1>Hello</H1>
// Renders in DOM
<h1 style="color: red; font-size: 18px;">Hello</h1>
At this point, the style we are passing to the function is hard coded and fixed; not able to dynamically change, based on the prop values the component receives.
Let’s look at how the ability to pass functions into our tagged templates can enable things to become more dynamic.
Using functions to access Props in Template Literals and Styled Components
As discussed we discussed, a function passed to a Template Literal placeholder can be executed. Styled Components utilize this feature to generate dynamic styles.
import styled from 'styled-components';
const Button = styled.button`
color: ${ (props) => props.primary ? 'blue' : 'red' };
`;
class StyledComponentsDemo extends Component {
render() {
return(
<>
<Button primary>Primary</Button>
<Button>Secondary</Button>
<Button primary
onSubmit={() => handleSubmit()}>Submit</Button>
</>
)
}
}
When the Styled Component is rendered, each function in the Template Literal is passed the component’s props and those props can be used to impact the presentation of the component.
Note: not all props passed to a Styled Component need to impact the presentation (e.g. onSubmit); they could also be used only by the underlying HTML element.
Styling regular, custom components
Styled Components allow you to style any custom component that you’ve created. First, the custom component must receive the prop className and pass it to the underlying DOM element. Once that is done, pass the custom component to the styled function and invoke it as a Tagged Template to receive a new Styled Component.
import styled from 'styled-components';
const Button = ({ className, children }) =>
<button className={className}>{children}</button>
const ButtonBlue = styled(Button)`color: blue`;
// Render
<ButtonBlue>Blue Button</ButtonBlue>
Styling the Styled Components
Styled Components uses the CSS preprocessor stylis, supporting SCSS-like syntax for automatically nesting styles.
const Thing = styled.button`
color: black;
:hover {
color: blue;
}
`
Within SCSS syntax, & references the current component. You can also reference other components like you would reference any other type of selector (e.g. .class or #id) by simply referencing ${OtherComponentName} , but only if it is a Styled Component.
import styled from 'styled-components';
const Item = styled.div`
color: red;
`
const Container = styled.div`
& > ${Item} {
font-size: 2rem;
}
`
class StyledComponentsDemo extends Component {
render() {
return(
<Container>
<Item>Item 1</Item>
</Container>
)
}
}
As you can see, we have the ability to not only specify the styles in our components, but also the ability to add some dynamic functionality. Building on this, we are able to better accommodate some common use cases such as adding themes to our applications.
Using Themes
Theming is accomplished by exporting the ThemeProvider component, passing an object to its theme prop, and wrapping the entire app in the ThemeProvider component. This will give every Styled Component access to the theme object.
import styled, { ThemeProvider } from 'styled-components';
const Item = styled.div`
color: ${( props ) => props.theme.color.primary}
`
const theme = {
color: {
primary: 'red'
}
}
class StyledComponentsDemo extends Component {
render() {
return(
<ThemeProvider theme={theme}>
<Item>Item 1</Item>
</ThemeProvider>
)
}
}
Components that are not Styled Components can also access theme by using the withTheme function.
import { withTheme } from 'styled-components';
class MyComponent extends React.Component {
render() {
return <p>{this.props.theme.color.primary}</p>
}
}
export default withTheme(MyComponent);
Styled System
Styled System is a collection of utility functions that add style props to your React components and allows you to control styles based on a global theme object with typographic scales, colors, and layout properties.
If you created a Button component from Styled Components and you want it to receive foreground and background color props, you can use the styled-system utility function color and pass it as a placeholder function in the Template Literal to enable these props.
import styled, { ThemeProvider } from 'styled-components';
import { color } from 'styled-system'
const theme = {
colors: {
primary: 'blue'
}
}
const Box = styled.div`
${color}
`
class StyledSystemDemo extends Component {
render() {
return (
<ThemeProvider theme={theme}>
<>
<Box color="#fff" bg="tomato">Tomato</Box>
<Box color="white" bg="primary">Tomato</Box>
</>
</ThemeProvider>
)
}
}
Note: The name of the generated props are all outlined in the styled system API.
If there is a theme available, the utility function will try to match the prop value to the theme before using the value as the raw value (e.g. #fff).
Structuring Theme objects
The structure of the theme object and styled-system are tightly coupled. The structure follows a Work-in-Progress specification called System UI Theme Specification.
For instance, the fontSizes and colors keys follow this specification, and their values (arrays or objects) also follow this specification.
export const theme = {
fontSizes: [
12, 14, 16, 20, 24, 32
]
fontSizes.body = fontSizes[1]
colors: {
blue: '#07c',
green: '#0fa',
}
}
With the above theme, the fontSize prop on a component could receive the index value of the array, or the alias body.
Under the hood of color
Let’s look at how styled-system implements the utility function color. Remember that a utility function is called like this:
const Button = styled.div`
${color}
`
This is what the function looks like.
// https://github.com/styled-system/styled-system/blob/v2.3.6/src/styles.js
export const color = props => ({
...textColor(props),
...bgColor(props)
}
Which is akin to writing this in the Template Literal:
const Button = styled.div`
${(props) => ({
...textColor(props),
...bgColor(props)
})}
`
The textColor and bgColor functions will return style objects that are spread within the function. These functions look like this.
// https://github.com/styled-system/styled-system/blob/v2.3.6/src/styles.js
export const textColor = responsiveStyle({
prop: 'color',
key: 'colors', // theme key
})
export const bgColor = responsiveStyle({
prop: 'bg',
cssProperty: 'backgroundColor',
key: 'colors'
}
The responsiveStyle function handles all the breakpoints, fallbacks and prop naming. Below, I simplified the styled-system code for demonstrative purposes.
// https://github.com/styled-system/styled-system/blob/v2.3.6/src/util.js
// I simplified this for demonstrative purposes
export const responsiveStyle = ({
prop,
cssProperty,
key,
}) => {
const fn = props => {
cssProperty = cssProperty || prop
const n = props[prop];
const theme = props.theme;
// ...
return {
[cssProperty]: theme[key][n]
}
// ...
}
return fn
}
Which can be represented to look something like this:
const Button = styled.div`
{
${...(props) => (
{ color: props.theme['colors'][props.color] }
)}
${...(props) => (
{ backgroundColor: props.theme['colors'][props.bg] }
)}
}
`
Breakpoints and responsive themes
For responsive themes, styled-system lets you establish breakpoints in the theme and then lets you pass an array as a prop with different values for each breakpoint. styled-system takes a mobile first approach, so the first index will always be the smallest breakpoint.
<Box
width={[
1, // 100% below the smallest breakpoint (all viewports)
1 / 2, // 50% from the next breakpoint and up
1 / 4, // 25% from the next breakpoint and up
]}
/>
// theme.js
const breakpoints = ['40em', '52em', '64em', '80em']
export default { breakpoints };
Conclusion
I was inspired to see how the developers of styled-components and styled-system employed the extended functionality of Template Literals and Tagged Templates to provide users with an intuitive way for adding SCSS to React components.
Have you seen any interesting uses of common functionality in your work recently? Please share!