34 CSS Floating Buttons
31 / 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

Voice Recording Microphone Floating Button CSS

Press-and-hold to record, exactly as messaging apps do it: the button swells, a live waveform grows out of it, a tabular timer counts up, and sliding away cancels. Because press-and-hold is unusable for many people, it also supports a click-to-toggle mode and full keyboard control — Space starts and stops, Escape cancels.

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-31" aria-label="Voice recording microphone button demo">
  <div class="fb-31__stage">
    <p class="fb-31__eyebrow">hold · slide to cancel · Space toggles</p>
    <h2 class="fb-31__title">Hold it down</h2>
    <p class="fb-31__sub">Press and hold the microphone. Slide left past the marker to discard. Prefer the keyboard? Focus it and press Space.</p>
    <div class="fb-31__panel" id="fb-31-panel" data-state="idle">
      <div class="fb-31__wave" id="fb-31-wave" aria-hidden="true"></div>
      <div class="fb-31__meta">
        <span class="fb-31__dot" aria-hidden="true"></span>
        <span class="fb-31__time" id="fb-31-time">0:00</span>
        <span class="fb-31__cancel" id="fb-31-cancelHint">‹ slide to cancel</span>
      </div>
    </div>
    <button class="fb-31__mic" id="fb-31-mic" type="button" aria-pressed="false" aria-label="Hold to record a voice message">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="9" y="3" width="6" height="11" rx="3" fill="currentColor"/><path d="M6 11.5a6 6 0 0 0 12 0M12 17.5V21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
    </button>
    <p class="fb-31__live" id="fb-31-live" role="status" aria-live="polite">Ready to record</p>
    <p class="fb-31__chip">28 bars at 12Hz · tabular timer · Escape discards</p>
  </div>
</section>
.fb-31 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-31-bg);
  --fb-31-bg: radial-gradient(110% 80% at 50% 100%,oklch(0.27 0.06 20),oklch(0.15 0.02 15) 70%);
  --fb-31-rec: oklch(0.64 0.22 22);
  --fb-31-ink: oklch(0.97 0.01 20);
  --fb-31-mut: oklch(0.76 0.02 20);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-31-ink);
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
}

.fb-31 *,
.fb-31 *::before,
.fb-31 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.fb-31__stage {
  width: min(100%,56ch);
  display: grid;
  gap: clamp(16px,3vw,24px);
  justify-items: center;
  text-align: center;
}

.fb-31__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: oklch(0.8 0.15 30);
}

.fb-31__title {
  font-size: clamp(26px,4.8vw,46px);
  line-height: 1.04;
  letter-spacing: -.035em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-31__sub {
  max-width: 48ch;
  font-size: clamp(13.5px,1.7vw,16.5px);
  line-height: 1.62;
  color: var(--fb-31-mut);
  text-wrap: pretty;
}

.fb-31__panel {
  width: min(100%,420px);
  display: grid;
  gap: 12px;
  padding: 18px;
  border-radius: 22px;
  background: oklch(1 0 0/.05);
  border: 1px solid oklch(1 0 0/.12);
  opacity: .45;
  transition: opacity .3s ease,border-color .3s ease,background .3s ease;
}

.fb-31__panel[data-state="rec"] {
  opacity: 1;
  background: oklch(0.64 0.22 22/.1);
  border-color: oklch(0.64 0.22 22/.4);
}

.fb-31__wave {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  height: 64px;
}

.fb-31__wave i {
  display: block;
  width: 4px;
  border-radius: 99px;
  background: linear-gradient(180deg,oklch(0.85 0.16 30),var(--fb-31-rec));
  height: calc(6px + var(--fb-31-h,0) * 52px);
  opacity: .5;
  transition: height .09s linear,opacity .2s ease;
}

.fb-31__panel[data-state="rec"] .fb-31__wave i {
  opacity: 1;
}

.fb-31__meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 13px;
}

.fb-31__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--fb-31-rec);
  opacity: .35;
}

