UI Components Medium
Popover
Floating popover panels that position themselves relative to a trigger — supports top, bottom, left, right placement with arrow pointer.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
text-align: center;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2.5rem;
}
.popover-grid {
display: flex;
gap: 1.5rem;
justify-content: center;
flex-wrap: wrap;
align-items: center;
}
.btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1.125rem;
border-radius: 0.625rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: #94a3b8;
transition: background 0.15s;
}
.btn:hover {
background: rgba(255, 255, 255, 0.08);
}
.btn--accent {
background: rgba(56, 189, 248, 0.12);
color: #38bdf8;
border-color: rgba(56, 189, 248, 0.2);
}
.btn--accent:hover {
background: rgba(56, 189, 248, 0.2);
}
/* ── Wrapper ── */
.popover-wrap {
position: relative;
display: inline-block;
}
/* ── Popover ── */
.popover {
position: absolute;
width: 240px;
background: #0d1117;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.875rem;
padding: 1rem;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
}
.popover-wrap.is-open .popover {
opacity: 1;
pointer-events: auto;
}
/* Placements */
.popover--bottom {
top: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(6px);
}
.popover-wrap.is-open .popover--bottom {
transform: translateX(-50%) translateY(0);
}
.popover--top {
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%) translateY(-6px);
}
.popover-wrap.is-open .popover--top {
transform: translateX(-50%) translateY(0);
}
.popover--left {
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(-6px);
}
.popover-wrap.is-open .popover--left {
transform: translateY(-50%) translateX(0);
}
.popover--right {
left: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(6px);
}
.popover-wrap.is-open .popover--right {
transform: translateY(-50%) translateX(0);
}
/* Arrows */
.popover-arrow {
position: absolute;
width: 10px;
height: 10px;
background: #0d1117;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.popover--bottom .popover-arrow {
top: -6px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
border-bottom: none;
border-right: none;
}
.popover--top .popover-arrow {
bottom: -6px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
border-top: none;
border-left: none;
}
.popover--left .popover-arrow {
right: -6px;
top: 50%;
transform: translateY(-50%) rotate(45deg);
border-left: none;
border-bottom: none;
}
.popover--right .popover-arrow {
left: -6px;
top: 50%;
transform: translateY(-50%) rotate(45deg);
border-right: none;
border-top: none;
}
/* Content */
.popover-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.popover-close {
background: none;
border: none;
color: #475569;
cursor: pointer;
font-size: 1.1rem;
line-height: 1;
padding: 0.1rem 0.25rem;
border-radius: 0.25rem;
transition: color 0.15s;
}
.popover-close:hover {
color: #f2f6ff;
}
.popover-body {
font-size: 0.8rem;
color: #64748b;
line-height: 1.5;
}
/* Rich form */
.popover--rich {
width: 260px;
}
.popover-form {
display: flex;
gap: 0.5rem;
margin-top: 0.875rem;
}
.pop-input {
flex: 1;
padding: 0.5rem 0.75rem;
background: #050910;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
color: #f2f6ff;
font-size: 0.8rem;
outline: none;
font-family: inherit;
}
.pop-input:focus {
border-color: #38bdf8;
}
.pop-submit {
padding: 0.5rem 0.875rem;
background: #38bdf8;
color: #0f172a;
border: none;
border-radius: 0.5rem;
font-size: 0.8rem;
font-weight: 700;
cursor: pointer;
}(function () {
var wraps = Array.from(document.querySelectorAll(".popover-wrap"));
function close(w) {
w.classList.remove("is-open");
var btn = w.querySelector("[aria-expanded]");
if (btn) btn.setAttribute("aria-expanded", "false");
}
function open(w) {
wraps.forEach(close);
w.classList.add("is-open");
var btn = w.querySelector("[aria-expanded]");
if (btn) btn.setAttribute("aria-expanded", "true");
}
wraps.forEach(function (w) {
var trigger = w.querySelector("[data-placement]");
if (trigger) {
trigger.addEventListener("click", function (e) {
e.stopPropagation();
w.classList.contains("is-open") ? close(w) : open(w);
});
}
w.querySelectorAll(".popover-close").forEach(function (btn) {
btn.addEventListener("click", function () {
close(w);
});
});
});
document.addEventListener("click", function () {
wraps.forEach(close);
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") wraps.forEach(close);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popover</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Popover</h1>
<p class="demo-sub">Floating panels in four placements — click trigger to toggle.</p>
<div class="popover-grid">
<!-- Top -->
<div class="popover-wrap" id="pw-top">
<button class="btn" aria-haspopup="true" aria-expanded="false" data-placement="top">Top</button>
<div class="popover popover--top" role="tooltip">
<div class="popover-arrow"></div>
<div class="popover-header"><strong>Top popover</strong><button class="popover-close" aria-label="Close">×</button></div>
<p class="popover-body">Content appears above the trigger with a downward-pointing arrow.</p>
</div>
</div>
<!-- Bottom -->
<div class="popover-wrap" id="pw-bottom">
<button class="btn" aria-haspopup="true" aria-expanded="false" data-placement="bottom">Bottom</button>
<div class="popover popover--bottom" role="tooltip">
<div class="popover-arrow"></div>
<div class="popover-header"><strong>Bottom popover</strong><button class="popover-close" aria-label="Close">×</button></div>
<p class="popover-body">Default placement — appears below the trigger.</p>
</div>
</div>
<!-- Left -->
<div class="popover-wrap" id="pw-left">
<button class="btn" aria-haspopup="true" aria-expanded="false" data-placement="left">Left</button>
<div class="popover popover--left" role="tooltip">
<div class="popover-arrow"></div>
<div class="popover-header"><strong>Left popover</strong><button class="popover-close" aria-label="Close">×</button></div>
<p class="popover-body">Floats to the left of the trigger element.</p>
</div>
</div>
<!-- Right -->
<div class="popover-wrap" id="pw-right">
<button class="btn" aria-haspopup="true" aria-expanded="false" data-placement="right">Right</button>
<div class="popover popover--right" role="tooltip">
<div class="popover-arrow"></div>
<div class="popover-header"><strong>Right popover</strong><button class="popover-close" aria-label="Close">×</button></div>
<p class="popover-body">Floats to the right of the trigger element.</p>
</div>
</div>
<!-- Rich with form -->
<div class="popover-wrap" id="pw-rich">
<button class="btn btn--accent" aria-haspopup="true" aria-expanded="false" data-placement="bottom">Subscribe ✦</button>
<div class="popover popover--bottom popover--rich" role="dialog" aria-label="Newsletter">
<div class="popover-arrow"></div>
<div class="popover-header"><strong>Stay in the loop</strong><button class="popover-close" aria-label="Close">×</button></div>
<p class="popover-body">Get weekly design & dev resources straight to your inbox.</p>
<div class="popover-form">
<input type="email" class="pop-input" placeholder="you@example.com" />
<button class="pop-submit">Subscribe</button>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Popover
Non-modal floating panels anchored to a trigger element — richer than a tooltip.
Placements
top · bottom · left · right
Each placement has a CSS arrow pointer via ::before border trick.
Features
- Toggle on trigger click
- Close via built-in × button, outside click, or
Escape - Rich content: title + body + optional actions
- Smooth fade-in transition