Pixel and Ink

    Adding separators to layouts with CSS-in-JS

    Mandy Michael
    Jan 10 · 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

    Our site uses React on the front end with Emotion (CSS-in-JS library) for styles. The examples will use Emotion, but the selectors can be converted to CSS, Sass or whatever method you use for styling.

    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

    One of our collections allows us an infinite number of articles and accepts a value for how many cards we want per row, we’ll use this as the example because it will cover a few scenarios. (This is one of our older collections, so it currently uses Flexbox).

    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

    To add the dividers we can use the :not and :first-child pseudo-class selectors to put the dividers in the correct place and add in a ::before pseudo-element to create the actual line. (You could also use :last-child here instead of :first-child ).

    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).

    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).

    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.

    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).

    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

    If you have an even number of cards that don’t perfectly fill each row and column then unfortunately this will not be enough — we will end up with a situation like the image below, where you have a hanging divider that looks out of place.

    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.

    Making it work Responsively

    Unfortunately because we determine where the dividers are applied based on the number of cards in each row if this number changes on smaller viewports it will no longer work.

    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.

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade