I first wrote this article two years ago. Time flies! I’ve completely rewritten and updated this article about custom tooltips. They are a very common user interface element and I’ve used them in large number of my projects. In this article I’m going to make custom CSS animated tooltips without any JavaScript code. And with Sass mixins to make reusable and easy to customize components.

Markup

I’m going to start with the HTML structure. A container with the has-tooltip class to enable the tooltip, and the nested tooltip element.

Here’s a tooltip enabled on a link:

<a href="#" class="has-tooltip">  
    This is a link with a tooltip
    <span class="tooltip" role="tooltip">
        This is the content of the tooltip
    </span>
</a>  

And on a button:

<button class="has-tooltip">  
    This is a button with a tooltip
    <span class="tooltip" role="tooltip">
        This is the content of the tooltip
    </span>
</button>  

The role="tooltip" attribute on the tooltip element is an ARIA accessibility attribute that defines the type of the element. It may be used when reading the text of a web page with a screen reader for example.

Mixins in Sass

I’m implementing the Sass code with generic mixins that can be applied with different parameters. This offers the possibility to have different styles of tooltips in a user interface. For example we could have tooltips on links and other tooltips on form input elements with a different styling.

I wrote my own mixins to draw CSS triangles in this article on my blog. Here I’m going to use a similar approach to draw the body of the tooltip and an arrow.

@mixin tooltip(
        $tooltip-background-color: white,
        $tooltip-arrow-base: 2em,
        $tooltip-arrow-length: 1em,
        $tooltip-border-width: 0,
        $tooltip-border-color: transparent,
        $tooltip-transition-out-duration: .3s
    ) {
    // Sizing and positioning
    box-sizing: border-box;
    position: absolute;
    left: 50%;
    bottom: 100%; // displayed on top
    margin-bottom: $tooltip-arrow-length;

    // Text and color properties.
    background-color: $tooltip-background-color;
    text-align: center;

    // Animated properties.
    visibility: hidden;
    opacity: 0;
    transform: translate(-50%, $tooltip-arrow-length);
    transition:
        visibility 0s linear $tooltip-transition-out-duration,
        opacity $tooltip-transition-out-duration ease-in 0s,
        transform $tooltip-transition-out-duration ease-in 0s;

    // Disable events.
    pointer-events: none;

    &::after {
        content: "";
        position: absolute;
        top: 100%; left: 50%;
        width: 0; height: 0;
        // Draw a triangle.
        border-top: ($tooltip-arrow-length - $tooltip-border-width) solid $tooltip-background-color;
        border-left: ($tooltip-arrow-base / 2 - $tooltip-border-width) solid transparent;
        border-right: ($tooltip-arrow-base / 2 - $tooltip-border-width) solid transparent;
        // Center the triangle.
        margin-left: ($tooltip-border-width - $tooltip-arrow-base / 2);
    }

    // Specific styles when the tooltip has a border
    @if $tooltip-border-width {
        border: $tooltip-border-width solid $tooltip-border-color;

        &::before {
            content: "";
            position: absolute;
            top: 100%; left: 50%;
            width: 0; height: 0;
            // Draw another triangle to simulate a border.
            border-top: $tooltip-arrow-length solid $tooltip-border-color;
            border-left: ($tooltip-arrow-base / 2) solid transparent;
            border-right: ($tooltip-arrow-base / 2) solid transparent;
            // Center the triangle.
            margin-left: ($tooltip-arrow-base / -2);
        }
    }
}

On the container element that has a tooltip attached the display is triggered on hover or when the element is focused while navigating with the keyboard.

@mixin has-tooltip($tooltip-transition-in-duration: .3s) {
    position: relative;
    // Prevent the tooltip from being clipped by its container.
    overflow: visible;

    // Display the tooltip on hover and on focus.
    &:hover, &:focus {
        .tooltip {
            visibility: visible;
            opacity: 1;
            transform: translate(-50%, 0); // horizontally centered
            transition:
                visibility 0s linear 0s,
                opacity $tooltip-transition-in-duration ease-out 0s,
                transform $tooltip-transition-in-duration ease-out 0s;
        }
    }
}

