Component
Selection Toolbar
A floating toolbar that appears above any selected text inside its article scope, offering Highlight (real <mark> wrap), Copy, and Share actions. Positions itself over the selection with viewport clamping and re-tracks on scroll and resize.
- Component
- Tailwind
- HTML
- MIT
What it does
Capabilities
- Floating toolbar above selected text
- Highlight wraps the range in a real <mark>
- Copy and Share with clipboard fallback
- Screen-reader status via aria-live region
Preview
Live component
9:41 100%
devsnips.site/library/tailwind/components/tooltips/selection-toolbar/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add Tailwind/Components/Tooltips/selection-toolbar Tailwind/Components/Tooltips/selection-toolbar <!--
Snippet Name: Selection Toolbar
Description: A floating neo-brutalist toolbar that appears over selected text offering highlight, copy, and share actions.
Author: DevSnips Contributors
Usage Example: Wrap any article with the data-selection-toolbar scope; selecting prose opens the toolbar. Highlight wraps the range in a <mark>.
-->
<style>
[data-selection-toolbar="selection-toolbar"] ::selection { background: #FFE600; color: #0a0a0a; }
</style>
<div data-selection-toolbar="selection-toolbar" class="relative">
<article class="border-2 border-black bg-white">
<header class="border-b-2 border-black p-6 pb-5 sm:p-8 sm:pb-6">
<span class="inline-block rounded-none bg-[#FFE600] px-2 py-1 text-xs font-bold uppercase tracking-widest text-black">Design Systems Weekly · Essay №42</span>
<h2 class="mt-4 text-3xl font-black tracking-tight text-black sm:text-4xl">Polish is a product feature</h2>
<div class="mt-4 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-neutral-600">
<span class="flex items-center gap-2">
<span aria-hidden="true" class="flex h-6 w-6 items-center justify-center border-2 border-black bg-[#FFE600] text-[11px] font-black text-black">MO</span>
<span class="font-semibold text-black">Marina Okafor</span>
</span>
<span aria-hidden="true" class="h-4 w-px bg-neutral-300"></span>
<span>Aug 25, 2026</span>
<span aria-hidden="true" class="h-4 w-px bg-neutral-300"></span>
<span>5 min read</span>
<span aria-hidden="true" class="h-4 w-px bg-neutral-300"></span>
<span data-highlights-count class="border-b-2 border-[#FFE600] font-medium text-black">1 highlight</span>
</div>
</header>
<div class="px-6 py-6 sm:px-8 sm:py-8 text-[15px] sm:text-base leading-relaxed text-neutral-800">
<p>
Every product team has a version of the same meeting. The feature works, the tests pass,
and someone on the call says the interface still <em>feels off</em> in a way no ticket can
quite capture. The room treats that feeling as a luxury, and the polish it asks for goes
to the bottom of the backlog. That is a mistake — and it compounds.
</p>
<p class="mt-4">
Rough edges accumulate the way interest does. A focus ring that never shows, a tooltip that
flickers, a button that does not visibly respond to press — none of these block a task on
their own, yet together they teach people to trust the product a little less with every
interaction. <mark class="bg-[#FFE600] px-0.5 font-medium text-black">Rough edges compound.</mark>
Users may never quote the defect, but they will feel the average of every one of them.
</p>
<p class="mt-4">
The fix is not heroics on a launch week. It is a standing budget for the last 1%: motion
tuned to 150ms ease-out, selection states that announce themselves, empty states written
like a colleague instead of a linter. Pick the details most worth sweating and make them
part of done.
</p>
<blockquote class="mt-6 border-l-4 border-black bg-neutral-50 px-4 py-3 font-medium text-black">
Users never separate polish from quality. To them, one is simply evidence of the other.
</blockquote>
<p class="mt-4">
Polish is not the coat of paint applied after engineering. It is the difference between a
tool that is merely correct and one that is a pleasure to operate — and it is why the
interface, not the feature list, is where brand gets built. Sweat it deliberately, or
watch it leak.
</p>
</div>
</article>
<div
data-st-toolbar
role="toolbar"
aria-label="Text formatting"
class="fixed z-50 hidden items-stretch gap-0 border-2 border-black bg-black shadow-[6px_6px_0_0_rgba(0,0,0,0.9)]"
>
<button
type="button"
data-st-highlight
class="flex h-11 items-center gap-1.5 bg-[#FFE600] px-3 text-sm font-bold text-black transition-colors duration-150 hover:bg-[#FF4FA3] focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white motion-reduce:transition-none"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.4" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.2 4.3 19.7 8.8m-8.9-4.8 8.9 8.9-4.2 4.2-8.9-8.9 4.2-4.2ZM6.1 12.5l1.4 5.8 5.3-1.4" />
</svg>
Highlight
</button>
<button
type="button"
data-st-copy
class="flex h-11 items-center gap-1.5 px-3 text-sm font-semibold text-white transition-colors duration-150 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#FFE600] motion-reduce:transition-none"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 8V5.6A1.6 1.6 0 0 1 9.6 4h8.8A1.6 1.6 0 0 1 20 5.6v8.8a1.6 1.6 0 0 1-1.6 1.6H16M4 9.6A1.6 1.6 0 0 1 5.6 8h8.8A1.6 1.6 0 0 1 16 9.6v8.8a1.6 1.6 0 0 1-1.6 1.6H5.6A1.6 1.6 0 0 1 4 18.4V9.6Z" />
</svg>
<span data-st-copy-label>Copy</span>
</button>
<button
type="button"
data-st-share
class="flex h-11 items-center gap-1.5 px-3 text-sm font-semibold text-white transition-colors duration-150 hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#FFE600] motion-reduce:transition-none"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0-12L7.5 8.5M12 4l4.5 4.5M5 13v5.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V13" />
</svg>
<span data-st-share-label>Share</span>
</button>
<span aria-hidden="true" class="w-px bg-white/20"></span>
<span data-st-count aria-hidden="true" class="flex items-center px-3 text-xs font-medium tabular-nums text-white/50"></span>
</div>
<span data-st-live class="sr-only" role="status" aria-live="polite"></span>
<script>
(function () {
var root = document.currentScript.closest('[data-selection-toolbar="selection-toolbar"]');
var article = root.querySelector('article');
var toolbar = root.querySelector('[data-st-toolbar]');
var counter = root.querySelector('[data-highlights-count]');
var countLabel = toolbar.querySelector('[data-st-count]');
var live = root.querySelector('[data-st-live]');
var debounce = null;
var visible = false;
var currentText = '';
function announce(message) {
live.textContent = message;
}
function updateCounter() {
var marks = article.querySelectorAll('mark').length;
var label = marks + (marks === 1 ? ' highlight' : ' highlights');
counter.textContent = label;
counter.setAttribute('aria-label', label);
}
function selectionInArticle() {
var sel = window.getSelection();
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
var range = sel.getRangeAt(0);
var node = range.commonAncestorContainer;
if (node !== article && !article.contains(node)) return null;
var text = sel.toString().trim();
if (!text) return null;
return { range: range, text: text };
}
function showToolbar() {
var snap = selectionInArticle();
if (!snap) { hideToolbar(); return; }
var rect = snap.range.getBoundingClientRect();
if (rect.width === 0) { hideToolbar(); return; }
toolbar.classList.remove('hidden');
toolbar.classList.add('flex');
visible = true;
currentText = snap.text;
countLabel.textContent = snap.text.length + (snap.text.length === 1 ? ' char' : ' chars');
var w = toolbar.offsetWidth;
var h = toolbar.offsetHeight;
var left = rect.left + rect.width / 2 - w / 2;
left = Math.min(Math.max(left, 8), window.innerWidth - w - 8);
var top = rect.top - h - 12;
if (top < 8) top = rect.bottom + 12;
toolbar.style.left = left + 'px';
toolbar.style.top = top + 'px';
}
function hideToolbar() {
toolbar.classList.add('hidden');
toolbar.classList.remove('flex');
visible = false;
}
function refresh() {
window.clearTimeout(debounce);
debounce = window.setTimeout(showToolbar, 120);
}
document.addEventListener('selectionchange', refresh);
window.addEventListener('scroll', function () {
if (visible) { refresh(); }
}, { passive: true });
window.addEventListener('resize', function () {
if (visible) { showToolbar(); }
});
document.addEventListener('pointerdown', function (event) {
if (!visible || root.contains(event.target)) return;
hideToolbar();
});
document.addEventListener('keydown', function (event) {
if (!visible || event.key !== 'Escape') return;
hideToolbar();
event.preventDefault();
event.stopPropagation();
}, true);
// Keep the selection alive across toolbar interaction.
toolbar.addEventListener('mousedown', function (event) {
event.preventDefault();
});
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
return navigator.clipboard.writeText(text).then(function () { return true; });
} catch (e) {
// Denied clipboard permissions fall through to execCommand below.
}
}
var helper = document.createElement('textarea');
helper.value = text;
helper.setAttribute('readonly', '');
helper.style.position = 'absolute';
helper.style.left = '-9999px';
document.body.appendChild(helper);
helper.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
document.body.removeChild(helper);
return Promise.resolve(ok);
}
function feedback(span, doneLabel) {
var original = span.textContent;
span.textContent = doneLabel;
window.setTimeout(function () { span.textContent = original; }, 1400);
}
toolbar.querySelector('[data-st-copy]').addEventListener('click', function () {
var span = toolbar.querySelector('[data-st-copy-label]');
copyText(currentText).then(function (ok) {
if (ok) {
feedback(span, 'Copied');
announce('Copied to clipboard');
} else {
announce('Copy failed');
}
});
});
toolbar.querySelector('[data-st-share]').addEventListener('click', function () {
var span = toolbar.querySelector('[data-st-share-label]');
var title = root.querySelector('h2') ? root.querySelector('h2').textContent.trim() : 'this article';
var payload = '“' + currentText + '” — ' + title;
copyText(payload).then(function (ok) {
if (ok) {
feedback(span, 'Copied');
announce('Share quote copied');
} else {
announce('Share failed');
}
});
});
toolbar.querySelector('[data-st-highlight]').addEventListener('click', function () {
var snap = selectionInArticle();
if (!snap) return;
var mark = document.createElement('mark');
mark.className = 'bg-[#FFE600] px-0.5 font-medium text-black';
try {
snap.range.surroundContents(mark);
} catch (e) {
mark.appendChild(snap.range.extractContents());
snap.range.insertNode(mark);
}
window.getSelection().removeAllRanges();
updateCounter();
announce('Highlighted passage');
hideToolbar();
});
updateCounter();
})();
</script>
</div> # Selection Toolbar
Selection toolbar with tooltip-like presentation.
## Usage
```bash
npx devsnips add Tailwind/Components/Tooltips/selection-toolbar
```
Copy from `code.html`.
## File Structure
`code.html` · `preview.html` · `metadata.json` · `README.md` 268 lines UTF-8 · LF · Spaces: 2
DevSnips AI ready Claude · Cursor · Windsurf
Agent skill & Model Context Protocol
Pass this component straight into your AI workflow. DevSnips exposes each resource as a structured, addressable registry entry, so your agent installs the real files instead of approximating them.
devsnips://components/tooltips/selection-toolbar I want to use this DevSnips resource in production: Page: https://devsnips.site/library/tailwind/components/tooltips/selection-toolbar/ Install: npx devsnips add Tailwind/Components/Tooltips/selection-toolbar This is the Selection Toolbar — A floating toolbar that appears above any selected text inside its article scope, offering Highlight (real <mark> wrap), Copy, and Share actions. Positions itself over the selection with viewport clamping and re-tracks on scroll and resize. Use this DevSnips resource as the starting point for my project. Adapt it to my project's stack and conventions, fix any issues, and make it production-ready while preserving its intended design and behavior.
Paste into ChatGPT, Claude or your agent — it already carries the page URL and the exact install path.
Resource URI
devsnips://components/tooltips/selection-toolbar