A Sass Mixin for CSS Triangles
There is a very popular CSS trick consisting on using a mix of solid and transparent borders on a 0 width, 0 height element in order to fake a triangle. If you don’t know technique yet, you can find more information about it in the following resources:
- CSS Triangle at CSS-Tricks
- CSS Triangle by David Walsh
- CSS Triangle generator
- CSS Arrow Please! (a CSS triangle generator)
While this technique comes with some quirks, it actually does the job well and has the benefit of being very compatible across the wide range of browsers we have to support.
Now the thing is, I never completely remember how to use this little piece of code. Which borders need to be transparent? Which one should be solid? I just can’t figure this out and I bet it’s the same for many of you as well. So this is typically the kind of thing you would like to automate using Sass.
There are probably as many CSS triangle Sass mixins as there are Sass developers. What if I showed you my own mixin to deal with CSS triangles/arrows in CSS?
What Do We Need?
Before digging into the code, it would be a good idea to check what we need in order to make a triangle out of CSS. First: a direction. We need to define a direction for our arrow, either top
, right
, bottom
, or left
. Also, what about a position? Like at 1.5em from the top, 100% from the left, for instance. Also we could define a color, and perhaps a size as well, although those 2 arguments could have defaults.
So in the end, our mixin is a short way for us to say Generate a triangle pointing to this direction, at this spot, in this color, and in the following dimensions. Sounds good, right?
Note that in order to avoid using extra markup, I like to use pseudo-elements for triangles. For instance:
.element {
/* Container of some kind */
&::before {
/* Including triangle mixin */
}
}
Using Our Toolbox
Last week, I wrote about some functions and mixins to kickstart your project. In case you haven’t read that article yet, go ahead. I’ll wait.
Okay, done? What if we reused some of our mixins in order to make this triangle mixin as sharp and clean as possible? For instance, we could use the size()
mixin so we don’t have to define both the width and the height. We could also keep a consistent API for positioning stuff and make use of the absolute()
mixin (more about that mixin here).
In order to have all the code in this post, allow me to include the 3 mixins here as well:
// Sizing stuff
@mixin size($width, $height: $width) {
width: $width;
height: $height;
}
// Positioning stuff
@mixin position($position, $args) {
@each $o in top right bottom left {
$i: index($args, $o);
@if $i and $i + 1 <;= length($args) and type-of(nth($args, $i + 1)) == number {
#{$o}: nth($args, $i + 1);
}
}
position: $position;
}
// Absolutely positioning stuff
@mixin absolute($args) {
@include position(absolute, $args);
}
Also, we will need the opposite-direction()
function; either the one from Compass or the one I wrote in my previous post.
Building the Mixin
Now that we have all the utilities we need, let’s begin creating our triangle mixin. First, let’s create the mixin’s skeleton.
The Mixin Signature
Here is what our mixin will look like:
@mixin triangle(
$direction,
$position,
$color: currentColor,
$size: 1em
) {
/* Mixin content */
}
Both the direction and the position are non-optional parameters. Meanwhile the color will default to the currentColor
CSS value, which is always better than falling back to black. Regarding the size, since all triangles in my project were normalized to have the same size, I made size an optional parameter but feel free to make it non-optional if you want.
For those of you who like very formal signature syntax, here is what that would look like:
triangle(string $direction, list $position, color $color: currentColor, number $size: 1em)
The Mixin Core
Let’s deal with the easiest part first: The mixin core. In other words, the dimensions and position of the triangle. We will take care of the borders later.
@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
@include absolute($position);
@include size(0);
content: '';
z-index: 2;
/* Border stuff */
}
Pretty clean, isn’t it? Here’s what it does:
- Positions the element thanks to the
absolute()
mixin. - Makes the element
0
width,0
height. - In case it is a pseudo-element (almost always the case), makes it appear using the
content
property. - Uses
z-index
to ensure it’s on top of the default layer (I suppose you could remove this if you want).
Borders
Okay, we’re done the easiest part. Mostly because we had custom mixins easing the pain. Now we need to deal with all the borders. This is where opposite-direction
comes into play. Using an example, let’s recall how this works. Consider that we want to define a triangle pointing to the right:
- Right border should not be defined
- Left border should be colored
- Top border should be transparent
- Bottom border should be transparent
This leads us to the following code:
@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
/* Core stuff */
border-#{opposite-position($direction)}: $size * 1.5 solid $color;
$perpendicular-border: $size solid transparent;
@if $direction == top or $direction == bottom {
border-left: $perpendicular-border;
border-right: $perpendicular-border;
}
@else if $direction == right or $direction == left {
border-bottom: $perpendicular-border;
border-top: $perpendicular-border;
}
}
At first, we define the opposite border. To do so, we use the opposite-direction()
function. In our previous example, opposite-direction
would return left
, hence the property would be border-left
. Regarding the size, it occurred to me that it looked better when slightly increasing the size of this border. But that’s rather arbitrary, so feel free to update it if you don’t like it.
Finally, we define perpendicular borders. If the direction is either top
or bottom
, those borders are border-left
and border-right
and if the direction is left
or right
, perpendicular borders are border-top
and border-bottom
. Both borders should be transparent.
Error Handling
As always, we shouldn’t forget to handle errors in our mixin. To keep the mixin simple, I made sure the direction is okay. That is, it has to be one of the 4 common offsets. I believe it’s important to make this check because one could decide to pass an angle (e.g. 42deg
), as is done for the linear-gradient()
function.
@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
@if not index(top right bottom left, $direction) {
@warn "Direction must be one of `top`, `right`, `bottom` or `left`; currently `#{$direction}`.";
}
@else {
/* Mixin content */
}
}
Note how we use the index()
function to see if the direction is one of the 4 offsets rather than manually checking each one with ==
.
In a Sass 3.3 environment, we could make our function slightly more bulletproof by lower-casing the direction first: $direction: to-lower-case($direction)
. This would prevent an error in case the direction is not completely lower-cased. Details matter.
I have very recently written about how to support multiple versions of Sass at the same time. I highly recommand you read the article, but basically it would look like this:
@mixin triangle($direction, $position, $color: currentcolor, $size: 1em) {
$direction: if(function-exists("to-lower-case") == true, to-lower-case($direction), $direction);
@if not index(top right bottom left, $direction) {
@warn "Direction must be one of `top`, `right`, `bottom` or `left`; currently `#{$direction}`.";
}
@else {
/* Mixin content */
}
}
Regarding the position, it should be the job of the position()
mixin to deal with error handling, instead of the triangle()
mixin. About the color and the size, we could probably check the data type but I felt like checking the position was enough for this demonstration.
Usage and Example
Using it is quite straightforward. But remember, since the element that includes the triangle mixin is being absolutely positioned, its parent should have position: relative
.
/**
* 1. Enable absolute positioning for pseudo-element
* 2. Using a pseudo-element to generate the arrow
* 3. Same as @include triangle(bottom,top 100% left 1em, $color);
*/
.tooltip {
$color: #3498db;
position: relative; /* 1 */
background: $color;
padding: .5em;
border-radius: .15em;
color: white;
text-align: center;
&::before { /* 2 */
@include triangle(
$direction : bottom,
$position : top 100% left 1em,
$color : $color
); /* 3 */
}
}
And here is a live demo for you to play with:
See the Pen Jabzj by SitePoint (@SitePoint) on CodePen.
Final Thoughts
In the end, the mixin is quite simple, don’t you think? Not only does it reuse existing mixins to avoid having to code annoying things manually, but it also makes use of clever features like opposite-direction
and the @if
directive to make our code as DRY as possible.
I hope you like it. Let us know if you find it useful.