10 CSS patterns that amount to 90% of my CSS code

10 CSS patterns that amount to 90% of my CSS code

While working on the frontend for a project with a lot of pages filled with elements and widgets in different styles and structures I have noticed that I keep repeating several generic CSS patterns from page to page. I thought it would be interesting to collect the most common patterns in my work and list them here.

Most of these patterns evolved and reached this form over a couple of years of me writing CSS in BEM. I would be happy to hear from you what kinds of repetitions appeared in your code.

Oh, and yeah, the code in this article will be written in BEM, because I like it and I'm used to it.

Quick start / Methodology / BEM
Quick start

And sorry, I'm not sorry for the clickbaity title.

Horizontal positioning

List elements horizontally

Position elements in a row. The elements can be of the same or of different widths. Usually, there is a horizontal gap between elements. The elements can wrap on the new row or not, and if they wrap, there is usually a vertical gap between rows.

<div class="row row--hgap--10 row--vgap--6">
    <div class="row__wrapper">
        <div class="element"></div>
        <div class="element"></div>
        <div class="element"></div>
        <div class="element"></div>
        <div class="element"></div>
    </div>
</div>

This can be achieved by making a row container that applies positioning and gaps to its children. I will explain why there is a need for .row__wrapper below.

Now to the technical part. The elements are laid out using display: flex.

.row__wrapper {
    display: flex;
    flex-flow: row wrap;
}

Then we add margins all around each element. Each individual margin is half the gap size. That way when margins of two neighboring elements sum up, the gaps amount to the required distance.

/* Horizontal gap of 10px */
.row--hgap--10 > .row__wrapper > * {
    margin-left: 5px;
    margin-right: 5px;
}

/* Vertical gap of 6px */
.row--vgap--6 > .row__wrapper > * {
    margin-top: 3px;
    margin-bottom: 3px;
}

At this point, we have a problem: BEM requires that blocks do not define their external geometry and that includes external margins. But our row has external margins of 5 and 3 px because of the elements at the edges. Because of this, wherever we place our row, it will annoyingly add extra spacing around itself.

To prevent this we add negative margins around the row:

.row--hgap--10 > .row__wrapper {
    margin-left: -5px;
    margin-right: -5px;
}

.row--vgap--6 > .row__wrapper {
    margin-top: -3px;
    margin-bottom: -3px;
}

And here's why we need a wrapper element inside our block: vertical margins in CSS have a tendency to collapse. Negative margins too, which is especially counter-intuitive. If we don't do something, our row block will (seemingly) randomly ignore the margins of the surrounding blocks.

There is a CSS hack to prevent collapsing for the margins of a given element: put a table before and after it. Since the row__wrapper is the element with the negative margins and it's located inside the row block, we can add these tables using pseudo-elements:

/* Prevent margin collapsing */
.row::before, .row::after {
    content: ' ';
    display: table;
}

And so this is the anatomy of the row pattern. It's useful if you need to place buttons in a form, tags under a post title, and so on.

Keep in mind that the clock for this method to produce uniform gaps is ticking and it will become obsolete when the new gap property becomes more mainstream:

gap (grid-gap) - CSS | MDN
row-gap и column-gap.The gap CSS property sets the gaps (gutters (en-US)) between rows and columns. It is a shorthand for row-gap and column-gap.

Complete stylesheet for this section:

/* Prevent margin collapsing */
.row::before, .row::after {
    content: ' ';
    display: table;
}

.row__wrapper {
    display: flex;
    flex-flow: row wrap;
}

/* Horizontal gap of 10px */
.row--hgap--10 > .row__wrapper {
    margin-left: -5px;
    margin-right: -5px;
}
.row--hgap--10 > .row__wrapper > * {
    margin-left: 5px;
    margin-right: 5px;
}

/* Vertical gap of 6px */
.row--vgap--6 > .row__wrapper {
    margin-top: -3px;
    margin-bottom: -3px;
}
.row--vgap--6 > .row__wrapper > * {
    margin-top: 3px;
    margin-bottom: 3px;
}

Split a container into horizontal sections

Given a container (usually with a visual border) split it into sections (usually separated with a border). Each section may contain an arbitrary element. Sections don't wrap, and one section usually expands to fill up the whole available width in the container.

