You are viewing this site in a simplified layout because your browser does not yet support CSS Subgrid.

op111.net

Search op111.net

Lean HTML markup with modern CSS

One of the reasons modern CSS is great is that it reduces the need for HTML elements that have no semantic meaning but are used purely for purposes of styling and layout. In other words, modern CSS reduces the need for wrapper and container elements.

This is good for three reasons:

  1. It improves the developer experience
  2. It reduces the bytes sent over the network
  3. It reduces the size of the HTML DOM

In this article I show three examples of using modern CSS—that is, CSS features like functions, custom properties, and Grid Layout— to implement common design patterns without relying on container and wrapper elements.

CONTENTS

  1. Full-width section with constrained content
  2. Section with title, description, and three cards
  3. Section with title, description, and two cards
  4. Notes
  5. A demo page
  6. Conclusion
  7. Resources

Full-width section with constrained content

The first example is a very common pattern, and one common way to achieve it is with a wrapper and auto margins:

  1. You put the element in a wrapper and set the wrapper width to 100%
  2. You add a width/max-width to the element and set its inline margin to auto

The technique I describe accomplishes the same without the extra wrapper, and, in my experience, works really well: I have been using it on client projects for the last three or four years without issues.

The HTML markup, which we are not allowed to change, is this:

<body class="site">
  <header class="header"></header>
  <main class="main">
    <section class="section"></section>
    <section class="section"></section>
    <section class="section"></section>
  </main>
  <footer class="footer"></footer>
</body>

The header, the sections, and the footer should all be full-width, while their content should be constrained to, let’s say, 1120px.

First I add a CSS variable (CSS custom property) for the max-width:

:root {
  --max-width: 1120px;
}

Then I add inline padding to the elements based on a calculation: The padding for each side is the space that remains after I subtract --max-width from 100% and divide the result by two.

.header,
.section,
.footer {
  width: 100%;
  padding-inline: calc(calc(100% - var(--max-width)) / 2);
}

After the padding is applied, the width that is left for the content is the value of --max-width. And, since the padding on either side is the same, the content is also centered.

There is an issue though: If the viewport is smaller than --max-width, there will be no padding at all. (The calculated value will be negative.)

I fix this with max():

.header,
.section,
.footer {
  width: 100%;
  padding-inline: max(calc(calc(100% - var(--max-width)) / 2), 32px);
}

After adding max(), inline padding will be either (a) the result of the calculation or (b) 32px, whichever is larger. That’s what max() does: It returns the largest from a list of values.

Now there is something more:

In this example I use the --max-width calculation to set inline padding. However, this is a calculation useful for more than that, as it gives you the exact distance from the edge of the viewport to the edge of the content: It can be used for padding and for margin, it can be used as the right or left of absolutely positioned elements, and more.

So, to make the calculation easier to use, I store it in a new variable, which I name --space-x, and then I just use --space-x:

:root {
  --max-width: 1120px;
  --space-x: max(calc(calc(100% - var(--max-width)) / 2), 32px);
}

.header,
.section,
.footer {
  width: 100%;
  padding-inline: var(--space-x);
}

And that’s it!

Full-width sections with constrained content, without extra divs, by using two CSS variables and two declarations.

Any decoration you need to add to the section’s background—color, gradient, image, etc.— can be added directly to the element. You don’t need a wrapper or container for it. (See the accompanying demo for some examples.)

I usually add one last thing that’s not essential to the technique but useful overall.

Normally, for small screens --space-x is a straight value and doesn’t need to be calculated. So, I change the setup to something like this:

:root {
  --space-x: 16px;
}

@media screen and (min-width: 480px) {
  :root {
    --space-x: 32px;
  }
}

@media screen and (min-width: 1080px) {
  :root {
    --max-width: 1120px;
    --space-x: max(calc(calc(100% - var(--max-width)) / 2), 32px);
  }
}

Section with title, description, and three cards

After setting up the outer spacing of all sections with the calc() and --max-width technique, let me now explain how to arrange the contents of a section without adding any extra, non-semantic markup.

The HTML for the section is this:

<section class="section">
  <h2 class="section__title"></h2>
  <p class="section__description"></p>
  <article class="card"></article>
  <article class="card"></article>
  <article class="card"></article>
