Bootstrap 5 Dropdown and Tooltips with CSS Anchor Positioning
An editor toolbar whose tooltips and dropdown menus are placed by CSS anchor positioning and the Popover API, flipping at screen edges with no positioning script. Browsers without anchor positioning get a working fallback.
<section class="wdw-stage">
<div class="container" style="max-width:860px">
<div class="wdw-card ap-card">
<div class="ap-bar d-flex align-items-center gap-2 p-3 border-bottom" role="toolbar" aria-label="Formatting">
<span class="ap-tip">
<button type="button" class="ap-icon" aria-label="Bold" aria-pressed="false" aria-describedby="tip-bold" data-toggle-btn>
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/></svg>
</button>
<span role="tooltip" id="tip-bold" class="ap-tooltip">Bold <kbd>Ctrl B</kbd></span>
</span>
<span class="ap-tip">
<button type="button" class="ap-icon" aria-label="Italic" aria-pressed="false" aria-describedby="tip-italic" data-toggle-btn>
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 5h8M6 19h8M14 5l-4 14"/></svg>
</button>
<span role="tooltip" id="tip-italic" class="ap-tooltip">Italic <kbd>Ctrl I</kbd></span>
</span>
<span class="ap-tip">
<button type="button" class="ap-icon" aria-label="Link" aria-describedby="tip-link">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>
</button>
<span role="tooltip" id="tip-link" class="ap-tooltip">Add a link <kbd>Ctrl K</kbd></span>
</span>
<span class="ap-tip">
<button type="button" class="ap-icon" aria-label="Comment" aria-describedby="tip-comment">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.5 7.2L4 20l1-4.5A8 8 0 1 1 21 12z"/></svg>
</button>
<span role="tooltip" id="tip-comment" class="ap-tooltip">Leave a comment</span>
</span>
<span class="ap-pop">
<button type="button" class="btn btn-outline-secondary btn-sm rounded-pill ms-2 ap-trigger" popovertarget="insert-menu">
Insert
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="insert-menu" popover class="dropdown-menu ap-menu" aria-label="Insert">
<button type="button" class="dropdown-item">Image</button>
<button type="button" class="dropdown-item">Table</button>
<button type="button" class="dropdown-item">Code block</button>
<div class="dropdown-divider"></div>
<button type="button" class="dropdown-item">Page break</button>
</div>
</span>
<span class="flex-grow-1"></span>
<button type="button" class="btn btn-primary rounded-pill px-4">Share</button>
<span class="ap-pop ap-pop--end">
<button type="button" class="ap-avatar ap-trigger" popovertarget="account-menu" aria-label="Account menu">MO</button>
<div id="account-menu" popover class="dropdown-menu ap-menu" aria-label="Account">
<div class="px-3 py-2">
<div class="fw-bold">Maya Ortiz</div>
<div class="small wdw-muted">[email protected]</div>
</div>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Profile</a>
<a class="dropdown-item" href="#">Billing</a>
<a class="dropdown-item" href="#">Team settings</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#">Sign out</a>
</div>
</span>
</div>
<div class="p-4 p-md-5">
<span class="wdw-eyebrow">Draft</span>
<h1 class="wdw-heading h2 mt-1">Quarterly product update</h1>
<p class="wdw-muted mb-3">Hover or focus a toolbar button to see its tooltip, then open Insert or the avatar. Move this card near the edge of a small window and the menus flip to stay on screen.</p>
<div class="ap-lines" aria-hidden="true">
<span></span><span></span><span></span>
</div>
</div>
</div>
</div>
</section>
/* Web Designer Wall library style — shared base for every Bootstrap 5 snippet.
Indigo accent, Plus Jakarta Sans, soft cards, pill buttons. Works in light and dark. */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap");
:root,
[data-bs-theme="light"] {
--wdw-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-family: var(--wdw-font);
--bs-primary: #4f46e5;
--bs-primary-rgb: 79, 70, 229;
--bs-primary-text-emphasis: #3730a3;
--bs-primary-bg-subtle: #eef2ff;
--bs-primary-border-subtle: #c7d2fe;
--bs-link-color: #4f46e5;
--bs-link-color-rgb: 79, 70, 229;
--bs-link-hover-color: #4338ca;
--bs-link-hover-color-rgb: 67, 56, 202;
--bs-focus-ring-color: rgba(79, 70, 229, 0.25);
--bs-border-radius: 0.75rem;
--bs-border-radius-lg: 1rem;
--bs-border-radius-xl: 1.25rem;
--wdw-radius-card: 1.25rem;
--wdw-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 16px 40px -16px rgba(16, 24, 40, 0.22);
--wdw-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px -4px rgba(16, 24, 40, 0.12);
--wdw-stage: #f5f6fb;
}
[data-bs-theme="dark"] {
--bs-primary: #818cf8;
--bs-primary-rgb: 129, 140, 248;
--bs-primary-text-emphasis: #c7d2fe;
--bs-primary-bg-subtle: rgba(129, 140, 248, 0.14);
--bs-primary-border-subtle: rgba(129, 140, 248, 0.35);
--bs-link-color: #a5b4fc;
--bs-link-color-rgb: 165, 180, 252;
--bs-link-hover-color: #c7d2fe;
--bs-link-hover-color-rgb: 199, 210, 254;
--bs-focus-ring-color: rgba(129, 140, 248, 0.35);
--wdw-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 40px -16px rgba(0, 0, 0, 0.7);
--wdw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--wdw-stage: #0f1117;
}
body { font-family: var(--wdw-font); -webkit-font-smoothing: antialiased; }
/* Bootstrap compiles its blue into these components; point them at the accent. */
.btn-primary {
--bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: #4338ca; --bs-btn-hover-border-color: #4338ca;
--bs-btn-active-bg: #3730a3; --bs-btn-active-border-color: #3730a3;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-primary { --bs-btn-color: #0f1117; --bs-btn-hover-bg: #a5b4fc; --bs-btn-hover-border-color: #a5b4fc; --bs-btn-hover-color: #0f1117; --bs-btn-active-color: #0f1117; }
.btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
.btn { --bs-btn-font-weight: 600; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sm { min-height: 36px; }
.form-control, .form-select { min-height: 44px; border-radius: .75rem; }
.form-control:focus, .form-select:focus, .form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .6); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.nav-pills { --bs-nav-pills-link-active-bg: var(--bs-primary); }
.nav { --bs-nav-link-color: var(--bs-secondary-color); --bs-nav-link-hover-color: var(--bs-emphasis-color); }
.pagination { --bs-pagination-active-bg: var(--bs-primary); --bs-pagination-active-border-color: var(--bs-primary); --bs-pagination-color: var(--bs-body-color); --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--bs-primary); }
.list-group { --bs-list-group-active-bg: var(--bs-primary); --bs-list-group-active-border-color: var(--bs-primary); }
.dropdown-menu { --bs-dropdown-link-active-bg: var(--bs-primary); border-radius: .875rem; box-shadow: var(--wdw-shadow-sm); }
.accordion { --bs-accordion-active-bg: var(--bs-primary-bg-subtle); --bs-accordion-active-color: var(--bs-primary-text-emphasis); --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.badge { font-weight: 600; }
/* Library building blocks — use these instead of re-inventing them per snippet. */
.wdw-stage { min-height: 100vh; display: flex; align-items: center; background: var(--wdw-stage); padding-block: 3rem; }
.wdw-card { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: var(--wdw-radius-card); box-shadow: var(--wdw-shadow); }
.wdw-card-sm { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: 1rem; box-shadow: var(--wdw-shadow-sm); }
.wdw-aurora { position: relative; color: #fff; background: radial-gradient(60% 55% at 100% 0, #818cf8 0, transparent 70%), radial-gradient(65% 60% at 0 100%, #ec4899 0, transparent 65%), #1e1b4b; }
.wdw-eyebrow { display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--bs-primary); }
.wdw-heading { font-weight: 800; letter-spacing: -.02em; color: var(--bs-emphasis-color); }
.wdw-muted { color: var(--bs-secondary-color); }
.wdw-icon { width: 2.75rem; height: 2.75rem; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: .875rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
/* Component */
.ap-card { overflow: visible; }
.ap-bar { flex-wrap: wrap; }
/* Icon buttons ---------------------------------------------------------- */
.ap-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 44px; height: 44px; padding: 0;
border: 0; border-radius: .75rem;
background: transparent; color: var(--bs-body-color);
}
.ap-icon:hover { background: var(--bs-tertiary-bg); }
.ap-icon[aria-pressed="true"] { background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
.ap-icon:focus-visible, .ap-avatar:focus-visible {
outline: 3px solid var(--bs-primary); outline-offset: 2px;
}
.ap-avatar {
width: 44px; height: 44px; border: 0; border-radius: 50%;
background: var(--bs-primary); color: #fff; font-weight: 700; font-size: .875rem;
}
[data-bs-theme="dark"] .ap-avatar { color: #0f1117; }
/* Tooltip. Base layer: absolute, centred above the wrapper. -------------- */
.ap-tip { position: relative; display: inline-block; anchor-scope: --tip; }
.ap-tip > .ap-icon { anchor-name: --tip; }
.ap-tooltip {
position: absolute; inset: auto auto 100% 50%; translate: -50% -.5rem;
z-index: 5; width: max-content; max-width: 16rem;
padding: .375rem .625rem; border-radius: .5rem;
background: var(--bs-body-color); color: var(--bs-body-bg);
font-size: .8125rem; font-weight: 500; pointer-events: none;
opacity: 0; visibility: hidden;
transition: opacity .15s, visibility .15s;
}
.ap-tooltip kbd {
margin-left: .375rem; padding: .05rem .375rem; border-radius: .3rem;
background: rgb(127 127 127 / .35); color: inherit; font-size: .6875rem;
}
.ap-tip:is(:hover, :focus-within) .ap-tooltip { opacity: 1; visibility: visible; }
.ap-tip.is-dismissed .ap-tooltip { visibility: hidden; }
/* Menus. Base layer: the UA's centred popover on a dimmed page. ---------- */
.ap-menu.dropdown-menu {
--bs-dropdown-min-width: 13rem;
--bs-dropdown-padding-x: .375rem; --bs-dropdown-padding-y: .375rem;
--bs-dropdown-item-padding-y: .5rem; --bs-dropdown-item-padding-x: .75rem;
--bs-dropdown-item-border-radius: .5rem;
--bs-dropdown-link-hover-bg: var(--bs-primary-bg-subtle);
--bs-dropdown-link-hover-color: var(--bs-primary-text-emphasis);
position: fixed; inset: 0; margin: auto; height: fit-content; width: fit-content;
display: none; box-shadow: var(--wdw-shadow);
}
.ap-menu.dropdown-menu:popover-open { display: block; }
.ap-menu .dropdown-item { min-height: 40px; display: flex; align-items: center; }
.ap-menu::backdrop { background: rgb(15 17 23 / .35); }
.ap-pop { position: relative; display: inline-block; anchor-scope: --pop; }
.ap-trigger { anchor-name: --pop; }
/* Upgrade where anchor positioning exists ------------------------------- */
@supports (anchor-name: --a) {
.ap-tip { position: static; }
.ap-tooltip {
position-anchor: --tip; inset: auto; translate: none;
position-area: block-start; margin: .5rem;
position-try-fallbacks: flip-block, flip-inline;
}
.ap-pop { position: static; }
.ap-menu.dropdown-menu {
position-anchor: --pop; inset: auto; margin: .375rem 0;
height: auto; width: auto;
position-area: block-end span-inline-end;
min-inline-size: max(13rem, anchor-size(inline));
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
.ap-pop--end .ap-menu.dropdown-menu { position-area: block-end span-inline-start; }
.ap-menu::backdrop { background: none; }
}
.ap-lines { display: grid; gap: .625rem; }
.ap-lines span { height: 10px; border-radius: 6px; background: var(--bs-secondary-bg); }
.ap-lines span:nth-child(1) { width: 92%; }
.ap-lines span:nth-child(2) { width: 78%; }
.ap-lines span:nth-child(3) { width: 55%; }
@media (prefers-reduced-motion: reduce) { .ap-tooltip { transition: none; } }
@media (max-width: 575.98px) { .ap-bar { gap: .25rem !important; } .ap-bar .ms-2 { margin-left: 0 !important; } }
function initToggles() {
document.querySelectorAll('[data-toggle-btn]').forEach(function (btn) {
btn.addEventListener('click', function () {
btn.setAttribute('aria-pressed', btn.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
});
});
}
// WCAG 1.4.13: Escape dismisses a tooltip without moving focus or pointer.
function initTooltipDismiss() {
var tips = document.querySelectorAll('.ap-tip');
if (!tips.length) return;
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
document.querySelectorAll('.ap-tip:hover, .ap-tip:focus-within').forEach(function (el) {
el.classList.add('is-dismissed');
});
});
tips.forEach(function (el) {
var reset = function () { el.classList.remove('is-dismissed'); };
el.addEventListener('pointerleave', reset);
el.addEventListener('focusout', reset);
});
}
function initMenus() {
document.querySelectorAll('.ap-menu a[href="#"]').forEach(function (a) {
a.addEventListener('click', function (e) { e.preventDefault(); a.closest('[popover]').hidePopover(); });
});
document.querySelectorAll('.ap-menu button').forEach(function (b) {
b.addEventListener('click', function () { b.closest('[popover]').hidePopover(); });
});
}
initToggles();
initTooltipDismiss();
initMenus();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.