For anyone who's worked with React at some point, styling often becomes boring.
"Who uses vanilla CSS anymore?"
Enter styled-components, an amazing way to style React components. I'm not going to go through all the great features it has, you can go through its website to know that.
What had me curious was how elegant yet illegal its syntax looked compared to regular JavaScript.
I mean, even for someone who's experienced in JavaScript, the following syntax looks off:
const Button = styled.button`
color: black;
background: ${props => props.bgColor};
`;
A lot of questions come into one's mind:
- Wait? What's the template literal doing here?
- How is the string connected to
styled.button
? What even is astyled.button
, is it an object, a function or worse yet, something else entirely? - How are
props
present in this string? Where is it coming from? - Is this even valid JavaScript?
- Why don't I just use Tailwind?
Well for the last question I don't have much to say, I don't want to start a war. For the 4th question: Yes, it is 100% legal JavaScript. For the rest of the questions, let's explore the answers to them in this blog post.
Tagged Templates: The Basics
Styled Components use a pattern called Tagged Templates. They are simply functions that are invoked with the following format:
functionName`string in literals`;
`<functionName>``string in literals
Although the syntax might feel illegal, it’s 100% legal to do so.
JavaScript simply passes the string as an Array of strings as the function's argument.
What about strings with variables and functions interpolated? We would need those for dynamic styling and run-time computation right?
The solution's fairly simple. JavaScript passes a single array of strings as the first argument to the function, and the remaining arguments are variables and expressions interpolated into the string.
So we can essentially do this with our function:
const tagFunction = (stringFragments, ...expressions) => {
let fullString = '';
stringFragments.forEach((str, index) => {
fullString += (str + (expressions[index] || ''));
});
return fullString;
}
This lays the foundation for building a library like Styled components where static strings can be used for property names and their corresponding expressions can be used for dynamic/run-time property values.
Complex Expressions
Styled components also give us the ability to pass a function in place of a simple value to be computed every time a component is rendered.
We would need a very simple modification in our code above to handle complex expressions like objects, arrays and functions.
const resolveExpression = (expression) => {
if (typeof expression === 'string' || typeof expression === 'number') return expression;
if (typeof expression === 'function') return expression();
if (typeof expression === 'object') return JSON.stringify(expression);
}
const tagFunction = (stringFragments, ...expressions) => {
let fullString = '';
stringFragments.forEach((str, index) => {
fullString += (
str + (resolveExpression(expressions[index]) || '')
);
});
return fullString;
}
Props in function expression, React Component Generation and applying styles via className
Note:
styled-components
uses theclassName
prop to apply the styles generated to a component.
Simply have the tagFunction
return a JSX expression that can further receive props, then pass those props to any function in the expressions.
Once the functions resolve, use the entire style string generated, create a CSS class with it and apply that class to the div/element you’re creating, and have a style tag adjacent to it containing the CSS class with the style string.
const resolveExpression = (expression, props) => {
if (typeof expression === "string" || typeof expression === "number")
return expression;
if (typeof expression === "function") return expression(props); // ${props => props...whatever}
if (typeof expression === "object") return JSON.stringify(expression);
};
const styled = {};
// Predefined function
styled.div =
(stringFragments, ...expressions) =>
// React component
(props) => {
let fullStyleString = '';
stringFragments.forEach((str, index) => {
fullStyleString += (
str + (resolveExpression(expressions[index]) || '')
);
});
return (
<>
<style type="text/css">
{`.<classNameGeneratedAtRunTime> { ${fullStyleString} }`}
</style>
<div
className={`${
props.className || ""
} <aboveClassNameGeneratedAtRunTime>`}
>
{props.children}
</div>
</>
);
};
// Usage
const Div = styled.div`
color: ${(props) => props.black};
`;
<Div $color="black">Hey There</Div>;
Applying Styles for Custom Components
Now the final stage is to apply the styles we have to the custom components we create.
Simply change styled
to a function by default that performs the same operation as styled.div
above, but on a Component that's passed.
For example:
const MyComponent = ({ className }) => {
// The existence of className is important.
// If your component does not have a className prop it won't work.
return <div className={className}>My content</div>
}
const StyledMyComponent = styled(MyComponent)`
color: #000000;
background: #ffffff;
`;
The snippet to make it all work would look like the following:
const styled =
// Custom component
(Component) =>
// Style Template Literals
(stringFragments, ...expressions) =>
// React component
(props) => {
let fullStyleString = '';
stringFragments.forEach((str, index) => {
fullStyleString += (
str + (resolveExpression(expressions[index]) || '')
);
});
return (
<>
<style type="text/css">
{`.<classNameGeneratedAtRunTime> { ${fullStyleString} }`}
</style>
<Component
className={`${
props.className || ""
} <aboveClassNameGeneratedAtRunTime>`}
>
{props.children}
</Component>
</>
);
};
With this, you should have a clear understanding of how styled-components internally work, it's a great library that perfectly abstracts a lot of complicated implementation details and makes the best use of a complicated feature of JavaScript to make it elegant and readable for the everyday web developer.