Skip to content

Instantly share code, notes, and snippets.

@stanwmusic
Created August 18, 2026 19:19
Show Gist options
  • Select an option

  • Save stanwmusic/178d1418a7b838821f9a0aef4a2ec449 to your computer and use it in GitHub Desktop.

Select an option

Save stanwmusic/178d1418a7b838821f9a0aef4a2ec449 to your computer and use it in GitHub Desktop.
CSS Tooltips (Before & After)
<header>
<h1>CSS Tooltips</h1>
<p>Just CSS ::before (bubble) and ::after (arrow). And a sprinkle of JavaScript.</p>
</header>
<div class="section">
<div class="section-label">01 — Positions</div>
<div class="demo-grid">
<button class="btn" data-tip="I appear on top" data-pos="top">Top</button>
<button class="btn" data-tip="I appear below" data-pos="bottom">Bottom</button>
<button class="btn" data-tip="I appear left" data-pos="left">Left</button>
<button class="btn" data-tip="I appear right" data-pos="right">Right</button>
</div>
</div>
<div class="section">
<div class="section-label">02 — Variants</div>
<div class="demo-grid">
<button class="btn" data-tip="Default style">Default</button>
<button class="btn" data-tip="Highlighted action" data-variant="accent">Accent</button>
<button class="btn" data-tip="This will delete everything" data-variant="danger">Danger</button>
<button class="btn" data-tip="More info available" data-variant="info">Info</button>
</div>
</div>
<div class="section">
<div class="section-label">03 — Icon buttons</div>
<div class="demo-grid">
<span class="icon-btn" tabindex="0" data-tip="Copy to clipboard"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M620-163 450-333l56-56 114 114 226-226 56 56-282 282Zm220-397h-80v-200h-80v120H280v-120h-80v560h240v80H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h167q11-35 43-57.5t70-22.5q40 0 71.5 22.5T594-840h166q33 0 56.5 23.5T840-760v200ZM508.5-771.5Q520-783 520-800t-11.5-28.5Q497-840 480-840t-28.5 11.5Q440-817 440-800t11.5 28.5Q463-760 480-760t28.5-11.5Z"/></svg></span>
<span class="icon-btn" tabindex="0" data-tip="Download" data-pos="bottom"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/></svg></span>
<span class="icon-btn" tabindex="0" data-tip="Share link" data-pos="right"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M318-120q-82 0-140-58t-58-140q0-40 15-76t43-64l134-133 56 56-134 134q-17 17-25.5 38.5T200-318q0 49 34.5 83.5T318-200q23 0 45-8.5t39-25.5l133-134 57 57-134 133q-28 28-64 43t-76 15Zm79-220-57-57 223-223 57 57-223 223Zm251-28-56-57 134-133q17-17 25-38t8-44q0-50-34-85t-84-35q-23 0-44.5 8.5T558-726L425-592l-57-56 134-134q28-28 64-43t76-15q82 0 139.5 58T839-641q0 39-14.5 75T782-502L648-368Z"/></svg></span>
<span class="icon-btn" tabindex="0" data-tip="Delete permanently" data-variant="danger"></span>
<span class="icon-btn" tabindex="0" data-tip="Settings" data-pos="left"></span>
<span class="icon-btn" tabindex="0" data-tip="Bookmark this" data-variant="accent"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m438-400 198-198-57-56-141 141-57-57-57 57 114 113ZM200-120v-640q0-33 23.5-56.5T280-840h400q33 0 56.5 23.5T760-760v640L480-240 200-120Zm80-122 200-86 200 86v-518H280v518Zm0-518h400-400Z"/></svg></span>
</div>
</div>
<div class="section">
<div class="section-label">04 — Inline text tooltips</div>
<p class="prose">
Pseudo-elements are
<span tabindex="0" data-tip="::before and ::after — they exist in the render tree but not the DOM">ghost elements</span>
generated by CSS. They require a
<span tabindex="0" data-tip="Even an empty string content: '' works for shape-based tricks">content property</span>
to render, and each element gets exactly
<span tabindex="0" data-tip="One ::before and one ::after — that's your lot">two of them</span>.
Together they're enough to build a complete
<span tabindex="0" data-tip="::before is the bubble, ::after is the arrow nub" data-variant="accent">tooltip system</span>
with zero extra markup.
</p>
</div>
<p class="note"><span>::before</span> = bubble &nbsp;·&nbsp; <span>::after</span> = arrow &nbsp;·&nbsp; no JS, no extra HTML</p>
document.addEventListener("click", (e) => {
if (!e.target.closest("[data-tip]")) {
document.activeElement.blur();
}
});
@import url ('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=DM+Sans:wght@300;400;500;600&display=swap')
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0d0d0d;
--surface: #161616;
--border: #2a2a2a;
--text: #e8e8e8;
--muted: #666;
--accent: #e8ff47;
--accent-dim: rgba(232, 255, 71, 0.12);
--tip-bg: #1e1e1e;
--tip-text: #f0f0f0;
--tip-border: #333;
--radius: 70px;
--radius1: 15px;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Sans', sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 24px;
gap: 64px;
}
header {
text-align: center;
}
header h1 {
font-family: 'DM Mono', monospace;
font-weight: 400;
font-size: clamp(1.1rem, 3vw, 1.5rem);
letter-spacing: 0.08em;
color: var(--accent);
}
header p {
margin-top: 10px;
font-size: 0.85rem;
color: var(--muted);
font-weight: 200;
}
.section {
width: 100%;
max-width: 700px;
display: flex;
flex-direction: column;
gap: 20px;
}
.section-label {
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
border-bottom: 1px solid var(--border);
padding-bottom: 8px;
}
.demo-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
}
[data-tip] {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: default;
}
[data-tip]::before {
content: attr(data-tip);
position: absolute;
background: var(--tip-bg);
color: var(--tip-text);
font-family: 'DM Mono', monospace;
font-size: 0.72rem;
font-weight: 400;
white-space: nowrap;
padding: 6px 12px;
border-radius: var(--radius1);
border: 1px solid var(--tip-border);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.18s ease, transform 0.18s ease;
z-index: 10;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
[data-tip]::after {
content: '';
position: absolute;
pointer-events: none;
opacity: 0;
transition: opacity 0.18s ease, transform 0.18s ease;
z-index: 11;
width: 0;
height: 0;
}
[data-tip]:hover::before,
[data-tip]:focus::before,
[data-tip]:hover::after,
[data-tip]:focus::after {
opacity: 1;
transform: translateY(0);
}
[data-tip]:focus {
outline: none;
}
[data-tip]:not([data-pos])::before,
[data-tip][data-pos="top"]::before {
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(4px);
}
[data-tip]:not([data-pos]):hover::before,
[data-tip]:not([data-pos]):focus::before,
[data-tip][data-pos="top"]:hover::before,
[data-tip][data-pos="top"]:focus::before {
transform: translateX(-50%) translateY(0);
}
[data-tip]:not([data-pos])::after,
[data-tip][data-pos="top"]::after {
bottom: calc(100% + 4px);
left: 50%;
transform: translateX(-50%) translateY(4px);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid var(--tip-border);
}
[data-tip]:not([data-pos]):hover::after,
[data-tip]:not([data-pos]):focus::after,
[data-tip][data-pos="top"]:hover::after,
[data-tip][data-pos="top"]:focus::after {
transform: translateX(-50%) translateY(0);
}
[data-tip][data-pos="bottom"]::before {
top: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(-4px);
}
[data-tip][data-pos="bottom"]:hover::before,
[data-tip][data-pos="bottom"]:focus::before {
transform: translateX(-50%) translateY(0);
}
[data-tip][data-pos="bottom"]::after {
top: calc(100% + 4px);
left: 50%;
transform: translateX(-50%) translateY(-4px);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid var(--tip-border);
}
[data-tip][data-pos="bottom"]:hover::after,
[data-tip][data-pos="bottom"]:focus::after {
transform: translateX(-50%) translateY(0);
}
[data-tip][data-pos="left"]::before {
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(4px);
}
[data-tip][data-pos="left"]:hover::before,
[data-tip][data-pos="left"]:focus::before {
transform: translateY(-50%) translateX(0);
}
[data-tip][data-pos="left"]::after {
right: calc(100% + 4px);
top: 50%;
transform: translateY(-50%) translateX(4px);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid var(--tip-border);
}
[data-tip][data-pos="left"]:hover::after,
[data-tip][data-pos="left"]:focus::after {
transform: translateY(-50%) translateX(0);
}
[data-tip][data-pos="right"]::before {
left: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(-4px);
}
[data-tip][data-pos="right"]:hover::before,
[data-tip][data-pos="right"]:focus::before {
transform: translateY(-50%) translateX(0);
}
[data-tip][data-pos="right"]::after {
left: calc(100% + 4px);
top: 50%;
transform: translateY(-50%) translateX(-4px);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid var(--tip-border);
}
[data-tip][data-pos="right"]:hover::after,
[data-tip][data-pos="right"]:focus::after {
transform: translateY(-50%) translateX(0);
}
.btn {
padding: 10px 22px;
border-radius: var(--radius);
font-family: 'DM Sans', sans-serif;
font-size: 0.85rem;
font-weight: 500;
border: 1px solid var(--surface);
background: var(--surface);
color: var(--text);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.btn:hover {
border-color: var(--muted);
color: var(--surface);
background: var(--muted);
border-radius: var(--radius1);
}
.icon-btn {
width: 50px;
height: 45px;
border-radius: var(--radius);
border: 1px solid var(--surface);
background: var(--surface);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.icon-btn:hover {
border-color: var(--muted);
color: var(--surface);
background: var(--muted);
border-radius: var(--radius1);
}
[data-tip][data-variant="accent"]::before {
background: var(--accent);
color: #0d0d0d;
border-color: var(--accent);
font-weight: 500;
}
[data-tip][data-variant="accent"]::after {
border-top-color: var(--accent) !important;
}
[data-tip][data-variant="danger"]::before {
background: #ff4444;
color: #fff;
border-color: #ff4444;
}
[data-tip][data-variant="danger"]::after {
border-top-color: #ff4444 !important;
}
[data-tip][data-variant="info"]::before {
background: #2563eb;
color: #fff;
border-color: #2563eb;
}
[data-tip][data-variant="info"]::after {
border-top-color: #2563eb !important;
}
.prose {
font-size: 1rem;
font-weight: 100;
line-height: 1.9;
color: var(--muted);
max-width: 100%;
}
.prose [data-tip] {
color: var(--text);
border-bottom: 1px dashed var(--muted);
cursor: help;
display: inline;
}
.prose [data-tip]::before {
white-space: normal;
width: 200px;
text-align: left;
line-height: 1.5;
}
.note {
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
color: var(--muted);
font-weight: 100;
text-align: center;
letter-spacing: 0.05em;
}
.note span {
color: var(--accent);
}
<link href="https://fonts.googleapis.com/css2?family=Honk:MORF@15&amp;display=swap" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment