HTML & CSS
Article

Tilted Angles in Sass

By Hugo Giraudel

Recently, I had to work on a website heavily using tilted angles as part of its design guidelines. By “tilted angles”, I mean these sections whose top or bottom edge is not completely horizontal and rather a bit inclined.

Illustration of a tilted angle in CSS

There are quite a few ways to implement this. You could have base 64 encoded images applied as a background, but it makes it hard to customise (color, angle, etc.).

Another way would be to skew and/or rotate an absolutely positioned pseudo-element, but if there is one thing I don’t want to deal with, it’s the skew transform. God, no.

When using Sass, you could use the Angled Edges library that encodes dynamically generated SVG. It works super well, however it requires a fixed width and height expressed in pixels, which kind of bothered me.

I also really wanted to see if and how I could be able to implement this. I ended up with a solution that I am really proud of, even if it might be a bit over-engineered for simple scenarios.

What’s the Idea?

My idea was to apply a half transparent, half solid gradient to an absolutely-positioned pseudo-element. The angle of the gradient defines the tilt angle.

background-image: linear-gradient($angle, $color 50%, transparent 50%);

This is packaged in a mixin that applies the background color to the container, and generates a pseudo-element with the correct gradient based on the given angle. Simple enough. You’d use it like this:

.container {
  @include tilted($angle: 3deg, $color: rgb(255, 255, 255));
}

The main problem I faced with this was to figure which height the pseudo-element should have. At first, I made it an argument of the mixin, but I ended up going trial-and-error at every new angle to figure out what would be the best height for the pseudo. Not ideal.

And just when I was about to table flip the whole thing, I decided to leave the laptop away, took a pen and paper and started doodling to figure out the formula behind it. It took me a while (and a few Google searches) to remember trigonometry I learnt in high-school, but eventually I made it.

To avoid having to guess or come up with good approximations, the height of the pseudo-element is computed from the given angle. This is, of course, all done with a bit of Sass and a lot of geometry. Let’s go.

Computing the Pseudo-element’s Height

Trust me when I’m telling you it won’t be too hard. The first thing we know is that we have a full-width pseudo-element. The gradient line will literally be the pseudo’s diagonal, so we end up with a rectangle triangle.

Let’s name it ABC, where C is the right angle, B is the known angle ($angle argument) and A is therefore C - B. As shown on this diagram, we try to figure out what b is.

Geometrical diagram displaying the aforementioned explanation

To do that, we need to find the value of c (the gradient line, aka the hypothenuse), which is the length of a (the bottom side, 100%) divided by sine of the A angle (with B = 5°, A would be 85° for instance).

c = a / sin(C - B)

From there, we have to use Pythagoras theorem:

The square of the hypotenuse (the side opposite the right angle) is equal to the sum of the squares of the other two sides.

Therefore, the square of one of the other side equals the square of the hypothenuse minus the square of the third side. So the square of b equals the square of c minus the square of a.

b² = c² - a²

Finally, the length of b equals the squared root of the square of c minus the square of a.

b = √(c² - a²)

That’s it. We can now build a small Sass function computing the height of the pseudo-element based on a given angle.

@function get-tilted-height($angle) {
  $a: (100% / 1%);
  $A: (90deg - $angle);
  $c: ($a / sin($A));
  $b: sqrt(pow($c, 2) - pow($a, 2));

  @return (abs($b) * 1%);
}

Note: the pow(), sqrt() and sin() functions can either come from Sassy-Math, Compass or custom sources.

Building the Tilted Mixin

We’ve done the hardest part, trust me! The last thing to do is build the actual tilted() mixin. It accepts an angle and a color as argument and generates a pseudo-element.

@mixin tilted($angle, $color) {
  $height: get-tilted-height($angle);

  position: relative;
  background-color: $color;

  &::before {
    content: '';
    padding-top: $height;
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    background-image: linear-gradient($angle, $color 50%, transparent 50%);
  }
}

A few things to note here: the mixin applies position: relative to the container to define a position context for the pseudo-element. When using this mixin on absolute or fixed elements, it might be worth considering removing this declaration from the mixin.

