UPD: About div vs button.
Originally styled-components as a library appeared in 2016. The idea was proposed by Glen Maddern. It is CSS-in-JS solution, but in my opinion, the most powerful part is not CSS-in-JS, but the ability to create small components fast.
Consider following snippet:
<h1 style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}>
Hello World, this is my first styled component!
</h1>
vs
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
<Title>Hello World, this is my first styled component!</Title>;
As you can see there is more semantics in this code. You can easily tell that it is a title, it can be hard to tell otherwise (in case of h1
it is possible to guess, but if it would be div
?..).
We can for sure do the same without styled-components:
const Title = ({ children }) => (
<h1
style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
>
{children}
</h1>
);
But let's face it: nobody does it. Where is with styled-components it happens naturally. This is simplified version, it doesn't pass props, it doesn't handle ref
property.
styled-components pair nicely with a11y. For example, instead of this:
<>
<div
role="button"
aria-expanded={expanded}
aria-controls={sectionId}
id={labelId}
className={styles.Label}
onClick={() => onToggle && onToggle(index)}
>
{title}
<span aria-hidden={true}>{expanded ? "▲" : "▼"}</span>
</div>
<div
role="region"
aria-labelledby={labelId}
id={sectionId}
hidden={!expanded}
className={styles.Panel}
>
{expanded && (isFunction(children) ? children() : children)}
</div>
</>
we can write
const Label = styled.div("Label");
Label.defaultProps = { role: "button" };
const Panel = styled.div("Panel");
Panel.defaultProps = { role: "region" };
<>
<Label
aria-expanded={expanded}
aria-controls={sectionId}
id={labelId}
onClick={() => onToggle && onToggle(index)}
>
{title}
<span aria-hidden={true}>{expanded ? "▲" : "▼"}</span>
</Label>
<Panel aria-labelledby={labelId} id={sectionId} hidden={!expanded}>
{expanded && (isFunction(children) ? children() : children)}
</Panel>
</>;
Nicer isn't it?
Alternatives
This idea is so popular that it got copied by other libraries.
CSS-in-JS
"Zero-runtime" CSS-in-JS
style property
CSS modules
DIY
Let's write (very) simplified implementation of styled-components, to demystify things a bit. So we started with this:
const Title = ({ children }) => (
<h1
style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
>
{children}
</h1>
);
1) we need to pass props:
const Title = ({ children, ...props }) => (
<h1
style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
{...props}
>
{children}
</h1>
);
2) We need to handle ref
const Title = React.forwardRef(({ children, ...props }, ref) => (
<h1
style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
ref={ref}
{...props}
>
{children}
</h1>
));
3) Let's make tag configurable with as
property (some libraries call it is
or use
as well):
const Title = React.forwardRef(({ children, as = "h1", ...props }, ref) =>
React.createElement(
as,
{
style: { fontSize: "1.5em", textAlign: "center", color: "palevioletred" },
...props,
ref
},
children
)
);
4) Let's add a way to override styles
const Title = React.forwardRef(({ children, as = "h1", ...props }, ref) =>
React.createElement(
as,
{
...props,
style: {
fontSize: "1.5em",
textAlign: "center",
color: "palevioletred",
...props.style
},
ref
},
children
)
);
5) Let's generate component
const styled = defaultAs =>
React.forwardRef(({ children, as = defaultAs, ...props }, ref) =>
React.createElement(
as,
{
...props,
style: {
fontSize: "1.5em",
textAlign: "center",
color: "palevioletred",
...props.style
},
ref
},
children
)
);
const Title = styled("h1");
6) Let's pass default styles from outside
const styled = defaultAs => defaultStyles =>
React.forwardRef(({ children, as = defaultAs, ...props }, ref) =>
React.createElement(
as,
{
...props,
style: {
...defaultStyles,
...props.style
},
ref
},
children
)
);
const Title = styled("h1")({
fontSize: "1.5em",
textAlign: "center",
color: "palevioletred"
});
7) Add display name to the component
const styled = defaultAs => defaultStyles => {
const component = React.forwardRef(
({ children, as = defaultAs, ...props }, ref) =>
React.createElement(
as,
{
...props,
style: {
...defaultStyles,
...props.style
},
ref
},
children
)
);
component.displayName = `${defaultAs}💅`;
return component;
};
8) Make styles customizable depending on properties
const isFunction = x => !!(x && x.constructor && x.call && x.apply);
const styled = defaultAs => defaultStyles => {
const component = React.forwardRef(
({ children, as = defaultAs, ...props }, ref) =>
React.createElement(
as,
{
...props,
style: {
...(isFunction(defaultStyles)
? defaultStyles(props)
: defaultStyles),
...props.style
},
ref
},
children
)
);
component.displayName = `${defaultAs}💅`;
return component;
};
const Title = styled("h1")(() => ({
fontSize: "1.5em",
textAlign: "center",
color: "palevioletred"
}));
9) Filter non-html properties, let's filter out all properties in propTypes
:
const filterObject = (rest, shouldForwardProp) =>
Object.keys(rest)
.filter(shouldForwardProp)
.reduce((obj, key) => {
obj[key] = rest[key];
return obj;
}, {});
const styled = defaultAs => defaultStyles => {
const component = React.forwardRef(
({ children, as = defaultAs, ...props }, ref) =>
React.createElement(
as,
{
...(component.propTypes
? filterObject(props, key =>
Object.keys(component.propTypes).includes(key)
)
: props),
style: {
...(isFunction(defaultStyles)
? defaultStyles(props)
: defaultStyles),
...props.style
},
ref
},
children
)
);
component.displayName = `${defaultAs}💅`;
return component;
};
10) Bonus. Let's use Proxy
instead of the first function:
const styled = Proxy(
{},
{
get: (_, defaultAs, __) => defaultStyles => {
const component = React.forwardRef(
({ children, as = defaultAs, ...props }, ref) =>
React.createElement(
as,
{
...(component.propTypes
? filterObject(props, key =>
Object.keys(component.propTypes).includes(key)
)
: props),
style: {
...(isFunction(defaultStyles)
? defaultStyles(props)
: defaultStyles),
...props.style
},
ref
},
children
)
);
component.displayName = `${defaultAs}💅`;
return component;
}
}
);
const Title = styled.h1({
fontSize: "1.5em",
textAlign: "center",
color: "palevioletred"
});
Now you know what is inside!