HTML & CSS
Article

Centering With Sass

By Hugo Giraudel

Centering in CSS is well known for being a tedious task. It is kind of the running gag from the language, leading to jokes such as “we managed to send men on the moon, but we can’t vertically align in CSS”.

While CSS is indeed a bit tricky when dealing with centering, especially vertical centering, I feel like those jokes are a bit unfair. Actually, there are plenty ways of centering content in CSS, you just have to know how to do it.

This article is not intended to explain how these methods work, but how we can wrap them in a Sass mixin for friendly and easy usage. So if you feel a bit uncomfortable with CSS centering, may I recommend a couple of resources to read beforehand:

All good? Let’s get started then.

What is it all about?

First, we will focus on centering an element within its parent as it is the most common use case for absolute centering (modals, content in section, etc.). When you ask someone about CSS centering, the usual question you get as a reply is: do you know the element’s dimensions? The reason behind this question is that if you don’t know them, the best solution is to rely on CSS transforms. It lowers the browser support a bit, but it is highly flexible. If you can’t use CSS transforms or do know the element’s width and height, then it is easy to rely on negative margins.

So our mixin is going to basically do this: position the top left corner of the element absolutely in the middle of the container, then shift if back of half its width and half its height with either CSS transforms or negative margins depending on whether or not dimensions are passed to the mixin. No dimensions: go for transforms; dimensions: use margins.

You would then use it like this:

/**
 * Enable position context for the child
 */
.parent {
  position: relative;
}

/**
 * Absolutely center the element in its parent
 * No dimensions are passed to the mixin, so it relies on CSS transforms
 */
.child-with-unknown-dimensions {
  @include center;
}

/**
 * Absolutely center the element in its parent
 * Width is passed to the mixin, so we rely on a negative margin for the
 * horizontal axis and CSS transforms for the vertical axis
 */
.child-with-known-width {
  @include center(400px);
}

/**
 * Absolutely center the element in its parent
 * Height is passed to the mixin, so we rely on a negative margin for the
 * vertical axis and CSS transforms for the horizontal axis
 */
.child-with-known-height {
  @include center($height: 400px);
}

/**
 * Absolutely center the element in its parent
 * Width is passed to the mixin, so we rely on a negative margins for both
 * horizontal axis and vertical axis
 */
.child-with-known-dimensions {
  @include center(400px, 400px);
}

When compiled, it should output this:

§.parent {
  position: relative;
}