The mixin applies the background color to the container itself as well as pseudo’s gradient, as they have to be synced anyway.

Finally, the pseudo-element’s height has to be conveyed through padding-top (or padding-bottom for that matter) instead of height. Since the height is expressed in percentages based on the parent’s width, we cannot rely on height (as it computes from the parent’s height).

Final Thoughts and Going Further

For this article, I’ve chosen to go with a simple version of the mixin, which might lack flexibility and could, in theory, present the following problems:

  • It is not possible to use it on an element already making use of its ::before pseudo-element. This could be solved by adding an optional parameter to specify the pseudo-element, defaulting to before.
  • It is not possible to display a tilted edge at the bottom of the container as bottom: 0 is currently hard-coded in the mixin core. This could be solved by making it possible to pass an extra position to the mixin.

Also, my existing version uses Sass-based math functions as it was in a Jekyll project, not allowing me to extend the Sass layer. If using node-sass, you could easily pass these functions from JavaScript to Sass through Eyeglass or Sassport, which would be definitely much better.

I hope you liked it! If you can think of anything to improve it, please feel free to share in the comments. :)

See the Pen Tilted Angles in CSS by SitePoint (@SitePoint) on CodePen.

  • Craig Buckler

    Nice solution – I hadn’t thought of using gradients.

    One method I’ve used in the past is borders on an absolutely positioned pseudo-element to produce a triangle, e.g. border-width: 20px 1000px; border-color: transparent transparent #fff #fff. It works and has less mathematical headaches although you needed to re-specify the width and quality could suffer on wide elements.

  • http://csspre.com/ Alexander Futekov

    The linear-gradient direction can just be “to top right”, no need for angle there if you already set a proper percent-based height to the pseudo element.

    • http://hugogiraudel.com/ Hugo Giraudel

      Well yes, but the point is to compute the height of the pseudo-element based on the angle.

  • willemvb

    That’s a clean way to tackle this. Thanks for taking me back to math class!
    It seems like both Safari and Chrome on my non-retina iMac are lacking some antialiasing in this gradient, resulting in jagged edges. A slight gradient transition with rgba colors improved this for me: linear-gradient(3deg, rgba(255, 255, 255, 1) 49%, rgba(255, 255, 255, 0) 50%) —but not ideal.
    In the example there seems to be an issue with Safari not taking the horizontal padding of the .container into account for calculating the height of the pseudo-element. If you increase padding on the container, this becomes more visible. Not sure why this happens, but seems safer to only use padding on the .content for now.

    • http://hugogiraudel.com/ Hugo Giraudel

      Hey, thank you for your comment. Because of the harsh aliasing on some browsers, I went with a slightly different solution in the end using skew instead of gradient. Everything else stays the same: still a pseudo-element, still a dynamically computed % height based on given angle. Just a skew instead of a gradient for better rendering.

      Regarding the second part of your comment, that’s a bug in Blink / WebKit (fixed in Chrome 52). I discussed it here on Twitter with some other folks: https://twitter.com/HugoGiraudel/status/758320653573484544. Basically the problem is that Chrome 51- and Safari does not include the padding in the calculation of percentages for padding-top and padding-bottom. That sucks. :(

      • willemvb

        Good to know your math refreshment wasn’t in vain. Thanks for the explanation!

  • René

    What a coincidence– I had to face this problem just recently and solved it with clip-path and the aspect-ratio-through-pseudo-elements.
    The angle is controlled by the padding-top of the pseudo-element. And the direction of the clipped area of course can be set by sub-classes with different clip-paths.
    There can easily put some math in there to extend the mixin with a explicit angle-parameter.


    .devider {
    background: inherit;
    clip-path: polygon(0% 0%, 100% 0%, 0% 100%);
    &:before { content:""; padding-top: 20.25%; }
    }

    I still like the aspect-ratio-hack better, but see way more power the gradient solution over the clip-path. Although there is also power in inheriting the background color. But I more often need 2 colors in my design.. choices, choices..

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.