Targeting nested elements with Emotion
CSS in JS is all the rage, but how do you target nested components?
2020-02-05
TL;DR
Using a pattern of CSS overrides in Emotion can enable you to target nested components within a React application.
const componentStyles = css`...`
const overrideStyles = css`...`
...
<div css={componentStyles}>
<MyComponent cssOverrides={overrideStyles} />
</div>
Problem
It is a common pattern to use CSS to target elements nested within another element, in order to only change the appearance of the element within a certain context. Using a CSS in JS solution such as Emotion makes this difficult because you end up with generated class names that are difficult to target when writing CSS.
This problem and solution were dependent on using the css
prop provided by emotion. This is my preferred use of emotion but this problem would exist in any tool that uses generated class names.
As an example, let's assume we have two components,
A button component
const buttonStyles = css`...`
function Button() {
return <button css={buttonStyles}>Button</button>
}
Then a card component that renders a button
const cardStyles = css`...`
function Card() {
return (
<div css={cardStyles}>
<Button />
</div>
)
}
Our button may normally have a red outline, but when in a modal, we need it to have a green outline. We could potentially write a rule targeting the button
element. But what if it was a <div>
, or a <p>
? We don't know what the class name will be applied to the button
element, so we can't target the class name. Or can we?
Functional, just not great
With the css
named template string, it actually returns an object describing the resulting stylesheet that was created. It is possible to access most of the generated class name through the object returned from a call to css
.
const cardStyles = css`
& .css-${buttonStyles.name} {
border: 1px solid green;
}
`
This way we have access to the generated class name and can apply conditional styling whenever a Button
appears nested within a Card
.
But this solution, while works, is not ideal. We are depending on an API that is not well documented and is bound to change. At some point, the name
property could include the css-
prefix. Or the property name could change to something else. It just is not very reliable. Which is why I prefer an override instead.
cssOverrides
Rather than depending on an internal Emotion API, I have gone the route of making my components accept a cssOverrides
prop that I then apply inside of my component. Any time my Button
is rendered, a cssOverrides
can be provided, and it will be used in conjunction with my default styles.
const buttonStyles = css`...`
function Button({ cssOverrides }) {
return <button css={[buttonStyles, cssOverrides]}>Button</button>
}
const cardStyles = css`...`
const buttonOverrideStyles = css`...`
function Card() {
return (
<div css={cardStyles}>
<Button cssOverrides={buttonOverrideStyles} />
</div>
)
}
With this solution, if emotion were to change their API, our styles should be largely unaffected. We don't have to worry about generated class names resulting in a more robust error resistant solution.