34 CSS Floating Buttons31 / 34
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
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><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;
}
}.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);
});
})();(() => {
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);
});
})();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);
});
})();
```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.getByteFrequencyDatafrom 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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.