Tooltips
CSS-only tooltips on four sides, plus the anchor-positioning version.
<div class="demo">
<button class="tip" data-tip="Copies the formatted result to your clipboard" data-side="top">Top</button>
<button class="tip" data-tip="Saves the output as an .html file" data-side="right">Right</button>
<button class="tip" data-tip="Clears both panels and starts again" data-side="bottom">Bottom</button>
<button class="tip" data-tip="Moves the result back into the input" data-side="left">Left</button>
</div>
<p class="note">
An inline hint on <abbr class="tip" data-tip="Cumulative Layout Shift — how much the page moves while loading"
data-side="top">CLS</abbr>, which is what <code>abbr</code> is for.
</p>* { box-sizing: border-box; margin: 0; }
body { font: 16px/1.6 system-ui, -apple-system, sans-serif; padding: 60px 24px; background: #fff; }
.demo { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-bottom: 40px; }
.note { text-align: center; color: #5b6b83; font-size: .94rem; }
.note code { background: #eef1f6; padding: 1px 5px; border-radius: 4px; font-size: .9em; }
button.tip {
font: inherit; font-size: .92rem;
padding: 10px 18px;
border: 1px solid #d9e0ea;
border-radius: 9px;
background: #fff;
color: #16202e;
cursor: pointer;
}
button.tip:hover { background: #f6f8fb; }
abbr.tip { text-decoration: underline dotted; text-underline-offset: 3px; cursor: help; }
/* ============ the tooltip ============ */
.tip { position: relative; }
.tip::after,
.tip::before {
position: absolute;
opacity: 0;
pointer-events: none;
transition: opacity .14s ease;
z-index: 10;
}
/* the bubble */
.tip::after {
content: attr(data-tip);
width: max-content;
max-width: 220px;
padding: 7px 11px;
border-radius: 7px;
background: #16202e;
color: #fff;
font-size: .8rem;
font-weight: 400;
line-height: 1.45;
white-space: normal;
text-align: left;
}
/* the arrow */
.tip::before {
content: "";
border: 6px solid transparent;
}
/* show on hover and on keyboard focus — focus is the half everyone forgets */
.tip:hover::after, .tip:hover::before,
.tip:focus-visible::after, .tip:focus-visible::before { opacity: 1; }
/* ---- positions ---- */
[data-side="top"]::after { bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%); }
[data-side="top"]::before { bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%); border-top-color: #16202e; }
[data-side="bottom"]::after { top: calc(100% + 9px); left: 50%; transform: translateX(-50%); }
[data-side="bottom"]::before { top: calc(100% + 3px); left: 50%; transform: translateX(-50%); border-bottom-color: #16202e; }
[data-side="left"]::after { right: calc(100% + 9px); top: 50%; transform: translateY(-50%); }
[data-side="left"]::before { right: calc(100% + 3px); top: 50%; transform: translateY(-50%); border-left-color: #16202e; }
[data-side="right"]::after { left: calc(100% + 9px); top: 50%; transform: translateY(-50%); }
[data-side="right"]::before { left: calc(100% + 3px); top: 50%; transform: translateY(-50%); border-right-color: #16202e; }
@media (prefers-reduced-motion: reduce) {
.tip::after, .tip::before { transition: none; }
}How it works
The whole thing is two pseudo-elements and one attribute. content: attr(data-tip) pulls the text out of the markup, so there is no extra element to position and nothing to keep in sync.
The arrow is a border trick. A zero-size element with a 6px transparent border on all sides, then one side given a colour. The coloured side points away from the element, which is why the top tooltip uses border-top-color.
focus-visible, not just hover. A tooltip that only appears on hover is invisible to anyone using a keyboard, and that is most tooltip implementations. Two extra selectors fix it.
width: max-content with a max-width. Without max-content the bubble inherits the width of its parent, so a tooltip on a narrow icon button wraps to one word per line. With it, short text stays on one line and long text wraps at 220 pixels.
pointer-events: none stops the bubble intercepting the mouse. Without it, a tooltip that appears under the cursor causes a hover flicker loop.
Accessibility notes
Never put essential information in a tooltip. It is invisible on touch until tapped, invisible to many screen reader configurations, and gone the moment focus moves. If someone needs it to complete a task, it belongs on the page.
attr() content is not reliably announced by screen readers. For a control that genuinely needs a description, add aria-describedby pointing at real text, or use aria-label when the tooltip is the button's only name — which is how the icon-only buttons in the button set handle it.
The <abbr> example is the one case where this markup is genuinely correct: the title attribute on an abbreviation is a native tooltip, and the dotted underline plus cursor: help is the long-standing convention for it.
WCAG 1.4.13 requires that content shown on hover can be dismissed, stays visible while the pointer is over it, and does not disappear on its own. This passes the first two; for the third, do not add a timeout.
Making it yours
The big limitation: a pure CSS tooltip cannot escape a parent with overflow: hidden, and it cannot flip sides when it would run off the edge of the screen. For tooltips inside a scrolling panel or near a viewport edge, you need either the new CSS anchor positioning with position-try-fallbacks, or a positioning library such as Floating UI.
CSS anchor positioning solves this natively, and where it is supported you can replace the whole block with anchor-name on the trigger and position-anchor plus position-area on the bubble. Support is still uneven, which is why the version above uses the technique that works everywhere.
On touch there is no hover, so the tooltip appears on tap and stays until the next tap elsewhere. That is usually acceptable for a hint and useless for anything important.
Related templates
Button set
Primary, secondary, ghost and danger, in three sizes, with every state covered.
HTMLCSSTailwindLoaders and skeletons
Six loading indicators in pure CSS, plus the skeleton pattern that usually beats all of them.
HTMLCSSModal dialog
A modal built on the native dialog element — focus trap and backdrop included, for free.
HTMLCSSJavaScriptAccordion and FAQ
Built on details and summary — open, close and keyboard support with no JavaScript.
HTMLCSSCheck your version
Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.