.fb-31__panel[data-state="rec"] .fb-31__dot {
  opacity: 1;
  animation: fb-31-blink 1.1s ease-in-out infinite;
}

@keyframes fb-31-blink {
  0%,
    100% {
    opacity: 1;
  }

  50% {
    opacity: .25;
  }
}

.fb-31__time {
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  min-width: 5ch;
}

.fb-31__cancel {
  color: var(--fb-31-mut);
  font-size: 11.5px;
  opacity: 0;
  transition: opacity .25s ease;
}

.fb-31__panel[data-state="rec"] .fb-31__cancel {
  opacity: 1;
}

.fb-31__mic {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 84px;
  block-size: 84px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  touch-action: none;
  color: oklch(0.99 0.01 22);
  background: linear-gradient(150deg,oklch(0.74 0.19 30),var(--fb-31-rec));
  box-shadow: 0 18px 44px -16px oklch(0.64 0.22 22/.75),0 3px 8px oklch(0.08 0.01 15/.7);
  transition: scale .28s cubic-bezier(.2,1.35,.4,1),box-shadow .3s ease;
}

.fb-31__mic svg {
  width: 34px;
  height: 34px;
}

.fb-31__mic:hover {
  scale: 1.04;
}

.fb-31__mic:focus-visible {
  outline: 3px solid oklch(0.98 0.01 22);
  outline-offset: 6px;
}

.fb-31__mic[aria-pressed="true"] {
  scale: 1.12;
  box-shadow: 0 22px 60px -14px oklch(0.64 0.22 22/.9),0 0 0 10px oklch(0.64 0.22 22/.14),0 0 0 22px oklch(0.64 0.22 22/.07);
}

.fb-31__mic[data-cancel="true"] {
  background: linear-gradient(150deg,oklch(0.5 0.02 20),oklch(0.35 0.02 20));
}

.fb-31__live {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  color: var(--fb-31-mut);
  background: oklch(1 0 0/.06);
  border-radius: 10px;
  padding: 8px 15px;
}

.fb-31__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--fb-31-mut);
  padding: 8px 14px;
  border: 1px solid oklch(1 0 0/.16);
  border-radius: 99px;
}

