Image for post
Image for post

Adding separators to layouts with CSS-in-JS

Mandy Michael
Jan 10, 2020 · 7 min read

Over the holiday period we did some experimentation to remove the blocky cards on our website and replace it with a solid background. As a result we needed dividers to separate the articles on the page (otherwise they all ran into each other — it was bad).

The TL;DR is to combine :not & :nth-child with a pseudo-element and props which define the number of columns you need.

The Setup

We are currently using either Flexbox or CSS Grid to create “collections” of articles. Our aim is to eventually move completely to CSS Grid but in the mean time we support both. At this stage neither Flexbox or CSS Grid provide an easy way to create dividers between content, for the sake of consistency I focused on approaches that would work with both Flexbox and CSS Grid layouts.

A Collection of Articles

As mentioned this collection has the ability to set a different number of columns at different breakpoints using props that we pass in when we set up the collection in our page routes. We set it up this way so that we only had to define one component that would work in a number of scenarios. Essentially the CSS is the same so having to maintain and manage multiple versions seemed unnecessary. Instead we make use of the flexibility of CSS-in-JS to output the CSS we need using the values from our props. This means the same component can render a 4 column, 3 column or 2 column list of cards.

interface CellProps {
numberOfItems: number
initialColumns: number // small screens
intermediateColumns: number // medium screens
finalColumns: number // larger screens
}

For example you might have something like the following example, where we use the finalColumns prop with the :not and :nth-child pseudo-class selectors to apply margins to cards. (If you are using CSS Grid you can just use grip-gap for this.)

export const GridItem = styled('div')<CellProps>(props => ({
[':not(nth-child(' + finalColumns + 'n))']: {
marginRight: 12,
},
})

As these props already exist in our collection for styling purposes it made adding the dividers in a lot easier because we already had the groundwork laid out.

Adding in the Dividers

Inside your component it might look the below example:

[':not(:first-child)::before']: {
content: `''`,
backgroundColor: black,
height: '100%',
width: 1,
position: 'absolute',
left: 0,
transform: 'translateX(-6px)',
},

By combining the :not pseudo-selector with the :first-child pseudo-selector we can add a pseudo-element to every item except for the first child. This prevents the divider appearing on the left of the first card (like the image below).

Image for post
Image for post

If there is only one row of cards in the collection this would be all the CSS we’d need. However, this particular example can have any number of rows so we need to remove the divider from the first (or last) card of every row, not just the first (or last) card.

If we have 6 cards set out as 3 columns across 2 rows instead of using:first-child we can use the :nth-child selector- which will match to elements based on their position in the collection. For example :nth-child(3n) will select every 3rd card (see the lavender cards in the image below).

Image for post
Image for post
The pseudo-class selector :nth-child(3n) sets every 3rd card to lavender.

By passing one of our props into the :nth-child selector (e.g. finalColumns ) the component will know how many columns there are going to be in each row.

'&:not(:nth-child(' + finalColumns + 'n))::after: {
content: `''`,
backgroundColor: black
height: '100%',
width: 1,
position: 'absolute',
right: 0,
transform: 'translateX(4px)',
}

If we put aside the pseudo-element for a moment this will output div:not(:nth-child(Xn)) — if we assume, for this example, we have three columns then it would be div:not(:nth-child(3n)). If we were to change the background of each matched card it would look like the image below.

Image for post
Image for post

Adding back in the pseudo-element: div:not(:nth-child(Xn))::before if we again assuming 3 cards per row, this code would add the pseudo-element to every child except for the 3rd card (which is the last card in the column).

Image for post
Image for post
Six cards laid out as two rows and 3 columns with black vertical dividers

This will work regardless of how many columns you define, for example, if the finalColumn prop had a value of 4 it would be 4n and would add the divider to every card except the 4th one.

Uneven columns

Image for post
Image for post
5 cards set out in two rows with 3 cards per row with a divider between each card. The last card has a hanging divider due to being an odd number.

