34 CSS Floating Buttons
33 / 34

All 34 in 34 CSS Floating Buttons

  1. 01Material Design 3 Floating Action Button (FAB)
  2. 02iOS-Style Compose Floating Button
  3. 03WhatsApp Floating Message Button CSS
  4. 04Intercom / Drift Chat Bubble Floating Button
  5. 05Accessible WAI-ARIA Floating Action Button (WCAG 2.2)
  6. 06Extended FAB with Icon and Label (Material Design 3)
  7. 07Speed Dial Radial Menu FAB with Backdrop
  8. 08Pure CSS FAB Speed Dial (No JavaScript)
  9. 09Material Gradient Floating Action Button CSS
  10. 10Vertical and Horizontal Speed Dial FAB Menu CSS
  11. 11Morphing FAB Button That Expands to Pill CSS
  12. 12Scroll-to-Top Floating Button with SVG Progress Ring
  13. 13Floating Chat Widget FAB with Notification Panel
  14. 14Fan and Arc Radial Spread FAB Menu CSS
  15. 15Apple Squircle Superellipse Floating Button CSS
  16. 16Social Share Floating Button Cluster CSS
  17. 17Glassmorphism Floating Action Button with Backdrop Blur
  18. 18Cyberpunk Neon Glow Floating Button CSS
  19. 19Floating Compose Button with Tooltip on Hover CSS
  20. 20Draggable Floating Action Button with Corner Snap CSS
  21. 21Notification Bell Floating Button with Badge Counter
  22. 22Floating Cart Button with Live Badge Counter CSS
  23. 23Magnetic Cursor Follower Floating Button CSS
  24. 24Dark Mode Theme Toggle Floating Button CSS
  25. 25Classic Gradient Circle Plus FAB CSS
  26. 26Pulse Ring Attention-Getter Floating Button CSS
  27. 27Pill-Shaped FAB with Icon and Text Label CSS
  28. 28Neo-Brutalist Hard Shadow Floating Button CSS
  29. 29Floating Button with Loading Spinner State CSS
  30. 30Draggable Floating Button with 6-Dot Grip Handle CSS
  31. 31Voice Recording Microphone Floating Button CSS
  32. 32Aurora Gradient Drift Premium Floating Button CSS
  33. 33Quick Emoji Reply Reaction Floating Button CSS
  34. 34Modern Squircle Rounded Square FAB CSS
Light JSMIT licensed

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.

Live Demo
Try it

Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149

Nucleo · Affiliate link

Get the Nucleo bundle

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>
.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'));
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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

ChromeSafariFirefoxEdge
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.

Search CodeFronts

Loading…