Saved 2 minutes ago.
<div class="toolbar">
    <div class="toolbar__section">
        <button class="button">File</button>
    </div>
    <div class="toolbar__section">
        <button class="button">Edit</button>
    </div>
    <div class="toolbar__section">
        <button class="button">Settings</button>
    </div>
    <div class="toolbar__section toolbar__section--expand">
        <div class="status-text">Saved 2 minutes ago.</div>
    </div>
</div>

Naturally, the container is a flex row. Section elements are nothing special except that each one except the last one has a right border. And the expanding section has flex-grow: 1.

.toolbar {
    display: flex;
    flex-flow: row nowrap;

    border: 1px solid #ccc;
    border-radius: 8px;
    overflow: hidden;
}
.toolbar__section:not(:last-child) {
    border-right: 1px solid #ccc;
}
.toolbar__section--expand {
    flex-grow: 1;
}

This pattern is useful for navbars.

Put an element to the left and an element to the right

Put two elements (usually text) in a row. Align the first one to the left and the second one to the right. If the elements don't fit in the row, carefully wrap the right one below the left one.

Hot Chocolate
$3 before taxes
Cinnamon Roll
$4 before taxes
Kiss on the Cheek
Free ;)
Fits (hopefully on your screen too)
Hot Chocolate
$3 before taxes
Cinnamon Roll
$4 before taxes
Kiss on the Cheek
Free ;)
Doesn't fit
<div class="invoice">
    <div class="invoice__item">
        <div class="invoice__item-text">Hot Chocolate</div>
        <div class="invoice__item-price">$3 before taxes</div>
    </div>
    <div class="invoice__item">
        <div class="invoice__item-text">Cinnamon Roll</div>
        <div class="invoice__item-price">$4 before taxes</div>
    </div>
    <div class="invoice__item">
        <div class="invoice__item-text">Kiss on the Cheek</div>
        <div class="invoice__item-price">Free ;)</div>
    </div>
</div>

Each row is a flex row aligned by the baseline since we optimize it to display text neatly.

The left element (invoice__item-text) has a right margin to prevent it from touching the right element if they get too close. This margin specifies the minimal spacing between them.

The right element (invoice__item-price) has a left auto margin to align it to the right and right text alignment in case the text doesn't fit on one line.

.invoice__item {
    display: flex;
    flex-flow: row wrap;
    align-items: baseline;
}
.invoice__item-text {
    margin-right: 20px;
}
.invoice__item-price {
    margin-left: auto;
    text-align: right;
    font-weight: 500;
}

This pattern is useful for data tables like product specifications.

Vertical positioning

List elements vertically

This is probably the most ubiquitous pattern. Place elements one after another with a given gap.

<div class="column column--gap--8">
    <div class="element"></div>
    <div class="element"></div>
    <div class="element"></div>
</div>

Basically, add a bottom margin to each element except the last one. The flex layout is not necessary, it can be removed, especially if you plan to use margin collapsing, since margins do not collapse inside a flexbox.

.column {
    display: flex;
    flex-flow: column;
}
.column--gap--8 > *:not(:last-child) {
    margin-bottom: 8px;
}

Works for many kinds of blocks like form fields, typography elements (paragraphs, headers, lists), sidebar menu items, etc, etc.

Split a container into vertical sections

A twin sister of the horizontal sections pattern. Split a container (usually with a visual border) into vertical sections (usually separated with a border). Sections may be filled with arbitrary content.

CSS Is Nice
Scientists discovered that CSS can be pleasant to work with.
285 comments
Backend Guy 20 minutes ago
No.
<div class="card">
    <div class="card__section">
        <h1>CSS Is Nice</h1>
        <p>
            Scientists discovered that CSS 
            can be pleasant to work with.
        </p>
    </div>
    <div class="card__section">
        <span class="status-text">285 comments</span>
    </div>
    <div class="card__section">
        <div><b>Backend Guy</b> <span class="status-text">20 minutes ago</span></div>
        <div>No.</div>
    </div>
</div>

Simply add a border to each section except the last one. Here we also add padding to each section because that makes sense for a card block.

.card {
    display: flex;
    flex-flow: column;
    
    border: 1px solid #ccc;
    border-radius: 8px;
}
.card__section {
    padding: 6px 10px;
}
.card__section:not(:last-child) {
    border-bottom: 1px solid #ccc;
}

Special positioning

Centering an element inside a container

Given a container, center an element of arbitrary size inside of it on both axes.

<button class="big-button">
    <img class="big-button__icon" src="icon.png">
</button>

This is an old hack. We place the icon's top left corner in the center of the button using top and left. Then we move the icon back by half its width and half its height using transform so it visually becomes centered.

