DEV Community

Bastien Calou
Bastien Calou

Posted on

CSS Layout: Get around frameworks wrapping your components

CSS is hard, so they say. But have you tried CSS while using a framework that messes up your DOM? Because that's where the real fun lies.

The problem

So I am working on a simple mobile interface: header, content, footer. The header and the footer have a fixed size, and I want the content to be placed at the center of the remaining space.

"Ah, flexbox, my dear old friend, we meet again!" ― Myself (not really, it's just for the storytelling)

So of course I come up with this simple implementation:

<body>
  <header></header>
  <main></main>
  <footer></footer>
</body>
Enter fullscreen mode Exit fullscreen mode
body {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

Each direct descendent of the body has a blue border. So far so good.

So I'm happy, as any front-end developer is when things align nicely.

But as the application grows, I realize that I'm gonna need to wrap the main and the footer elements inside a single Angular component, for structural reasons inherent to the application.

That's when my beautiful page breaks, and you can see why by looking at the generated markup taking in account my new component:

<body>
  <header></header>
  <my-component>
    <main></main>
    <footer></footer>
  </my-component>
</body>
Enter fullscreen mode Exit fullscreen mode

Alas! Flexbox doesn't care for anyone but its own direct children, which are now header and my-component.

Look at the blue borders: the body now has only two direct children.

My first thought was to make my-component a flex container itself, but I couldn't get very far with this idea.

my-component {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

my-component is a flex container itself, with "justify-content: space-between". But of course that doesn't make the `main` bloc vertically centered.

Now, take a moment: how would you do it?

A dirty way

I know you didn't take a moment and read this right away. It's okay.

One way I didn't think of before writing this article is to add a pseudo element to my-container:

my-component::before {
  content: '';
}
Enter fullscreen mode Exit fullscreen mode

my-container has now three children: the before pseudo element, the main bloc and the footer bloc.

You can see it in action here!

Just set the pseudo element's content to an empty string instead, and voilà!

That's it! Sometimes a dirty fix like this one is so simple that it becomes quite attractive…

A cleaner way

But adding a pseudo element just to trick the flexbox system does not feel very clean.

A few weeks ago I read this great article by Rachel Andrews: Digging Into The Display Property: Box Generation.

While reading about the display: contents property, I had two conflicting thoughts:

  • "Well now, that's very interesting"
  • "Yep, never gonna use that though"

But working on my layout problem, I thought about it again. From the article:

"The value display: contents will remove the box it is applied to from the box tree in the same way that display: none does, but leave the children in place."

In other words, it will result in a situation similar (in our case*) to what would happen if we commented out the element (but not its children):

<body>
  <header></header>
  <!-- <my-component> -->
    <main></main>
    <footer></footer>
  <!-- </my-component> -->
</body>
Enter fullscreen mode Exit fullscreen mode

And this is how you actually do it:

my-component {
  display: contents;
}
Enter fullscreen mode Exit fullscreen mode

*In reality, CSS properties applied on my-component still impact its children (font-size, color...). So not exactly like commenting out the element, but you get the idea.

So… Does it mean that flexbox-wise, main and footer become direct children of body again? Yes! my-component doesn't have to be a flex container itself anymore. The justify-content: space-between applied to the body now works again.

This is the shortest solution presented in this article: one selector and one property. Almost magical.

The blue border wrapping my-component disappeared: indeed, the box is not rendered anymore, so box properties (margin, padding, border…) are not rendered either.

While I love the beautiful simplicity of this solution, display: contents has its drawbacks.

There can be accessibility problems with display: contents, because it hides the element from the accessibility tree. If you use it on a ul element in order to achieve some layout, the information that this is a list will be lost in many browsers.

This is not a problem in our case though, since my-component doesn't carry a specific semantic meaning. But one could be concerned about browser support, which is in that grey area that I would call the "meh" area.

A cleaner cleaner way?

Enter the margin: auto technique.

It's only while writing this article that I remembered about another article, one of the greatest I read on this subject: Flexbox's Best Kept Secret, by Sam Provenza. It's from 2015 and I used this "secret" countless times since then.

4 years later, I'm not sure this is a well-known fact. But you can use margin-left: auto , for exemple, in order to "push" a flexbox item to the right as much as possible.

A pen that saved me many headaches. The price element has its margin-left set to auto.

That's super useful ! With this technique, you can push a flex item in any direction, horizontal or vertical.

But there was a part of the article that I had forgotten:

"If you don't specify a direction, simply applying margin: auto, a flex item would evenly distribute any extra space on either side of the itself equally"

So the "secret" also works with multiple auto margins.

And there it lies, a wonderful solution to our problem: make my-component a flex container again, and use margin: auto 0 on the main element.

my-component {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

main {
  margin: auto 0; 
}
Enter fullscreen mode Exit fullscreen mode

The empty space is evenly distributed between the margin-top and the margin-bottom of the main element.

See it here!

No magical pseudo element. Less accessibility and compatibility risks. Just sweet and glorious flexpower™.

The journey of vertical centering never stops.

Top comments (0)