In order to resolve this we can go back to our selector and add another :not pseudo-class selector to exclude the last child in the collection ( :not(:last-child) ).

[':not(:nth-child(' + finalColumns + 'n)):not(:last-child)::after']: {
content: `''`,
backgroundColor: black
height: '100%',
width: 1,
position: 'absolute',
right: 0,
transform: 'translateX(4px)',
}

Using three columns as our example, this will add the divider to every card except for the 3rd card in every column AND the last card in the collection.

Image for post
Image for post

Making it work Responsively

Because we already set how many cards appear in each row based on breakpoints we can use those props again to add/remove dividers as needed.

At the moment we are using the following selector for larger viewports e.g. 968px and above. If we wrap this in a min-width breakpoint it will only apply to larger viewports, smaller viewports will be unaffected and the pseudo-element will not appear.

@media screen and (min-width: 968px) {
[':not(:nth-child(' + finalColumns + 'n)):not(:last-child)::after']
...
}

Typically, I’d take a mobile first approach to breakpoints and only add what is needed. However, when you want to completely change something between breakpoints, like adding something new in that only appears at a specific breakpoint (or between breakpoints), the min-width approach can get very messy due to the number of overrides needed.

In this situation I use a max-width breakpoint instead. For example, if I only wanted horizontal dividers on smaller viewports then I could say @media screen and (max-width: 400px) then add in my pseudo-element styles restricting it only to mobile. This removes the necessity to then reset the pseudo-element to none on larger viewports.

For the purposes of the example I will assume on smaller viewports the rows will be only 1 column wide, which means we won’t need the dividers. Because we are ignoring the smallest viewports we can do a combination media query and put the selector for the pseudo-element inside. This will restrict the pseudo-element to only appear between 400px and 967px.

@media screen and (min-width: 400px and max-width: 967px) {  '&:not(:nth-child(' + intermediateColumns + 'n))
:not(:last-child)::after': {
content: `''`,
backgroundColor: black,
height: '100%',
width: 1,
position: 'absolute',
right: 0,
transform: 'translateX(4px)',
}
}

For the purposes of consistency you can pull all the divider styles out into a variable (or if you a mixin if you are using Sass) and then import the styles into the component. For example:

export const divider: CSSObject = {
content: `''`,
backgroundColor: black,
height: '100%',
width: 1,
position: 'absolute',
right: 0,
transform: 'translateX(4px)',
}
export const GridItem = styled('div')<CellProps>(props => ({ ['@media screen and (min-width: 400px and max-width: 967px)']: { '&:not(:nth-child(' + intermediateColumns + 'n))
:not(:last-child)::after': {
...divider
}
}, ['media screen and (min-width: 968px)']: { ['&:not(:nth-child(' + finalColumns + 'n))
:not(:last-child)::after']: {
...divider
}
}))

It doesn’t really matter how you write your CSS, whether it’s with CSS-in-JS, a preprocessor, post-css or straight up awesome CSS. There are a lot of useful selectors, and combining them allows you to be quite specific and targeted. As a result it’s important to know what selectors are available and how to apply them! Pseudo-class selectors are among my favourite aspects of CSS and is often largely forgotten about by developers. Make the most of them, they are amazing.

There are many different ways to apply dividers to layouts, and many different approaches you can take, this is just one of those options. This may or may not work for you, but it does provide a starting point for you to come up with your own solutions and experiments! Have fun!

💜 Mandy

Pixel and Ink

Seven West Media (WA) Digital Team blog.

Mandy Michael

Written by

Lover of CSS, and Batman. Front End Developer, Writer, Speaker, Development Manager | Founder & Organiser @fendersperth | Organiser @mixinconf

Pixel and Ink

Seven West Media (WA) Digital Team blog.

Mandy Michael

Written by

Lover of CSS, and Batman. Front End Developer, Writer, Speaker, Development Manager | Founder & Organiser @fendersperth | Organiser @mixinconf

Pixel and Ink

Seven West Media (WA) Digital Team blog.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store