Animations and effects in CSS

For a smooth visual effect I’m animating the translation, opacity and visibility of the tooltip element. The translation and opacity properties are easy to animate as they have continuous values. But the visibility has a discrete value and it’s a bit trickier to use.

Animating the visibility

In CSS the display property of an element can’t be animated. That’s why we can’t use a CSS transition that changes the display property of an element from none to block.

But the visibility property can be animated. The visibility can be changed from hidden to visible through a transition. Here’s the full list of animated properties in CSS.

Implementation in CSS

The transition in to display the tooltip:

  • Enable transitions on the translation and opacity properties for a fade in and slide in effect.
  • Enable the visibility of the tooltip immediately at the very beginning of the transition.

The transition out to hide the tooltip:

  • Enable transitions on the translation and opacity properties for a fade out and slide out effect.
  • Disable the visibility of the tooltip at the end of the transition. Otherwise the tooltip would be hidden immediately and we could’t see the animation.

The syntax of the shorthand transition property in CSS:

.element {
    transition:
        <transition-property>
        <transition-duration>
        <transition-timing-function>
        <transition-delay>;
}

Several transitions can be enabled on different properties:

.element {
    transition:
        property 1s linear 0s,
        another-property 1s linear 0s;
}

This is slimmed-down version of the plain CSS code to animate the tooltip with an animation duration of 300 milliseconds (0.3 seconds):

.tooltip {
    visibility: hidden;
    opacity: 0;
    transform: translateY(20px);
    transition:
        visibility 0s linear .3s, /* change visibility at the end */
        opacity .3s ease-in 0s,
        transform .3s ease-in 0s;
}

.has-tooltip:hover .tooltip {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    transition:
        visibility 0s linear 0s,  /* change visibility immediately */
        opacity .3s ease-out 0s,
        transform .3s ease-out 0s;
}

For the visibility property the trick is to use 0 seconds for the transition duration and apply a delay when hiding. The full documentation about the transition property is here.

Easing functions

A few built-in animation easing functions are available in CSS:

  • With ease-in the animation is smooth at the beginning, accelerates progressively and stops suddenly at the end.
  • With ease-out the animation starts suddenly and is smooth at the end of the transition duration.
  • With ease-in-out we have a smooth start and smooth end.

We might also use cubic Bézier curves to define completely custom transition effects, but that would be a whole subject for another article!

Browser compatibility

On the demo of this article I’ve enabled autoprefixer on CodePen to automatically add -webkit- prefixes to make the transform and transition properties work on older WebKit based browsers like older Safari and on older Android devices. This generates a quite verbose CSS code as you can see if you activate the compiled CSS view! Anyway for this implementation I’m targeting mainly the recent versions of Chrome, Firefox, Safari and Edge.

Generating the CSS styles

Applying the mixin on the container that has a tooltip attached:

.has-tooltip {
    @include has-tooltip($tooltip-transition-in-duration: .3s);
}

And now we must include the mixin of the tooltip itself. I prefer using explicit keyword arguments when there are a lot of parameters. This is easier to read and we may omit some of them since I’ve defined default values for arguments in the definition of the mixin. In this case the parameters in Sass behave like an unordered list.

.tooltip {
    @include tooltip(
        $tooltip-background-color: white,
        $tooltip-arrow-base: 1.5em,
        $tooltip-arrow-length: .75em,
        $tooltip-border-width: .05em,
        $tooltip-border-color: #999,
        $tooltip-transition-out-duration: .2s
    );
    min-width: 12em;
    padding: .5em .75em;
    box-shadow: 0 .05em .15em rgba(black, .1);
    color: #333;
}

Demo

Here’s the preview, if you want to play with the code the demo is here on CodePen.

See the Pen Tooltips with Sass Mixins by Frederic Perrin (@blustemy) on CodePen.