34 CSS Floating Buttons31 / 34

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.

Published Updated

Live Demo
Try it

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…