.big-button {
    position: relative;
}
.big-button__icon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

This hack implies that the container has a size defined by something. For example, it can have a fixed size or it can wrap its children elements. It's useful for icon containers like icon buttons because this way the size of the icon cannot affect the size of the parent and unexpectedly push around its neighbors.

If you want the centered element to affect the parent's size, you might want to simply use padding:

.big-button {
    padding: 8px;
}

Or flexbox:

.big-button {
    min-width: 32px;
    min-height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
}

Floating corner element

Place a floating element in the corner of a container. The floating element can push away other elements to prevent overlapping or floating over them.

For example, this "close" button:

Are you sure?
<div class="modal">
    <div class="modal__header modal__header--with-corner-button">
        <p>Are you sure?</p>
        
        <button class="modal__corner-button close-button">&times;</button>
    </div>
    <div class="modal__body">
        <button>Yes</button>
        <button>Absolutely</button>
    </div>
</div>

One way to achieve this is to make a "with corner button" modifier that enables relative positioning and makes room for the button so that it doesn't float over text. Then add the corner button element positioned absolutely.

.modal__header {
    padding: 12px;
    font-weight: bold;
}
.modal__header--with-corner-button {
    position: relative;
    /* Don't forget to make room. */
    padding-right: 56px;
}
.modal__corner-button {
    position: absolute;
    top: 12px;
    right: 12px;
}

Besides modal windows, this is useful for floating buttons a-la material design. In that case, the button can float over other elements, but it's still a good idea to leave extra scrollable space below it or you risk infuriating your users by making some elements unreachable:

Buying List
Milk
Bread
Butter
Meat
Veggies
Fruits
Non-fat milk pistachio deep mocha dipped cream frappaccino with chocolate drizzle and lots of whipped cream
Try reading the last item in this list.
Buying List
Milk
Bread
Butter
Meat
Veggies
Fruits
Non-fat milk pistachio deep mocha dipped cream frappaccino with chocolate drizzle and lots of whipped cream
Much better.

Visuals

Fixed aspect ratio

Make an element match its height to its variable width keeping a fixed aspect ratio.

For example, 16:9:

<div class="aspect-ratio aspect-ratio--16-by-9">
    <div class="aspect-ratio__wrapper">
        <img src="image.png" class="aspect-ratio__image" />
    </div>
</div>

Or 4:3:

<div class="aspect-ratio aspect-ratio--4-by-3">
    <div class="aspect-ratio__wrapper">
        <img src="image.png" class="aspect-ratio__image" />
    </div>
</div>

This is achieved by the padding hack. In CSS specification fractional padding of an element is always calculated against its parent's width event it's vertical padding. So we use the block root (.aspect-ratio) as the parent, apply fractional padding to its child .aspect-ratio__wrapper and position the image (or any other element) inside it absolutely edge-to-edge:

.aspect-ratio__wrapper {
    width: 100%;
    position: relative;
}
.aspect-ratio--16-to-9 .aspect-ratio__wrapper {
    padding-top: 56.25%;
}
.aspect-ratio--4-to-3 .aspect-ratio__wrapper {
    padding-top: 75%;
}
.aspect-ratio__image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

Images with a fixed aspect ratio are useful when you want to make a grid of cards look uniform.

Just like the hack with negative margins, this method will also soon become obsolete, because a new simpler aspect-ratio property is coming up:

aspect-ratio - CSS: Cascading Style Sheets | MDN
The aspect-ratio CSS property sets a preferred aspect ratio for the box, which will be used in the calculation of auto sizes and some other layout functions.

Icons

A set of icon elements that in the most basic case can be placed inline.

Using in a title
<h1>
    <i class="icon icon--pencil icon--size--24 b-2"></i> Using in a title
</h1>
<button>
    <i class="icon icon--plus b-1"></i> Add
</button>
<button>
    <i class="icon icon--pencil b-1"></i> Edit
</button>
<button>
    <i class="icon icon--trash b-1"></i> Delete
</button>
.icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    background-position: center;
    background-size: contain;
    background-repeat: no-repeat;
    vertical-align: middle;
}
.icon--plus {
    background-image: url('images/plus-icon.png');
}
.icon--pencil {
    background-image: url('images/pencil-icon.png');
}
.icon--trash {
    background-image: url('images/trash-icon.png');
}
.icon--size--24 {
    width: 24px;
    height: 24px;
}

