/ css

Separating list items only with CSS

Very often we have to implement a list of items with spacing or borders between them.

List with border separators

The implementation of such styling is straight forward:

<ul className="list">
  {array.map((item, index) => (
    <li key={item.id} className={index > 0 ? : 'item withBorder' :'item'}><Item item={item} /></li>
  )}
</ul>
.item {
  /* other properties */
}

.withBorder {
   margin-top: 10px;
   border-top: 1px solid black;
   padding-top: 10px;
}

I dislike having unneeded logic checks and conditions. index > 0 is simple, but the reader of the code still have to parse it in their head. I call logic like this noise.

Having a lot of noise in your system makes it harder to understand and reason about. In this case, we also have two classes on a single element, and this increases the complexity.

So...can we remove the conditional and use one class name? Yes, we can ?:

<ul className="list">
  {array.map((item) => (
    <li key={item.id} className="item"><Item item={item} /></li>
  )}
</ul>

The JavaScript portion of the code is a lot simpler now.

...but this CSS portion is more complex:

.item {
   /* other properties */
   margin-top: 10px;
   border-top: 1px solid black;
   padding-top: 10px;
}

.item:first-child {
   margin-top: 0;
   border-top: none;
   padding-top: 0;
}

We simplified the JavaScript by pushing the complexity down to the CSS level. This is usually a good thing.

An issue with this CSS code is that we are setting properties and then resetting them. This often leads people forgetting to reset all properties especially when they adding more in the future. When somebody reads the above styles, he/she have to parse in their head both definitions and see what is negated.

So we can still do better:

The solution

In general, we want to put a certain style on all elements except the first one. By using the adjacent sibling combinator selector we can do just that:

.item {
   /* other properties */
}
.item + .item {
    margin-top: 10px;
  border-top: 1px solid black;
  padding-top: 10px;
}

I find this solution very elegant. The styling will be applied to all elements except the first one. We are setting the properties once.

Having this as separate selector groups the separator logic from the rest of the styles a component

Bonus: this approach works with Styled-Components as well.

const Li = styled.li`  
  & + & {
     margin-top: 10px;
    border-top: 1px solid black;
    padding-top: 10px;
  }
`

Update: A couple of people mentioned that you could use :not(first-child):

.item {
   /* other properties */
}
.item:not(first-child) {
    margin-top: 10px;
  border-top: 1px solid black;
  padding-top: 10px;
}