.child-with-unknown-dimensions {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.child-with-known-width {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -200px;
  width: 400px;
  transform: translateY(-50%);
}

.child-with-known-height {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translateX(-50%);
  margin-top: -200px;
  height: 400px;
}

.child-with-known-dimensions {
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -200px;
  width: 400px;
  margin-top: -200px;
  height: 400px;
}

Okay, that looks a bit verbose but keep in mind that this output is for demonstration purpose. It is rather unlikely you find yourself using them all in a given situation.

Building the mixin

Okay, let’s dig. From the previous code snippets, we already know our mixin’s signature: it has two optional parameters, $width and $height.

/// Horizontal, vertical or absolute centering of element within its parent
/// If specified, this mixin will use negative margins based on element's
/// dimensions. Else, it will rely on CSS transforms which have a lesser
/// browser support but are more flexible as they are dimension-agnostic.
///
/// @author Hugo Giraudel
///
/// @param {Length | null} $width [null] - Element width
/// @param {Length | null} $height [null] - Element height
///
@mixin center($width: null, $height: null) { .. }

Moving on. In all circumstances, the mixin needs to make the element absolutely positioned, so we can start with that.

@mixin center($width: null, $height: null) {
  position: absolute;
  top: 50%;
  left: 50%;

  // Moar magic here...
}

We will have to be clever with our code. Let’s pause here for a second and analyse the different options we have:

Width Height Solution
Undefined Undefined translate
Defined Defined margin
Defined Undefined margin-left + translateY
Undefined Defined translateX + margin-top

Let’s go with this.

@mixin center($width: null, $height: null) {
  position: absolute;
  top: 50%;
  left: 50%;

  @if not $width and not $height {
    // Go with `translate`
  } @else if $width and $height {
    // Go width `margin`
  } @else if not $height {
    // Go with `margin-left` and `translateY`
  } @else {
    // Go with `margin-top` and `translateX`
  }
}

Now that we have set up the skeleton for our mixin, we only have to fill the gaps with actual CSS declarations.

@mixin center($width: null, $height: null) {
  position: absolute;
  top: 50%;
  left: 50%;

  @if not $width and not $height {
    transform: translate(-50%, -50%);
  } @else if $width and $height {
    width: $width;
    height: $height;
    margin: -($width / 2) #{0 0} -($height / 2);
  } @else if not $height {
    width: $width;
    margin-left: -($width / 2);
    transform: translateY(-50%);
  } @else {
    height: $height;
    margin-top: -($height / 2);
    transform: translateX(-50%);
  }
}

Note: the #{0 0} trick is a dirty hack to prevent a slightly to agressive minification from Sass that would lead to margin: mt 0 ml instead of margin: mt 0 0 ml.

So far, so good.

Going further

There are several things we could do to push our mixin further, such as including a @supports rule inside the mixin to check for CSS transforms support or assume there is (or allow) Modernizr and output condition styles depending on whether or not CSS transforms are supported. We could also do some more agressive checking on the arguments to make sure they are valid values for width and height.

Although you have to ask yourself whether it’s a good thing to go that far. The mixin, as is, already has a cyclomatic complexity of 6, which is getting quite a lot for a Sass helper. It’s still okay, but adding more code to it likely means bumping the cyclomatic complexity further.

What about Flexbox?

I’m pretty sure some of you folks are jumping on your seat, thinking about how we can use Flexbox to center an element within its parent. Indeed, it’s possible and it turns out to be the easiest solution of all if you can afford it.

The main difference between the solution we have just set up and the Flexbox one is that the latter is built on top of the parent while the former is mainly focusing on the child (provided any of its ancestors have a position different from static).

To make an element have its child(ren) centered, you only have to print a triplet of properties. You can make a mixin, placeholder, class or whatever you fancy for this.

@mixin center-children {
  display: flex;
  justify-content: center;
  align-items: center;
}

Provided you add relevant vendor prefixes (through the mixin or Autoprefixer), this solution should Just Work in many browsers.

.parent {
  @include center-children;
}

Yielding, as you can surely guess:

.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

Final thoughts

We wanted a short mixin to easily center an element within its parent; this one does the job, and it does it well. Not only is it clever enough to work no matter whether or not the element has specific dimensions, but it also provides a friendly and obvious API which is extremely important.

By looking at the code, anybody understands right away that the @include center line is the inclusion of a helper that does some logic in order to make the element centered within its parent. However remember, the latter (or any parent in the DOM tree) has to have a position different than static for this to work! ;)

You can play with the code on SassMeister: http://sassmeister.com/gist/550809f5aa00b73d932c.

  • Gatilin Maxim

    margin: -($width / 2) #{0 0} -($height / 2);

    is not valid!
    Valid version:
    margin: -($height / 2) #{0 0} -($width / 2);

    • http://www.alwaystwisted.com/ Stuart Robson

      Thanks @gatilinmaxim:disqus, this has been fixed and updated :)

      • Алесей

        Are you sure? I see this:
        margin: -($width / 2) #{0 0} -($height / 2);

  • http://grawl.ru/ Grawl

    What about Centering in the Unknown https://css-tricks.com/centering-in-the-unknown/ article with pseudos? I use it now.

    • http://hugogiraudel.com/ Hugo Giraudel

      I am not a big fan of using pseudo-elements when they are not needed.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.