@media (prefers-reduced-motion: reduce) {
  .fb-31 * {
    animation: none !important;
    transition: none !important;
  }
}
(() => {
  const mic = document.getElementById('fb-31-mic');
  const panel = document.getElementById('fb-31-panel');
  const wave = document.getElementById('fb-31-wave');
  const timeEl = document.getElementById('fb-31-time');
  const live = document.getElementById('fb-31-live');
  if (!mic || mic.dataset.fbWired) return;
  mic.dataset.fbWired = '1';
  const BARS = 28;
  for (let i = 0; i < BARS; i += 1) wave.append(document.createElement('i'));
  const bars = [...wave.children];
  let recording = false, t0 = 0, tick = null, meter = null, startX = 0;
  const fmt = (ms) => {
    const s = Math.floor(ms / 1000);
    return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
  };
  const start = () => {
    if (recording) return;
    recording = true; t0 = performance.now();
    panel.dataset.state = 'rec';
    mic.setAttribute('aria-pressed', 'true');
    mic.setAttribute('aria-label', 'Recording. Release to send, Escape to discard.');
    live.textContent = 'Recording…';
    tick = setInterval(() => { timeEl.textContent = fmt(performance.now() - t0); }, 200);
    meter = setInterval(() => {
      bars.forEach((b, i) => {
        const env = Math.sin((i / BARS) * Math.PI);
        b.style.setProperty('--fb-31-h', (0.15 + Math.random() * 0.85 * env).toFixed(2));
      });
    }, 84);
  };
  const finish = (cancelled) => {
    if (!recording) return;
    const dur = fmt(performance.now() - t0);
    recording = false;
    clearInterval(tick); clearInterval(meter);
    panel.dataset.state = 'idle';
    mic.setAttribute('aria-pressed', 'false');
    mic.dataset.cancel = 'false';
    mic.setAttribute('aria-label', 'Hold to record a voice message');
    bars.forEach((b) => b.style.setProperty('--fb-31-h', '0'));
    live.textContent = cancelled ? 'Recording discarded' : 'Voice message ready · ' + dur;
    timeEl.textContent = '0:00';
  };
  mic.addEventListener('pointerdown', (e) => { mic.setPointerCapture(e.pointerId); startX = e.clientX; start(); });
  mic.addEventListener('pointermove', (e) => {
    if (!recording) return;
    const slid = startX - e.clientX > 90;
    mic.dataset.cancel = String(slid);
    if (slid) finish(true);
  });
  mic.addEventListener('pointerup', () => finish(false));
  mic.addEventListener('pointercancel', () => finish(true));
  mic.addEventListener('keydown', (e) => {
    if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); recording ? finish(false) : start(); }
    if (e.key === 'Escape') finish(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: Voice Recording Microphone Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/fb13/

Press-and-hold to record, exactly as messaging apps do it: the button swells, a live waveform grows out of it, a tabular timer counts up, and sliding away cancels. Because press-and-hold is unusable for many people, it also supports a click-to-toggle mode and full keyboard control — Space starts and stops, Escape cancels.
## HTML
```html
<section class="fb-31" aria-label="Voice recording microphone button demo">
  <div class="fb-31__stage">
    <p class="fb-31__eyebrow">hold · slide to cancel · Space toggles</p>
    <h2 class="fb-31__title">Hold it down</h2>
    <p class="fb-31__sub">Press and hold the microphone. Slide left past the marker to discard. Prefer the keyboard? Focus it and press Space.</p>
    <div class="fb-31__panel" id="fb-31-panel" data-state="idle">
      <div class="fb-31__wave" id="fb-31-wave" aria-hidden="true"></div>
      <div class="fb-31__meta">
        <span class="fb-31__dot" aria-hidden="true"></span>
        <span class="fb-31__time" id="fb-31-time">0:00</span>
        <span class="fb-31__cancel" id="fb-31-cancelHint">‹ slide to cancel</span>
      </div>
    </div>
    <button class="fb-31__mic" id="fb-31-mic" type="button" aria-pressed="false" aria-label="Hold to record a voice message">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="9" y="3" width="6" height="11" rx="3" fill="currentColor"/><path d="M6 11.5a6 6 0 0 0 12 0M12 17.5V21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
    </button>
    <p class="fb-31__live" id="fb-31-live" role="status" aria-live="polite">Ready to record</p>
    <p class="fb-31__chip">28 bars at 12Hz · tabular timer · Escape discards</p>
  </div>
</section>
```
## CSS
```css
.fb-31 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-31-bg);
  --fb-31-bg: radial-gradient(110% 80% at 50% 100%,oklch(0.27 0.06 20),oklch(0.15 0.02 15) 70%);
  --fb-31-rec: oklch(0.64 0.22 22);
  --fb-31-ink: oklch(0.97 0.01 20);
  --fb-31-mut: oklch(0.76 0.02 20);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-31-ink);
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
}

.fb-31 *,
.fb-31 *::before,
.fb-31 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.fb-31__stage {
  width: min(100%,56ch);
  display: grid;
  gap: clamp(16px,3vw,24px);
  justify-items: center;
  text-align: center;
}

.fb-31__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: oklch(0.8 0.15 30);
}

.fb-31__title {
  font-size: clamp(26px,4.8vw,46px);
  line-height: 1.04;
  letter-spacing: -.035em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-31__sub {
  max-width: 48ch;
  font-size: clamp(13.5px,1.7vw,16.5px);
  line-height: 1.62;
  color: var(--fb-31-mut);
  text-wrap: pretty;
}

.fb-31__panel {
  width: min(100%,420px);
  display: grid;
  gap: 12px;
  padding: 18px;
  border-radius: 22px;
  background: oklch(1 0 0/.05);
  border: 1px solid oklch(1 0 0/.12);
  opacity: .45;
  transition: opacity .3s ease,border-color .3s ease,background .3s ease;
}

.fb-31__panel[data-state="rec"] {
  opacity: 1;
  background: oklch(0.64 0.22 22/.1);
  border-color: oklch(0.64 0.22 22/.4);
}

.fb-31__wave {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  height: 64px;
}

.fb-31__wave i {
  display: block;
  width: 4px;
  border-radius: 99px;
  background: linear-gradient(180deg,oklch(0.85 0.16 30),var(--fb-31-rec));
  height: calc(6px + var(--fb-31-h,0) * 52px);
  opacity: .5;
  transition: height .09s linear,opacity .2s ease;
}

.fb-31__panel[data-state="rec"] .fb-31__wave i {
  opacity: 1;
}

.fb-31__meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 13px;
}

.fb-31__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--fb-31-rec);
  opacity: .35;
}

.fb-31__panel[data-state="rec"] .fb-31__dot {
  opacity: 1;
  animation: fb-31-blink 1.1s ease-in-out infinite;
}

@keyframes fb-31-blink {
  0%,
    100% {
    opacity: 1;
  }

  50% {
    opacity: .25;
  }
}

.fb-31__time {
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  min-width: 5ch;
}

.fb-31__cancel {
  color: var(--fb-31-mut);
  font-size: 11.5px;
  opacity: 0;
  transition: opacity .25s ease;
}

.fb-31__panel[data-state="rec"] .fb-31__cancel {
  opacity: 1;
}

.fb-31__mic {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 84px;
  block-size: 84px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  touch-action: none;
  color: oklch(0.99 0.01 22);
  background: linear-gradient(150deg,oklch(0.74 0.19 30),var(--fb-31-rec));
  box-shadow: 0 18px 44px -16px oklch(0.64 0.22 22/.75),0 3px 8px oklch(0.08 0.01 15/.7);
  transition: scale .28s cubic-bezier(.2,1.35,.4,1),box-shadow .3s ease;
}

.fb-31__mic svg {
  width: 34px;
  height: 34px;
}

.fb-31__mic:hover {
  scale: 1.04;
}

.fb-31__mic:focus-visible {
  outline: 3px solid oklch(0.98 0.01 22);
  outline-offset: 6px;
}

.fb-31__mic[aria-pressed="true"] {
  scale: 1.12;
  box-shadow: 0 22px 60px -14px oklch(0.64 0.22 22/.9),0 0 0 10px oklch(0.64 0.22 22/.14),0 0 0 22px oklch(0.64 0.22 22/.07);
}

.fb-31__mic[data-cancel="true"] {
  background: linear-gradient(150deg,oklch(0.5 0.02 20),oklch(0.35 0.02 20));
}

.fb-31__live {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12px;
  color: var(--fb-31-mut);
  background: oklch(1 0 0/.06);
  border-radius: 10px;
  padding: 8px 15px;
}

.fb-31__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--fb-31-mut);
  padding: 8px 14px;
  border: 1px solid oklch(1 0 0/.16);
  border-radius: 99px;
}

@media (prefers-reduced-motion: reduce) {
  .fb-31 * {
    animation: none !important;
    transition: none !important;
  }
}
```

## JavaScript
```js
(() => {
  const mic = document.getElementById('fb-31-mic');
  const panel = document.getElementById('fb-31-panel');
  const wave = document.getElementById('fb-31-wave');
  const timeEl = document.getElementById('fb-31-time');
  const live = document.getElementById('fb-31-live');
  if (!mic || mic.dataset.fbWired) return;
  mic.dataset.fbWired = '1';
  const BARS = 28;
  for (let i = 0; i < BARS; i += 1) wave.append(document.createElement('i'));
  const bars = [...wave.children];
  let recording = false, t0 = 0, tick = null, meter = null, startX = 0;
  const fmt = (ms) => {
    const s = Math.floor(ms / 1000);
    return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
  };
  const start = () => {
    if (recording) return;
    recording = true; t0 = performance.now();
    panel.dataset.state = 'rec';
    mic.setAttribute('aria-pressed', 'true');
    mic.setAttribute('aria-label', 'Recording. Release to send, Escape to discard.');
    live.textContent = 'Recording…';
    tick = setInterval(() => { timeEl.textContent = fmt(performance.now() - t0); }, 200);
    meter = setInterval(() => {
      bars.forEach((b, i) => {
        const env = Math.sin((i / BARS) * Math.PI);
        b.style.setProperty('--fb-31-h', (0.15 + Math.random() * 0.85 * env).toFixed(2));
      });
    }, 84);
  };
  const finish = (cancelled) => {
    if (!recording) return;
    const dur = fmt(performance.now() - t0);
    recording = false;
    clearInterval(tick); clearInterval(meter);
    panel.dataset.state = 'idle';
    mic.setAttribute('aria-pressed', 'false');
    mic.dataset.cancel = 'false';
    mic.setAttribute('aria-label', 'Hold to record a voice message');
    bars.forEach((b) => b.style.setProperty('--fb-31-h', '0'));
    live.textContent = cancelled ? 'Recording discarded' : 'Voice message ready · ' + dur;
    timeEl.textContent = '0:00';
  };
  mic.addEventListener('pointerdown', (e) => { mic.setPointerCapture(e.pointerId); startX = e.clientX; start(); });
  mic.addEventListener('pointermove', (e) => {
    if (!recording) return;
    const slid = startX - e.clientX > 90;
    mic.dataset.cancel = String(slid);
    if (slid) finish(true);
  });
  mic.addEventListener('pointerup', () => finish(false));
  mic.addEventListener('pointercancel', () => finish(true));
  mic.addEventListener('keydown', (e) => {
    if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); recording ? finish(false) : start(); }
    if (e.key === 'Escape') finish(true);
  });
})();
```

How this works

Pointer down starts, pointer up stops, and horizontal travel past a threshold cancels:

btn.addEventListener('pointerdown', start);
btn.addEventListener('pointermove', e => {
  if (recording && startX - e.clientX > 90) cancel();   // slide-to-cancel
});
btn.addEventListener('pointerup', () => recording && stop());

The waveform is 28 bars whose heights are driven by a custom property updated on a setInterval at 12Hz — deliberately not 60fps, because real level meters lag and a perfectly smooth waveform reads as fake. Each bar transitions its own height over 90ms, so the motion smooths itself.

The timer uses font-variant-numeric:tabular-nums and formats through padded integers, so digits never shift the layout as they change. A jittering timer beside a pulsing button is visual noise you notice without knowing why.

Keyboard and toggle modes exist because holding a pointer down for 30 seconds is not possible for everyone. Space toggles start/stop, Escape cancels and discards, and the state is announced through a status region at each transition — start, stop with duration, or cancelled.

Make it yours

  • Real levels: swap the random heights for AnalyserNode.getByteFrequencyData from getUserMedia — the visual layer needs no changes.
  • Lock mode: swipe up to lock recording hands-free, the pattern most messaging apps ship. Add a second threshold on the Y axis.
  • Max duration: draw a thin progress ring around the button and auto-stop at the limit rather than silently truncating.
  • Playback state: reuse the same button as a play control after recording, with the waveform frozen as a static preview.

Gotchas — read before shipping

  • Press-and-hold is inaccessible on its own. Always ship a toggle or keyboard equivalent — this is a common audit failure.
  • Set touch-action:none on the button or the page scrolls under the finger mid-recording on mobile.
  • Recording UI must be unmistakable. A subtle colour change is not enough — size, colour, a timer and motion together.
  • Requesting microphone permission on page load rather than on first press is the fastest way to get permanently blocked.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

Pointer Events and CSS custom properties are universal. The demo simulates levels; a production version needs MediaRecorder (Chrome 47+, Firefox 25+, Safari 14.1+) and a secure context.

Search CodeFronts

Loading…