</section>

The CSS for this task does not use CSS functions or custom properties. The main tool here is another modern CSS feature, CSS Grid Layout, in a straightforward implementation:

.section {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-template-rows: auto auto 1fr;
  gap: 32px 16px;
}

.section__title,
.section__description {
  grid-column: 1 / -1;
}

.card {
  grid-column: span 2;
}

Let me explain the three rules:

  1. I define a grid with 6 columns, 3 rows, 32px gap between rows and 16px gap between columns.
  2. I set the title and the description to take one row each (from the first column to the first column from the end). The title, as the first child of the grid, will take the first row. The description will take the next available row (second row).
  3. I set each card to span 2 columns. Since the 6 columns of the grid are just enough space for three 2-column cards, all cards will be in the same row (third row).

There will also be 32px of space between rows (title, description, and cards) and 16px of horizontal space between the cards.

That’s all you need for large screens.

For small screens, depending on the desired layout for the cards, an extra HTML element may be needed. If you want to stack the cards vertically, you just switch the grid from 6-column to single-column and that’s it. If you want the cards to scroll horizontally or to be part of a slider, you need to wrap them in an element that will be the scroll container or the root element of the slider:

<section class="section">
  <h2 class="section__title"></h2>
  <p class="section__description"></p>
  <div class="cards">
    <article class="card"></article>
    <article class="card"></article>
    <article class="card"></article>
  </div>
</section>

You then modify the CSS accordingly:

.section {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-template-rows: auto auto 1fr;
  row-gap: 32px;
}

.section__title,
.section__description {
  grid-column: 1 / -1;
}

.cards {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  column-gap: 16px;
}

.card {
  grid-column: span 2;
}

(The accompanying demo uses this extra element to make the cards scrollable in small screens.)

Section with title, description, and two cards

This pattern is the same as the previous one with one difference: It has two cards instead of three. I include it here to show how easy it is to do layout with CSS Grid.

<section class="section section--2-col">
  <h2 class="section__title"></h2>
  <p class="section__description"></p>
  <article class="card"></article>
  <article class="card"></article>
</section>

Each card should have the same width as the cards in the 3-column section. The cards should also have the same space between them and they should be centered.

The only change required in the markup is an extra class for the section, to mark the difference of this section from the other sections: .section--2-col.

So, the section and its content will use the CSS for the 3-column section, plus one additional rule:

.section--2-col .card:not(:last-child) {
  grid-column: 2 / span 2;
}

This tells the first card to take the second and third from the six columns. The second card will then take the next available space, that is, the fourth and fifth columns. (Remember that cards are set to span 2 columns each.) There will be one empty column at the start and another at the end. So, the cards will be centered.

Note that I selected the first card by using :not(:last-child), but there are more ways to select it, either with complex selectors or with an extra class for the first card. If you can add an extra class when you generate the markup, I think it’s the cleanest and safest way to do it.

Notes

A note on accessibility.

CSS Flex and, more so, CSS Grid give you great power for arranging elements visually. To take a silly example, you can put the site header at the bottom of the body and still display it at its expected visual position, at the top.

But this is not a substitute for having a correct order in the HTML document source. Correct ordering in the HTML document is important, especially for media and user agents that present the content linearly. For this reason, always take good care to arrange the elements in the HTML document in a correct, logical way.

See Reordering and Accessibility in the CSS Grid Layout specification for an explanation.

A demo page

I put together a page demonstrating everything I describe in this article. The page also includes a site header and a multi-column site footer. The multi-column footer uses CSS Grid, while the header uses CSS Flex.

Conclusion

Modern CSS is great, and the features that have been added in the last few years make today’s CSS very different from what it was 10 years ago. Not only you can do more with it, you can do more with less (or the same with less, for that matter) and without hacks. This means a better authoring experience, better performance, and better maintainability.

The only caveat, if you want to take advantage of all that, is that you should keep an eye on browser support and browser usage. Modern CSS features do not land at the same time in all modern browsers and, even after a feature has landed in all modern browsers, there will still be users of older versions where the new feature will not work.

Thank you for reading,

— Demetris

Resources