If the project's icon library is not very tidy, the icons can have different styles and different padding (that is, forced padding inside the image file itself). If that's the case, it's useful to have a set of utility classes to tweak an icon's top or bottom relative position to make it align with the text.

.b-1, .b-2, .b-3, .t-1, .t-2, .t-3 {
    position: relative;
}
.b-1 { bottom: 1px; }
.b-2 { bottom: 2px; }
.b-3 { bottom: 3px; }
.t-1 { top: 1px; }
.t-2 { top: 2px; }
.t-3 { top: 3px; }

Loading animation over an element

Given an element (a button, a card, a form field, a checkbox), make a modifier for its "loading" state. In this state, the element becomes non-interactive, slightly fades away, and displays a loading animation on top of itself. Ideally, the element's size and position should not change.

<button class="btn">
    Ready to load!
</button>
<button class="btn btn--loading">
    Brrrrrr...
</button>

Let's say this is our button's base CSS:

.btn {
    position: relative;
    overflow: hidden;
    padding: 0 12px;
    border: 1px solid #ccc;
    cursor: pointer;
    background: none;
    border-radius: 8px;
}
.btn:hover {
    background: rgba(128, 128, 128, 0.4);
}
.btn:active {
    background: rgba(128, 128, 128, 0.6);
}

I'll be using rgb(128, 128, 128) in this section, because this color works in both dark and light modes of this site.

First, we disable interactiveness:

.btn--loading, .btn--loading:hover, .btn--loading:active {
    background: none;
    cursor: wait;
}

Then we put a partially transparent gray (or the color of your choice) overlay over the button:

.btn--loading::before {
    content: '';
    display: block;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    background: rgba(128, 128, 128, 0.6);
}

Notice that we have put overflow: hidden into the .btn stylesheet above to make sure that this overlay doesn't overflow its parent.

Then we put the loader on top like a cherry on a cake. This loader is centered using absolute positioning and transform just like we centered an icon inside a button before.

.btn--loading::after {
    content: '';
    display: block;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
    /* 
      This pseudo-element can be used to display any sort of loader animation. 
      For the sake of this demo let's use a simple rotating dashed circle. 
    */
    width: 20px;
    height: 20px;
    border-width: 2px;
    border-style: solid;
    border-color: black transparent;
    border-radius: 50%;
    animation: btn-loading-rotation .75s linear infinite;
}
@media (prefers-color-scheme: dark) {
    .btn--loading::after {
        border-color: white transparent;
    }
}
@keyframes btn-loading-rotation {
    from {
        transform: translate(-50%, -50%) rotate(0);
    }
    to {
        transform: translate(-50%, -50%) rotate(360deg);
    }
}

This same method is used to produce card--loading or menu-item--loading or whatever modifier which is then switched on and off by JS.

If you want to hide the button's text, change its color to transparent instead of removing it from the DOM. This way the button keeps its size. If you don't preserve the size, the button will jar under the cursor when it switches state.


The whole CSS for this section:

.btn {
    position: relative;
    overflow: hidden;
    padding: 0 12px;
    border: 1px solid #ccc;
    cursor: pointer;
    background: none;
    border-radius: 8px;
}
.btn:hover {
    background: rgba(128, 128, 128, 0.4);
}
.btn:active {
    background: rgba(128, 128, 128, 0.6);
}
.btn--loading, .btn--loading:hover, .btn--loading:active {
    background: none;
    cursor: wait;
}
.btn--loading::before {
    content: '';
    display: block;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    background: rgba(128, 128, 128, 0.6);
}
.btn--loading::after {
    content: '';
    display: block;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
    /* 
      This pseudo-element can be used to display any sort of loader animation. 
      For the sake of this demo let's use a simple rotating dashed circle. 
    */
    width: 20px;
    height: 20px;
    border-width: 2px;
    border-style: solid;
    border-color: black transparent;
    border-radius: 50%;
    animation: btn-loading-rotation .75s linear infinite;
}
@media (prefers-color-scheme: dark) {
    .btn--loading::after {
        border-color: white transparent;
    }
}
@keyframes btn-loading-rotation {
    from {
        transform: translate(-50%, -50%) rotate(0);
    }
    to {
        transform: translate(-50%, -50%) rotate(360deg);
    }
}

Conclusion

Thanks for reading this. My experience of coding in CSS will probably be individual and specific to my project history. Any comment to share or discuss your views is very much welcome.

Cover photo by Gradienta / Unsplash