34 CSS Floating Buttons33 / 34
Quick Emoji Reply Reaction Floating Button CSS
The reaction bar: six quick replies that pop out of a floating button, magnify under the pointer like a desktop dock, and burst into particles when chosen. Every reaction is a labelled toggle with aria-pressed, the counts update live, and the whole strip is keyboard-navigable with arrow keys — because reactions are content, not decoration.
Published Updated
The code
<section class="fb-33" aria-label="Quick emoji reaction floating button demo">
<div class="fb-33__stage">
<p class="fb-33__eyebrow">:has() neighbour magnify · trig burst</p>
<h2 class="fb-33__title">React without leaving the thread</h2>
<p class="fb-33__sub">Open the strip, then sweep across it. The hovered reaction grows most and its neighbours grow less — that ramp is what makes it feel like a dock.</p>
<article class="fb-33__msg">
<span class="fb-33__av" aria-hidden="true"></span>
<div class="fb-33__bub">
<b>Marcus Tan</b>
<p>Shipped the FAB set — 34 patterns, all keyboard-tested. Docs are in the same file.</p>
<ul class="fb-33__tally" id="fb-33-tally"></ul>
</div>
</article>
<div class="fb-33__dock" id="fb-33-dock" data-open="false">
<ul class="fb-33__strip" id="fb-33-strip">
<li style="--fb-33-i:0"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="👍" data-fb-n="Thumbs up"><span aria-hidden="true">👍</span></button></li>
<li style="--fb-33-i:1"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🎉" data-fb-n="Celebrate"><span aria-hidden="true">🎉</span></button></li>
<li style="--fb-33-i:2"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🔥" data-fb-n="Fire"><span aria-hidden="true">🔥</span></button></li>
<li style="--fb-33-i:3"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="❤️" data-fb-n="Love"><span aria-hidden="true">❤️</span></button></li>
<li style="--fb-33-i:4"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="😮" data-fb-n="Wow"><span aria-hidden="true">😮</span></button></li>
<li style="--fb-33-i:5"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🙌" data-fb-n="Applause"><span aria-hidden="true">🙌</span></button></li>
</ul>
<button class="fb-33__fab" id="fb-33-fab" type="button" aria-expanded="false" aria-controls="fb-33-strip" aria-label="Add a reaction">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.4" fill="none" stroke="currentColor" stroke-width="1.9"/><circle cx="9.3" cy="10.2" r="1.2" fill="currentColor"/><circle cx="14.7" cy="10.2" r="1.2" fill="currentColor"/><path d="M8.6 14.2c1.9 1.9 4.9 1.9 6.8 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
</button>
</div>
<p class="fb-33__live" id="fb-33-live" role="status" aria-live="polite"></p>
<p class="fb-33__chip">arrow keys wrap · 8 particles · aria-pressed toggles</p>
</div>
</section><section class="fb-33" aria-label="Quick emoji reaction floating button demo">
<div class="fb-33__stage">
<p class="fb-33__eyebrow">:has() neighbour magnify · trig burst</p>
<h2 class="fb-33__title">React without leaving the thread</h2>
<p class="fb-33__sub">Open the strip, then sweep across it. The hovered reaction grows most and its neighbours grow less — that ramp is what makes it feel like a dock.</p>
<article class="fb-33__msg">
<span class="fb-33__av" aria-hidden="true"></span>
<div class="fb-33__bub">
<b>Marcus Tan</b>
<p>Shipped the FAB set — 34 patterns, all keyboard-tested. Docs are in the same file.</p>
<ul class="fb-33__tally" id="fb-33-tally"></ul>
</div>
</article>
<div class="fb-33__dock" id="fb-33-dock" data-open="false">
<ul class="fb-33__strip" id="fb-33-strip">
<li style="--fb-33-i:0"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="👍" data-fb-n="Thumbs up"><span aria-hidden="true">👍</span></button></li>
<li style="--fb-33-i:1"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🎉" data-fb-n="Celebrate"><span aria-hidden="true">🎉</span></button></li>
<li style="--fb-33-i:2"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🔥" data-fb-n="Fire"><span aria-hidden="true">🔥</span></button></li>
<li style="--fb-33-i:3"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="❤️" data-fb-n="Love"><span aria-hidden="true">❤️</span></button></li>
<li style="--fb-33-i:4"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="😮" data-fb-n="Wow"><span aria-hidden="true">😮</span></button></li>
<li style="--fb-33-i:5"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🙌" data-fb-n="Applause"><span aria-hidden="true">🙌</span></button></li>
</ul>
<button class="fb-33__fab" id="fb-33-fab" type="button" aria-expanded="false" aria-controls="fb-33-strip" aria-label="Add a reaction">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.4" fill="none" stroke="currentColor" stroke-width="1.9"/><circle cx="9.3" cy="10.2" r="1.2" fill="currentColor"/><circle cx="14.7" cy="10.2" r="1.2" fill="currentColor"/><path d="M8.6 14.2c1.9 1.9 4.9 1.9 6.8 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
</button>
</div>
<p class="fb-33__live" id="fb-33-live" role="status" aria-live="polite"></p>
<p class="fb-33__chip">arrow keys wrap · 8 particles · aria-pressed toggles</p>
</div>
</section>.fb-33 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-33-bg);
--fb-33-bg: linear-gradient(180deg,oklch(0.97 0.01 250),oklch(0.93 0.02 262));
--fb-33-brand: oklch(0.5 0.16 272);
--fb-33-ink: oklch(0.22 0.025 265);
--fb-33-mut: oklch(0.53 0.015 265);
--fb-33-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-33-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-33 *,
.fb-33 *::before,
.fb-33 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-33__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-33__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--fb-33-brand);
}
.fb-33__title {
font-size: clamp(25px,4.4vw,42px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-33__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-33-mut);
text-wrap: pretty;
}
.fb-33__msg {
display: flex;
gap: 12px;
align-items: flex-start;
width: min(100%,440px);
text-align: left;
padding: 14px;
border-radius: 20px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 18px 44px -30px oklch(0.22 0.025 265/.9);
}
.fb-33__av {
flex: none;
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(140deg,oklch(0.78 0.12 250),oklch(0.55 0.16 300)),url("https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=160&auto=format&fit=crop") center/cover;
}
.fb-33__bub {
display: grid;
gap: 5px;
min-width: 0;
}
.fb-33__bub b {
font-size: 13.5px;
font-weight: 650;
}
.fb-33__bub p {
font-size: 13.5px;
line-height: 1.55;
color: oklch(0.35 0.02 265);
text-wrap: pretty;
}
.fb-33__tally {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.fb-33__tally li {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 620;
padding: 4px 9px;
border-radius: 99px;
background: oklch(0.5 0.16 272/.1);
border: 1px solid oklch(0.5 0.16 272/.28);
color: var(--fb-33-brand);
font-variant-numeric: tabular-nums;
animation: fb-33-pop .3s cubic-bezier(.2,1.6,.4,1);
}
@keyframes fb-33-pop {
from {
scale: .5;
opacity: 0;
}
}
.fb-33__dock {
position: relative;
display: grid;
justify-items: center;
gap: 14px;
padding-top: 10px;
}
.fb-33__strip {
list-style: none;
display: flex;
align-items: flex-end;
gap: 6px;
padding: 7px 10px;
border-radius: 99px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 16px 40px -22px oklch(0.22 0.025 265/.9);
opacity: 0;
translate: 0 10px;
pointer-events: none;
transition: opacity .24s ease,translate .3s cubic-bezier(.2,1.1,.3,1);
}
.fb-33__dock[data-open="true"] .fb-33__strip {
opacity: 1;
translate: 0 0;
pointer-events: auto;
}
.fb-33__react {
position: relative;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 24px;
line-height: 1;
scale: 0;
translate: 0 12px;
transform-origin: 50% 100%;
transition: scale .34s cubic-bezier(.2,1.6,.4,1),translate .3s cubic-bezier(.2,1.4,.4,1),background .18s ease;
transition-delay: calc(var(--fb-33-i) * 42ms);
}
.fb-33__dock[data-open="true"] .fb-33__react {
scale: 1;
translate: 0 0;
}
.fb-33__react span {
display: block;
filter: drop-shadow(0 2px 4px oklch(0.22 0.025 265/.2));
}
.fb-33__dock[data-open="true"] .fb-33__react:hover {
scale: 1.5;
translate: 0 -10px;
transition-delay: 0s;
}
.fb-33__dock[data-open="true"] li:hover + li .fb-33__react,
.fb-33__dock[data-open="true"] li:has(+ li:hover) .fb-33__react {
scale: 1.24;
translate: 0 -5px;
transition-delay: 0s;
}
.fb-33__react:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 2px;
scale: 1.35;
}
.fb-33__react[aria-pressed="true"] {
background: oklch(0.5 0.16 272/.14);
}
.fb-33__burst {
position: absolute;
inset: 0;
pointer-events: none;
}
.fb-33__burst i {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fb-33-brand);
animation: fb-33-fly .62s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes fb-33-fly {
to {
translate: calc(cos(var(--fb-33-a)) * 42px) calc(sin(var(--fb-33-a)) * -42px);
opacity: 0;
scale: .3;
}
}
.fb-33__fab {
display: grid;
place-items: center;
inline-size: 58px;
block-size: 58px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.99 0.01 272);
background: linear-gradient(150deg,oklch(0.6 0.17 276),var(--fb-33-brand));
box-shadow: 0 12px 30px -12px oklch(0.5 0.16 272/.85),0 2px 6px oklch(0.3 0.07 272/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),rotate .3s ease;
}
.fb-33__fab svg {
width: 26px;
height: 26px;
}
.fb-33__fab:hover {
transform: translateY(-2px) scale(1.04);
}
.fb-33__fab:active {
transform: scale(.94);
}
.fb-33__fab:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 4px;
}
.fb-33__dock[data-open="true"] .fb-33__fab {
rotate: 22deg;
}
.fb-33__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--fb-33-brand);
min-height: 18px;
}
.fb-33__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-33-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-33 * {
transition-duration: .01ms !important;
animation: none !important;
}
}.fb-33 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-33-bg);
--fb-33-bg: linear-gradient(180deg,oklch(0.97 0.01 250),oklch(0.93 0.02 262));
--fb-33-brand: oklch(0.5 0.16 272);
--fb-33-ink: oklch(0.22 0.025 265);
--fb-33-mut: oklch(0.53 0.015 265);
--fb-33-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-33-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-33 *,
.fb-33 *::before,
.fb-33 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-33__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-33__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--fb-33-brand);
}
.fb-33__title {
font-size: clamp(25px,4.4vw,42px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-33__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-33-mut);
text-wrap: pretty;
}
.fb-33__msg {
display: flex;
gap: 12px;
align-items: flex-start;
width: min(100%,440px);
text-align: left;
padding: 14px;
border-radius: 20px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 18px 44px -30px oklch(0.22 0.025 265/.9);
}
.fb-33__av {
flex: none;
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(140deg,oklch(0.78 0.12 250),oklch(0.55 0.16 300)),url("https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=160&auto=format&fit=crop") center/cover;
}
.fb-33__bub {
display: grid;
gap: 5px;
min-width: 0;
}
.fb-33__bub b {
font-size: 13.5px;
font-weight: 650;
}
.fb-33__bub p {
font-size: 13.5px;
line-height: 1.55;
color: oklch(0.35 0.02 265);
text-wrap: pretty;
}
.fb-33__tally {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.fb-33__tally li {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 620;
padding: 4px 9px;
border-radius: 99px;
background: oklch(0.5 0.16 272/.1);
border: 1px solid oklch(0.5 0.16 272/.28);
color: var(--fb-33-brand);
font-variant-numeric: tabular-nums;
animation: fb-33-pop .3s cubic-bezier(.2,1.6,.4,1);
}
@keyframes fb-33-pop {
from {
scale: .5;
opacity: 0;
}
}
.fb-33__dock {
position: relative;
display: grid;
justify-items: center;
gap: 14px;
padding-top: 10px;
}
.fb-33__strip {
list-style: none;
display: flex;
align-items: flex-end;
gap: 6px;
padding: 7px 10px;
border-radius: 99px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 16px 40px -22px oklch(0.22 0.025 265/.9);
opacity: 0;
translate: 0 10px;
pointer-events: none;
transition: opacity .24s ease,translate .3s cubic-bezier(.2,1.1,.3,1);
}
.fb-33__dock[data-open="true"] .fb-33__strip {
opacity: 1;
translate: 0 0;
pointer-events: auto;
}
.fb-33__react {
position: relative;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 24px;
line-height: 1;
scale: 0;
translate: 0 12px;
transform-origin: 50% 100%;
transition: scale .34s cubic-bezier(.2,1.6,.4,1),translate .3s cubic-bezier(.2,1.4,.4,1),background .18s ease;
transition-delay: calc(var(--fb-33-i) * 42ms);
}
.fb-33__dock[data-open="true"] .fb-33__react {
scale: 1;
translate: 0 0;
}
.fb-33__react span {
display: block;
filter: drop-shadow(0 2px 4px oklch(0.22 0.025 265/.2));
}
.fb-33__dock[data-open="true"] .fb-33__react:hover {
scale: 1.5;
translate: 0 -10px;
transition-delay: 0s;
}
.fb-33__dock[data-open="true"] li:hover + li .fb-33__react,
.fb-33__dock[data-open="true"] li:has(+ li:hover) .fb-33__react {
scale: 1.24;
translate: 0 -5px;
transition-delay: 0s;
}
.fb-33__react:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 2px;
scale: 1.35;
}
.fb-33__react[aria-pressed="true"] {
background: oklch(0.5 0.16 272/.14);
}
.fb-33__burst {
position: absolute;
inset: 0;
pointer-events: none;
}
.fb-33__burst i {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fb-33-brand);
animation: fb-33-fly .62s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes fb-33-fly {
to {
translate: calc(cos(var(--fb-33-a)) * 42px) calc(sin(var(--fb-33-a)) * -42px);
opacity: 0;
scale: .3;
}
}
.fb-33__fab {
display: grid;
place-items: center;
inline-size: 58px;
block-size: 58px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.99 0.01 272);
background: linear-gradient(150deg,oklch(0.6 0.17 276),var(--fb-33-brand));
box-shadow: 0 12px 30px -12px oklch(0.5 0.16 272/.85),0 2px 6px oklch(0.3 0.07 272/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),rotate .3s ease;
}
.fb-33__fab svg {
width: 26px;
height: 26px;
}
.fb-33__fab:hover {
transform: translateY(-2px) scale(1.04);
}
.fb-33__fab:active {
transform: scale(.94);
}
.fb-33__fab:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 4px;
}
.fb-33__dock[data-open="true"] .fb-33__fab {
rotate: 22deg;
}
.fb-33__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--fb-33-brand);
min-height: 18px;
}
.fb-33__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-33-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-33 * {
transition-duration: .01ms !important;
animation: none !important;
}
}(() => {
const dock = document.getElementById('fb-33-dock');
const fab = document.getElementById('fb-33-fab');
const tally = document.getElementById('fb-33-tally');
const live = document.getElementById('fb-33-live');
if (!dock || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const items = [...dock.querySelectorAll('.fb-33__react')];
const counts = new Map();
const set = (open) => {
dock.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
items.forEach((i) => { i.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => items[0].focus(), 90);
};
const render = () => {
tally.innerHTML = '';
counts.forEach((n, e) => {
if (!n) return;
const li = document.createElement('li');
li.textContent = e + ' ' + n;
tally.append(li);
});
};
const burst = (btn) => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const wrap = document.createElement('span');
wrap.className = 'fb-33__burst';
for (let i = 0; i < 8; i += 1) {
const p = document.createElement('i');
p.style.setProperty('--fb-33-a', (i * 45) + 'deg');
wrap.append(p);
}
btn.append(wrap);
setTimeout(() => wrap.remove(), 700);
};
items.forEach((btn, idx) => {
btn.setAttribute('aria-label', btn.dataset.fbN + ', 0 reactions');
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
const e = btn.dataset.fbE;
const n = (counts.get(e) || 0) + (on ? -1 : 1);
counts.set(e, Math.max(0, n));
btn.setAttribute('aria-pressed', String(!on));
btn.setAttribute('aria-label', btn.dataset.fbN + ', ' + counts.get(e) + ' reactions');
if (!on) burst(btn);
render();
live.textContent = (on ? 'Removed ' : 'Added ') + btn.dataset.fbN;
});
btn.addEventListener('keydown', (ev) => {
if (ev.key === 'ArrowRight') { ev.preventDefault(); items[(idx + 1) % items.length].focus(); }
if (ev.key === 'ArrowLeft') { ev.preventDefault(); items[(idx - 1 + items.length) % items.length].focus(); }
if (ev.key === 'Escape') { set(false); fab.focus(); }
});
});
fab.addEventListener('click', () => set(dock.dataset.open !== 'true'));
})();(() => {
const dock = document.getElementById('fb-33-dock');
const fab = document.getElementById('fb-33-fab');
const tally = document.getElementById('fb-33-tally');
const live = document.getElementById('fb-33-live');
if (!dock || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const items = [...dock.querySelectorAll('.fb-33__react')];
const counts = new Map();
const set = (open) => {
dock.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
items.forEach((i) => { i.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => items[0].focus(), 90);
};
const render = () => {
tally.innerHTML = '';
counts.forEach((n, e) => {
if (!n) return;
const li = document.createElement('li');
li.textContent = e + ' ' + n;
tally.append(li);
});
};
const burst = (btn) => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const wrap = document.createElement('span');
wrap.className = 'fb-33__burst';
for (let i = 0; i < 8; i += 1) {
const p = document.createElement('i');
p.style.setProperty('--fb-33-a', (i * 45) + 'deg');
wrap.append(p);
}
btn.append(wrap);
setTimeout(() => wrap.remove(), 700);
};
items.forEach((btn, idx) => {
btn.setAttribute('aria-label', btn.dataset.fbN + ', 0 reactions');
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
const e = btn.dataset.fbE;
const n = (counts.get(e) || 0) + (on ? -1 : 1);
counts.set(e, Math.max(0, n));
btn.setAttribute('aria-pressed', String(!on));
btn.setAttribute('aria-label', btn.dataset.fbN + ', ' + counts.get(e) + ' reactions');
if (!on) burst(btn);
render();
live.textContent = (on ? 'Removed ' : 'Added ') + btn.dataset.fbN;
});
btn.addEventListener('keydown', (ev) => {
if (ev.key === 'ArrowRight') { ev.preventDefault(); items[(idx + 1) % items.length].focus(); }
if (ev.key === 'ArrowLeft') { ev.preventDefault(); items[(idx - 1 + items.length) % items.length].focus(); }
if (ev.key === 'Escape') { set(false); fab.focus(); }
});
});
fab.addEventListener('click', () => set(dock.dataset.open !== 'true'));
})();Here's a working CSS Floating Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Quick Emoji Reply Reaction Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/fb15/
The reaction bar: six quick replies that pop out of a floating button, magnify under the pointer like a desktop dock, and burst into particles when chosen. Every reaction is a labelled toggle with aria-pressed, the counts update live, and the whole strip is keyboard-navigable with arrow keys — because reactions are content, not decoration.
## HTML
```html
<section class="fb-33" aria-label="Quick emoji reaction floating button demo">
<div class="fb-33__stage">
<p class="fb-33__eyebrow">:has() neighbour magnify · trig burst</p>
<h2 class="fb-33__title">React without leaving the thread</h2>
<p class="fb-33__sub">Open the strip, then sweep across it. The hovered reaction grows most and its neighbours grow less — that ramp is what makes it feel like a dock.</p>
<article class="fb-33__msg">
<span class="fb-33__av" aria-hidden="true"></span>
<div class="fb-33__bub">
<b>Marcus Tan</b>
<p>Shipped the FAB set — 34 patterns, all keyboard-tested. Docs are in the same file.</p>
<ul class="fb-33__tally" id="fb-33-tally"></ul>
</div>
</article>
<div class="fb-33__dock" id="fb-33-dock" data-open="false">
<ul class="fb-33__strip" id="fb-33-strip">
<li style="--fb-33-i:0"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="👍" data-fb-n="Thumbs up"><span aria-hidden="true">👍</span></button></li>
<li style="--fb-33-i:1"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🎉" data-fb-n="Celebrate"><span aria-hidden="true">🎉</span></button></li>
<li style="--fb-33-i:2"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🔥" data-fb-n="Fire"><span aria-hidden="true">🔥</span></button></li>
<li style="--fb-33-i:3"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="❤️" data-fb-n="Love"><span aria-hidden="true">❤️</span></button></li>
<li style="--fb-33-i:4"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="😮" data-fb-n="Wow"><span aria-hidden="true">😮</span></button></li>
<li style="--fb-33-i:5"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🙌" data-fb-n="Applause"><span aria-hidden="true">🙌</span></button></li>
</ul>
<button class="fb-33__fab" id="fb-33-fab" type="button" aria-expanded="false" aria-controls="fb-33-strip" aria-label="Add a reaction">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.4" fill="none" stroke="currentColor" stroke-width="1.9"/><circle cx="9.3" cy="10.2" r="1.2" fill="currentColor"/><circle cx="14.7" cy="10.2" r="1.2" fill="currentColor"/><path d="M8.6 14.2c1.9 1.9 4.9 1.9 6.8 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
</button>
</div>
<p class="fb-33__live" id="fb-33-live" role="status" aria-live="polite"></p>
<p class="fb-33__chip">arrow keys wrap · 8 particles · aria-pressed toggles</p>
</div>
</section>
```
## CSS
```css
.fb-33 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-33-bg);
--fb-33-bg: linear-gradient(180deg,oklch(0.97 0.01 250),oklch(0.93 0.02 262));
--fb-33-brand: oklch(0.5 0.16 272);
--fb-33-ink: oklch(0.22 0.025 265);
--fb-33-mut: oklch(0.53 0.015 265);
--fb-33-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-33-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-33 *,
.fb-33 *::before,
.fb-33 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-33__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-33__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--fb-33-brand);
}
.fb-33__title {
font-size: clamp(25px,4.4vw,42px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-33__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-33-mut);
text-wrap: pretty;
}
.fb-33__msg {
display: flex;
gap: 12px;
align-items: flex-start;
width: min(100%,440px);
text-align: left;
padding: 14px;
border-radius: 20px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 18px 44px -30px oklch(0.22 0.025 265/.9);
}
.fb-33__av {
flex: none;
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(140deg,oklch(0.78 0.12 250),oklch(0.55 0.16 300)),url("https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=160&auto=format&fit=crop") center/cover;
}
.fb-33__bub {
display: grid;
gap: 5px;
min-width: 0;
}
.fb-33__bub b {
font-size: 13.5px;
font-weight: 650;
}
.fb-33__bub p {
font-size: 13.5px;
line-height: 1.55;
color: oklch(0.35 0.02 265);
text-wrap: pretty;
}
.fb-33__tally {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.fb-33__tally li {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 620;
padding: 4px 9px;
border-radius: 99px;
background: oklch(0.5 0.16 272/.1);
border: 1px solid oklch(0.5 0.16 272/.28);
color: var(--fb-33-brand);
font-variant-numeric: tabular-nums;
animation: fb-33-pop .3s cubic-bezier(.2,1.6,.4,1);
}
@keyframes fb-33-pop {
from {
scale: .5;
opacity: 0;
}
}
.fb-33__dock {
position: relative;
display: grid;
justify-items: center;
gap: 14px;
padding-top: 10px;
}
.fb-33__strip {
list-style: none;
display: flex;
align-items: flex-end;
gap: 6px;
padding: 7px 10px;
border-radius: 99px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 16px 40px -22px oklch(0.22 0.025 265/.9);
opacity: 0;
translate: 0 10px;
pointer-events: none;
transition: opacity .24s ease,translate .3s cubic-bezier(.2,1.1,.3,1);
}
.fb-33__dock[data-open="true"] .fb-33__strip {
opacity: 1;
translate: 0 0;
pointer-events: auto;
}
.fb-33__react {
position: relative;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 24px;
line-height: 1;
scale: 0;
translate: 0 12px;
transform-origin: 50% 100%;
transition: scale .34s cubic-bezier(.2,1.6,.4,1),translate .3s cubic-bezier(.2,1.4,.4,1),background .18s ease;
transition-delay: calc(var(--fb-33-i) * 42ms);
}
.fb-33__dock[data-open="true"] .fb-33__react {
scale: 1;
translate: 0 0;
}
.fb-33__react span {
display: block;
filter: drop-shadow(0 2px 4px oklch(0.22 0.025 265/.2));
}
.fb-33__dock[data-open="true"] .fb-33__react:hover {
scale: 1.5;
translate: 0 -10px;
transition-delay: 0s;
}
.fb-33__dock[data-open="true"] li:hover + li .fb-33__react,
.fb-33__dock[data-open="true"] li:has(+ li:hover) .fb-33__react {
scale: 1.24;
translate: 0 -5px;
transition-delay: 0s;
}
.fb-33__react:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 2px;
scale: 1.35;
}
.fb-33__react[aria-pressed="true"] {
background: oklch(0.5 0.16 272/.14);
}
.fb-33__burst {
position: absolute;
inset: 0;
pointer-events: none;
}
.fb-33__burst i {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fb-33-brand);
animation: fb-33-fly .62s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes fb-33-fly {
to {
translate: calc(cos(var(--fb-33-a)) * 42px) calc(sin(var(--fb-33-a)) * -42px);
opacity: 0;
scale: .3;
}
}
.fb-33__fab {
display: grid;
place-items: center;
inline-size: 58px;
block-size: 58px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.99 0.01 272);
background: linear-gradient(150deg,oklch(0.6 0.17 276),var(--fb-33-brand));
box-shadow: 0 12px 30px -12px oklch(0.5 0.16 272/.85),0 2px 6px oklch(0.3 0.07 272/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),rotate .3s ease;
}
.fb-33__fab svg {
width: 26px;
height: 26px;
}
.fb-33__fab:hover {
transform: translateY(-2px) scale(1.04);
}
.fb-33__fab:active {
transform: scale(.94);
}
.fb-33__fab:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 4px;
}
.fb-33__dock[data-open="true"] .fb-33__fab {
rotate: 22deg;
}
.fb-33__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--fb-33-brand);
min-height: 18px;
}
.fb-33__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-33-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-33 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```
## JavaScript
```js
(() => {
const dock = document.getElementById('fb-33-dock');
const fab = document.getElementById('fb-33-fab');
const tally = document.getElementById('fb-33-tally');
const live = document.getElementById('fb-33-live');
if (!dock || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const items = [...dock.querySelectorAll('.fb-33__react')];
const counts = new Map();
const set = (open) => {
dock.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
items.forEach((i) => { i.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => items[0].focus(), 90);
};
const render = () => {
tally.innerHTML = '';
counts.forEach((n, e) => {
if (!n) return;
const li = document.createElement('li');
li.textContent = e + ' ' + n;
tally.append(li);
});
};
const burst = (btn) => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const wrap = document.createElement('span');
wrap.className = 'fb-33__burst';
for (let i = 0; i < 8; i += 1) {
const p = document.createElement('i');
p.style.setProperty('--fb-33-a', (i * 45) + 'deg');
wrap.append(p);
}
btn.append(wrap);
setTimeout(() => wrap.remove(), 700);
};
items.forEach((btn, idx) => {
btn.setAttribute('aria-label', btn.dataset.fbN + ', 0 reactions');
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
const e = btn.dataset.fbE;
const n = (counts.get(e) || 0) + (on ? -1 : 1);
counts.set(e, Math.max(0, n));
btn.setAttribute('aria-pressed', String(!on));
btn.setAttribute('aria-label', btn.dataset.fbN + ', ' + counts.get(e) + ' reactions');
if (!on) burst(btn);
render();
live.textContent = (on ? 'Removed ' : 'Added ') + btn.dataset.fbN;
});
btn.addEventListener('keydown', (ev) => {
if (ev.key === 'ArrowRight') { ev.preventDefault(); items[(idx + 1) % items.length].focus(); }
if (ev.key === 'ArrowLeft') { ev.preventDefault(); items[(idx - 1 + items.length) % items.length].focus(); }
if (ev.key === 'Escape') { set(false); fab.focus(); }
});
});
fab.addEventListener('click', () => set(dock.dataset.open !== 'true'));
})();
```Here's a working CSS Floating Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Quick Emoji Reply Reaction Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/fb15/
The reaction bar: six quick replies that pop out of a floating button, magnify under the pointer like a desktop dock, and burst into particles when chosen. Every reaction is a labelled toggle with aria-pressed, the counts update live, and the whole strip is keyboard-navigable with arrow keys — because reactions are content, not decoration.
## HTML
```html
<section class="fb-33" aria-label="Quick emoji reaction floating button demo">
<div class="fb-33__stage">
<p class="fb-33__eyebrow">:has() neighbour magnify · trig burst</p>
<h2 class="fb-33__title">React without leaving the thread</h2>
<p class="fb-33__sub">Open the strip, then sweep across it. The hovered reaction grows most and its neighbours grow less — that ramp is what makes it feel like a dock.</p>
<article class="fb-33__msg">
<span class="fb-33__av" aria-hidden="true"></span>
<div class="fb-33__bub">
<b>Marcus Tan</b>
<p>Shipped the FAB set — 34 patterns, all keyboard-tested. Docs are in the same file.</p>
<ul class="fb-33__tally" id="fb-33-tally"></ul>
</div>
</article>
<div class="fb-33__dock" id="fb-33-dock" data-open="false">
<ul class="fb-33__strip" id="fb-33-strip">
<li style="--fb-33-i:0"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="👍" data-fb-n="Thumbs up"><span aria-hidden="true">👍</span></button></li>
<li style="--fb-33-i:1"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🎉" data-fb-n="Celebrate"><span aria-hidden="true">🎉</span></button></li>
<li style="--fb-33-i:2"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🔥" data-fb-n="Fire"><span aria-hidden="true">🔥</span></button></li>
<li style="--fb-33-i:3"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="❤️" data-fb-n="Love"><span aria-hidden="true">❤️</span></button></li>
<li style="--fb-33-i:4"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="😮" data-fb-n="Wow"><span aria-hidden="true">😮</span></button></li>
<li style="--fb-33-i:5"><button class="fb-33__react" type="button" tabindex="-1" aria-pressed="false" data-fb-e="🙌" data-fb-n="Applause"><span aria-hidden="true">🙌</span></button></li>
</ul>
<button class="fb-33__fab" id="fb-33-fab" type="button" aria-expanded="false" aria-controls="fb-33-strip" aria-label="Add a reaction">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.4" fill="none" stroke="currentColor" stroke-width="1.9"/><circle cx="9.3" cy="10.2" r="1.2" fill="currentColor"/><circle cx="14.7" cy="10.2" r="1.2" fill="currentColor"/><path d="M8.6 14.2c1.9 1.9 4.9 1.9 6.8 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
</button>
</div>
<p class="fb-33__live" id="fb-33-live" role="status" aria-live="polite"></p>
<p class="fb-33__chip">arrow keys wrap · 8 particles · aria-pressed toggles</p>
</div>
</section>
```
## CSS
```css
.fb-33 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--fb-33-bg);
--fb-33-bg: linear-gradient(180deg,oklch(0.97 0.01 250),oklch(0.93 0.02 262));
--fb-33-brand: oklch(0.5 0.16 272);
--fb-33-ink: oklch(0.22 0.025 265);
--fb-33-mut: oklch(0.53 0.015 265);
--fb-33-card: oklch(1 0 0);
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--fb-33-ink);
display: grid;
place-items: center;
padding: clamp(20px,5vw,56px);
}
.fb-33 *,
.fb-33 *::before,
.fb-33 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.fb-33__stage {
width: min(100%,56ch);
display: grid;
gap: clamp(16px,3vw,26px);
justify-items: center;
text-align: center;
}
.fb-33__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--fb-33-brand);
}
.fb-33__title {
font-size: clamp(25px,4.4vw,42px);
line-height: 1.05;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.fb-33__sub {
max-width: 50ch;
font-size: clamp(13.5px,1.7vw,16.5px);
line-height: 1.62;
color: var(--fb-33-mut);
text-wrap: pretty;
}
.fb-33__msg {
display: flex;
gap: 12px;
align-items: flex-start;
width: min(100%,440px);
text-align: left;
padding: 14px;
border-radius: 20px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 18px 44px -30px oklch(0.22 0.025 265/.9);
}
.fb-33__av {
flex: none;
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(140deg,oklch(0.78 0.12 250),oklch(0.55 0.16 300)),url("https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?q=80&w=160&auto=format&fit=crop") center/cover;
}
.fb-33__bub {
display: grid;
gap: 5px;
min-width: 0;
}
.fb-33__bub b {
font-size: 13.5px;
font-weight: 650;
}
.fb-33__bub p {
font-size: 13.5px;
line-height: 1.55;
color: oklch(0.35 0.02 265);
text-wrap: pretty;
}
.fb-33__tally {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.fb-33__tally li {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 620;
padding: 4px 9px;
border-radius: 99px;
background: oklch(0.5 0.16 272/.1);
border: 1px solid oklch(0.5 0.16 272/.28);
color: var(--fb-33-brand);
font-variant-numeric: tabular-nums;
animation: fb-33-pop .3s cubic-bezier(.2,1.6,.4,1);
}
@keyframes fb-33-pop {
from {
scale: .5;
opacity: 0;
}
}
.fb-33__dock {
position: relative;
display: grid;
justify-items: center;
gap: 14px;
padding-top: 10px;
}
.fb-33__strip {
list-style: none;
display: flex;
align-items: flex-end;
gap: 6px;
padding: 7px 10px;
border-radius: 99px;
background: var(--fb-33-card);
border: 1px solid oklch(0.22 0.025 265/.08);
box-shadow: 0 16px 40px -22px oklch(0.22 0.025 265/.9);
opacity: 0;
translate: 0 10px;
pointer-events: none;
transition: opacity .24s ease,translate .3s cubic-bezier(.2,1.1,.3,1);
}
.fb-33__dock[data-open="true"] .fb-33__strip {
opacity: 1;
translate: 0 0;
pointer-events: auto;
}
.fb-33__react {
position: relative;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 24px;
line-height: 1;
scale: 0;
translate: 0 12px;
transform-origin: 50% 100%;
transition: scale .34s cubic-bezier(.2,1.6,.4,1),translate .3s cubic-bezier(.2,1.4,.4,1),background .18s ease;
transition-delay: calc(var(--fb-33-i) * 42ms);
}
.fb-33__dock[data-open="true"] .fb-33__react {
scale: 1;
translate: 0 0;
}
.fb-33__react span {
display: block;
filter: drop-shadow(0 2px 4px oklch(0.22 0.025 265/.2));
}
.fb-33__dock[data-open="true"] .fb-33__react:hover {
scale: 1.5;
translate: 0 -10px;
transition-delay: 0s;
}
.fb-33__dock[data-open="true"] li:hover + li .fb-33__react,
.fb-33__dock[data-open="true"] li:has(+ li:hover) .fb-33__react {
scale: 1.24;
translate: 0 -5px;
transition-delay: 0s;
}
.fb-33__react:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 2px;
scale: 1.35;
}
.fb-33__react[aria-pressed="true"] {
background: oklch(0.5 0.16 272/.14);
}
.fb-33__burst {
position: absolute;
inset: 0;
pointer-events: none;
}
.fb-33__burst i {
position: absolute;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fb-33-brand);
animation: fb-33-fly .62s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes fb-33-fly {
to {
translate: calc(cos(var(--fb-33-a)) * 42px) calc(sin(var(--fb-33-a)) * -42px);
opacity: 0;
scale: .3;
}
}
.fb-33__fab {
display: grid;
place-items: center;
inline-size: 58px;
block-size: 58px;
border: 0;
border-radius: 50%;
cursor: pointer;
color: oklch(0.99 0.01 272);
background: linear-gradient(150deg,oklch(0.6 0.17 276),var(--fb-33-brand));
box-shadow: 0 12px 30px -12px oklch(0.5 0.16 272/.85),0 2px 6px oklch(0.3 0.07 272/.28);
transition: transform .28s cubic-bezier(.2,1.3,.4,1),rotate .3s ease;
}
.fb-33__fab svg {
width: 26px;
height: 26px;
}
.fb-33__fab:hover {
transform: translateY(-2px) scale(1.04);
}
.fb-33__fab:active {
transform: scale(.94);
}
.fb-33__fab:focus-visible {
outline: 3px solid var(--fb-33-brand);
outline-offset: 4px;
}
.fb-33__dock[data-open="true"] .fb-33__fab {
rotate: 22deg;
}
.fb-33__live {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--fb-33-brand);
min-height: 18px;
}
.fb-33__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11px;
color: var(--fb-33-mut);
padding: 8px 14px;
border: 1px solid oklch(0.22 0.025 265/.14);
border-radius: 99px;
}
@media (prefers-reduced-motion: reduce) {
.fb-33 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```
## JavaScript
```js
(() => {
const dock = document.getElementById('fb-33-dock');
const fab = document.getElementById('fb-33-fab');
const tally = document.getElementById('fb-33-tally');
const live = document.getElementById('fb-33-live');
if (!dock || !fab || fab.dataset.fbWired) return;
fab.dataset.fbWired = '1';
const items = [...dock.querySelectorAll('.fb-33__react')];
const counts = new Map();
const set = (open) => {
dock.dataset.open = String(open);
fab.setAttribute('aria-expanded', String(open));
items.forEach((i) => { i.tabIndex = open ? 0 : -1; });
if (open) setTimeout(() => items[0].focus(), 90);
};
const render = () => {
tally.innerHTML = '';
counts.forEach((n, e) => {
if (!n) return;
const li = document.createElement('li');
li.textContent = e + ' ' + n;
tally.append(li);
});
};
const burst = (btn) => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const wrap = document.createElement('span');
wrap.className = 'fb-33__burst';
for (let i = 0; i < 8; i += 1) {
const p = document.createElement('i');
p.style.setProperty('--fb-33-a', (i * 45) + 'deg');
wrap.append(p);
}
btn.append(wrap);
setTimeout(() => wrap.remove(), 700);
};
items.forEach((btn, idx) => {
btn.setAttribute('aria-label', btn.dataset.fbN + ', 0 reactions');
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
const e = btn.dataset.fbE;
const n = (counts.get(e) || 0) + (on ? -1 : 1);
counts.set(e, Math.max(0, n));
btn.setAttribute('aria-pressed', String(!on));
btn.setAttribute('aria-label', btn.dataset.fbN + ', ' + counts.get(e) + ' reactions');
if (!on) burst(btn);
render();
live.textContent = (on ? 'Removed ' : 'Added ') + btn.dataset.fbN;
});
btn.addEventListener('keydown', (ev) => {
if (ev.key === 'ArrowRight') { ev.preventDefault(); items[(idx + 1) % items.length].focus(); }
if (ev.key === 'ArrowLeft') { ev.preventDefault(); items[(idx - 1 + items.length) % items.length].focus(); }
if (ev.key === 'Escape') { set(false); fab.focus(); }
});
});
fab.addEventListener('click', () => set(dock.dataset.open !== 'true'));
})();
```How this works
Two independent behaviours share one strip. Entry is a staggered spring; magnification is proximity-based:
/* entry */
.react{ scale:0; translate:0 12px; transition:scale .34s cubic-bezier(.2,1.6,.4,1);
transition-delay:calc(var(--i) * 42ms) }
[data-open="true"] .react{ scale:1; translate:0 0 }
/* dock magnify */
.react:hover{ scale:1.5; translate:0 -10px }
.react:hover + .react, .react:has(+ .react:hover){ scale:1.25; translate:0 -5px }The neighbour rules are what make it feel like a dock rather than a row of buttons: the hovered item grows most, its immediate siblings grow less. :has(+ .react:hover) reaches backwards to the previous sibling — something CSS could not do before :has() and which used to require JavaScript.
The burst is eight particles positioned with cos()/sin() from an index, animated outward once and removed on animationend. Generating them in JS keeps the markup clean; doing the geometry in CSS keeps the JS to five lines.
Each reaction is a toggle: aria-pressed tracks whether you reacted, the count updates in the accessible name, and the strip supports Arrow keys with wrap-around plus Escape to close. Reaction bars that are div soup are unusable with a screen reader — and reactions are exactly the kind of thing people want to check without looking.
Make it yours
- Reaction set: six is the maximum before the strip gets wider than a thumb's reach. Put the most-used first, not the most expressive.
- Skin tones / variants: long-press a reaction to open a second row, the same way messaging apps handle variants.
- Counts: show them under each reaction once any is above zero; hide the row entirely at zero to avoid an empty scoreboard.
- Custom emoji: swap the glyphs for SVG or images of team emoji — the dock magnification works identically.
Gotchas — read before shipping
- Emoji render at wildly different sizes across platforms. Set a fixed font-size and line-height on the glyph and centre it in a fixed box.
- Do not animate scale on the strip container; the magnification must be per-item or the neighbours all move together.
- Particle bursts must clean themselves up. Removing them on animationend prevents hundreds of orphaned nodes after heavy use.
- A reaction that only communicates via colour or scale is invisible to screen readers. aria-pressed plus a count in the label is the minimum.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by :has(), oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
:has() needs Chrome 105+, Safari 15.4+, Firefox 121+; without it the dock magnification affects only the hovered item, which still works fine. CSS trig functions need Chrome 111 / Safari 15.4 / Firefox 108.