_kud's tech blog

Component namespacing with Emotion

(or styled-components or whatever CSS-in-JS)

Naming, naming, naming. Oh god. One of the hardest part in computer science. Fortunately, we've got some conventions about that which help us not to overthink every time we must name something.

For instance, there's one: BEM.

BEM

I'm a BEM lover for ages. The first time I saw this method, I understood quickly it was a really great way to avoid conflict between elements and - IMO - to remove all the painful part of CSS. (You've got a good article about this on css-tricks.com).

Before using CSS Modules or styled-components or Emotion, my stylesheet code was something like that:

1.MyBlock {
2 [...]
3}
4
5.MyBlock-myElement {
6 [...]
7}
8
9.MyBlock-myElement--myModifier {
10 [...]
11}

It is not exactly the ✌️ official ✌️ convention (you've got different ones in fact) but for some reasons like readability or double-clicking on name to select, this was the convention I used and it worked really well!

But this is a different time and now I mostly use Emotion to create my styled components.

Emotion

So let's begin about how to namespace your components.

Here is a basic example:

1import styled from "@emotion/styled"
2
3const MyStyledComponent = styled.div``

Alright, now let's imagine now we've got two components: one parent, one child.

The classic way could be:

1const Parent = styled.div``
2
3const Child = styled.div``

We want now to adjust Child depending on triggering a hover on Parent.

We can write this:

1const Parent = styled.div`
2 &:hover ${Child} {
3 color: red;
4 }
5`
6
7const Child = styled.div``

Great.

But as a BEM fan, I rather prefer to write my code like this:

1const MyBlock = styled.div`
2 &:hover ${MyBlockMyElement} {
3 color: red;
4 }
5`
6
7const MyBlockMyElement = styled.div``

This is cool because you know that your element depends on a block. But when you want to export, you'll get something like:

1// my-component.js
2export const MyBlock = styled.div`
3 &:hover ${MyBlockMyElement} {
4 color: red;
5 }
6`
7
8export const MyBlockMyElement = styled.div``
1// app.js
2import { MyBlock, MyBlockMyElement } from "./my-component"

Nope. It is unpleasant to read now.

A solution could be to create a namespace by using objects.

1// my-component.js
2const MyComponent = styled.div``
3
4MyComponent.MyElement = styled.div``
5
6export default MyBlock

which is great because you can do:

1// app.js
2import MyComponent from "./my-component"
3
4const MyApp = () => (
5 <MyComponent>
6 <MyComponent.MyElement />
7 </MyComponent>
8)

Elegant, no?

This is my favourite way to write namespaced components. 🙌🏻

Oh noes!

But, there's a but. 😬

Did you remember when we added a :hover trigger? Let's try with this convention:

1// my-component.js
2const MyComponent = styled.div`
3 &:hover ${MyComponent.MyElement} {
4 color: red;
5 }
6`
7
8MyComponent.MyElement = styled.div``
9
10export default MyBlock

By doing this, you'll get this error:

1🙅‍♂️ can't access property "MyElement", MyComponent is undefined

Well, it is quite obvious as at this moment, MyComponent doesn't exist yet.

🗣 So you told us to use namespacing via objects but it seems that it creates some issues now!.

Yes, that's right but I still find that this convention is elegant and I want to go deeper with it. So how could we do?

The trick

Well, after looking into the github issues of emotion, I found a solution, it is just to create an anonymous function which returns the child!

1// my-component.js
2const MyComponent = styled.div`
3 &:hover ${() => MyComponent.MyElement} {
4 color: red;
5 }
6`
7
8MyComponent.MyElement = styled.div``
9
10export default MyBlock

Boom! 💥

Be careful however to use the babel-plugin-emotion to do that.

Bye bye and see you soon!