We've already looked at CSS only modals, there is another similar UI pattern traditionally implemented via JavaScript which could be done using only CSS. The humble tooltip.
In theory, they're very simple widgets. You mouseover an element, and a little box with some text is overlaid in proximity to that element.
The JavaScript way would require a mouseover
and mouseout
event listener to be added on the trigger element so that the tip can be shown or hidden. In more complex setups, the contents of the tooltip might be fetched or lookedup and the tooltip DOM element constructed - so in practice, there's actually a lot of main thread work going on here.
The Tooltip Contents
First, let's tackle the contents of the tooltip. You could simply add an element such as a div
with the contents, however, assuming the content is simply text, there's a better option.
Let's say we have a span
element in a paragraph and we want to show a tooltip on hover.
We'll store the tooltip content in a data-*
attribute.
<p>
<span data-title="Look Ma! No JS!">Hover for tooltip...</span>
</p>
Display on hover
We can now use a CSS psuedo-element
and the contents of the data-title
attribute, to style and position the text as a tooltip; we'll use a ::before
element:
[data-title] {
/* allows us to position the tooltip correctly */
position: relative;
}
[data-title]:hover::before {
content: attr(data-title);
display: inline-flex;
position: absolute;
bottom: 100%;
left: 50%;
width: max-content;
padding: 10px;
transform: translate(-50%, 0);
background: rgba(#ff7705, 0.8);
}
Hover for tooltip...
Pointer
If we want a pointer/chevron on the tooltip, we can use an ::after
element, presented as a triangle using the border trick - we'll also need to adjust the tooltip's bottom position to account for the height of pointer:
[data-title]:hover::before {
/* ... */
bottom: calc(100% + 10px);
}
[data-title]:hover::after {
content: "";
display: block;
position: absolute;
bottom: 100%;
left: 50%;
width: 0;
height: 0;
transform: translate(-50%, 0);
border-width: 10px 10px 0;
border-style: solid;
border-color: rgba(#ff7705, 0.8) transparent transparent;
}
Hover for tooltip...
Now for the fun part - visual effects! ✨.
Transitions
We usually want our tooltips to have a tween effect in and out; but we can't use the display
property and add transitions, usually they are skipped. So just like we did with the CSS only modals, we'll rely on the transform
property to hide and show the tooltip. We'll also use the opacity
and transform
properties for the appearance and dissappearance effects, though we won't apply any tweening on the transform
. Let's refactor the current styles:
[data-title] {
/* allows us to position the tooltip correctly */
position: relative;
}
[data-title]::before {
content: attr(data-title);
display: inline-flex;
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
width: max-content;
padding: 10px;
background: rgba(#ff7705, 0.8);
/* setup effects */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 300ms;
/* initial hidden state */
transform:
translate(-50%, 0)
scale(0);
opacity: 0;
}
[data-title]:hover::before {
/* adjust effects timings */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 50ms;
/* visible state */
transform:
translate(-50%, 0)
scale(1);
opacity: 1;
}
[data-title]::after {
content: "";
display: block;
position: absolute;
bottom: 100%;
left: 50%;
width: 0;
height: 0;
border-width: 10px 10px 0;
border-style: solid;
border-color: rgba(#ff7705, 0.8) transparent transparent;
/* setup effects */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 300ms;
/* initial hidden state */
transform:
translate(-50%, 0)
scale(0);
opacity: 0;
}
[data-title]:hover::after {
/* adjust effects timings */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 50ms;
/* visible state */
transform:
translate(-50%, 0)
scale(1);
opacity: 1;
}
You'll notice we've used delays on the transitions, a base 50ms
delay prevents flashing as the pointer travels across the tooltip trigger - this ensures the tooltip only appears when the user has settled on the trigger, that is, shows intent that they want to trigger the tooltip.
Our 0ms
transition timing on the transform
means that on hover, the tooltip element shows but with the opacity
still at 0
. The transition to 1
then kicks in and we get the fade in effect.
Hover for tooltip...
Display on touch
Here's where tooltips get a little tricky.
We are certainly in a mobile-first world. Most websites will find their user base is largely visiting on a touch enabled device, such as smartphones or tablets - such devices often do not implement a hover
state and instead emulate the behaviour. UI interactions using the :hover
psuedo-class are usually applied when an element is clicked or tapped, and removed when any other element is clicked.
However, this is not a universal or standardised behaviour, so some devices/browsers/OSs may behave differently, while others may just ignore the hover
state altogether.
To ensure our CSS only solution works as needed for touch devices, we should explicitly apply the emulated behaviour. To cover both, we can use media queries
to apply different styles based on hover
support.
Let's wrap our :hover
rules in a media query for clients that support it:
/* user agent supports hover */
@media (hover: hover) {
[data-title]:hover::before {
/* adjust effects timings */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 50ms;
/* visible state */
transform:
translate(-50%, 0)
scale(1);
opacity: 1;
}
[data-title]:hover::after {
/* adjust effects timings */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 50ms;
/* visible state */
transform:
translate(-50%, 0)
scale(1);
opacity: 1;
}
}
The above styles will apply only when the user's primary input mechanism can hover over elements, such as a mouse.
Now for devices that do not support hover
, we first need to make a very tiny tweak to the HTML markup; we need to give the tooltip trigger elements a tabindex
attribute set to 0
. This tells the browser the element is focusable.
<p>
<span data-title="Look Ma! No JS!" tabindex="0">Hover for tooltip...</span>
</p>
Now, we apply the same style rules for the hover states, but using the :focus
selector, inside a media query for clients that do not support it:
/* user agent DOES NOT support hover */
@media (hover: none) {
[data-title]:focus::before {
/* adjust effects timings */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 50ms;
/* visible state */
transform:
translate(-50%, 0)
scale(1);
opacity: 1;
}
[data-title]:focus::after {
/* adjust effects timings */
transition:
opacity 250ms ease-out 50ms,
transform 0ms ease-out 50ms;
/* visible state */
transform:
translate(-50%, 0)
scale(1);
opacity: 1;
}
}
Touch only tooltip*...
* does not apply hover rules
There you have it, our tooltips will work consistently across mobile and desktop, applied using only CSS.