16 CSS Pulse Animations15 / 16
Voice Search Mic Pulse
A voice-assistant modal showing an active listening state — microphone icon at the center with expanding concentric rings pulsing outward + a live waveform of vertical bars responding to "voice input." Deep purple gradient background (#4c1d95 to #7c3aed) with white mic icon and glassmorphic modal card. Google Assistant / Alexa / Siri modal aesthetic. The pulse conveys "I'm actively listening, keep speaking."
Published
The code
<div class="pl-15">
<div class="pl-15__modal" role="dialog" aria-modal="true" aria-labelledby="pl-15-title">
<button class="pl-15__close" aria-label="Cancel voice input">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="18" y1="6" x2="6" y2="18"/>
</svg>
</button>
<p class="pl-15__eyebrow">Northlight Assistant</p>
<h2 class="pl-15__title" id="pl-15-title">Listening…</h2>
<button class="pl-15__mic" aria-label="Stop listening">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</button>
<div class="pl-15__wave" aria-hidden="true">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<p class="pl-15__hint">Say something like <em>"What's the weather today?"</em></p>
<p class="pl-15__status" aria-live="polite">Listening now · release to send</p>
</div>
</div><div class="pl-15">
<div class="pl-15__modal" role="dialog" aria-modal="true" aria-labelledby="pl-15-title">
<button class="pl-15__close" aria-label="Cancel voice input">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="18" y1="6" x2="6" y2="18"/>
</svg>
</button>
<p class="pl-15__eyebrow">Northlight Assistant</p>
<h2 class="pl-15__title" id="pl-15-title">Listening…</h2>
<button class="pl-15__mic" aria-label="Stop listening">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</button>
<div class="pl-15__wave" aria-hidden="true">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<p class="pl-15__hint">Say something like <em>"What's the weather today?"</em></p>
<p class="pl-15__status" aria-live="polite">Listening now · release to send</p>
</div>
</div>.pl-15 {
--brand-a: #4c1d95;
--brand-b: #7c3aed;
--brand-glow: rgba(167,139,250,.5);
--ink: #f5f3ff;
--sub: #c4b5fd;
--white: #ffffff;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Rounded','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: grid;
place-items: center;
padding: 32px;
background: radial-gradient(60% 100% at 20% 0%, rgba(167,139,250,.3) 0%, transparent 55%), radial-gradient(70% 100% at 100% 100%, rgba(124,58,237,.35) 0%, transparent 55%), linear-gradient(135deg,var(--brand-a) 0%,var(--brand-b) 100%);
color: var(--ink);
}
.pl-15 *,
.pl-15 *::before,
.pl-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pl-15 ::selection {
background: var(--white);
color: var(--brand-a);
}
.pl-15__modal {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: min(420px,100%);
padding: 36px 28px 28px;
border-radius: 20px;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.14);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 30px 80px -20px rgba(76,29,149,.6);
}
.pl-15__close {
position: absolute;
top: 14px;
right: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
background: rgba(255,255,255,.1);
color: var(--ink);
cursor: pointer;
transition: background .15s ease;
}
.pl-15__close:hover {
background: rgba(255,255,255,.2);
}
.pl-15__close:focus-visible {
outline: 2px solid var(--white);
outline-offset: 2px;
}
.pl-15__eyebrow {
font-size: .72rem;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--sub);
margin-bottom: 8px;
}
.pl-15__title {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -.02em;
line-height: 1.1;
margin-bottom: 32px;
}
/* Mic button with concentric outward rings */
.pl-15__mic {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: none;
border-radius: 50%;
background: var(--white);
color: var(--brand-a);
cursor: pointer;
box-shadow: 0 10px 30px -8px rgba(255,255,255,.4);
margin-bottom: 22px;
z-index: 1;
transition: transform .15s ease;
}
.pl-15__mic:hover {
transform: scale(1.03);
}
.pl-15__mic:focus-visible {
outline: 3px solid var(--white);
outline-offset: 6px;
}
.pl-15__mic svg {
position: relative;
z-index: 1;
}
.pl-15__mic::before,
.pl-15__mic::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--brand-glow);
pointer-events: none;
z-index: 0;
animation: pl-15-ring 1.8s ease-out infinite;
}
.pl-15__mic::after {
animation-delay: .9s;
}
@keyframes pl-15-ring {
0% {
transform: scale(1);
opacity: .55;
}
70% {
transform: scale(2.3);
opacity: 0;
}
100% {
transform: scale(2.4);
opacity: 0;
}
}
/* Waveform bars — 5 vertical bars with coprime durations */
.pl-15__wave {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 36px;
margin-bottom: 20px;
}
.pl-15__wave span {
display: inline-block;
width: 4px;
height: 100%;
border-radius: 2px;
background: var(--white);
transform-origin: center;
}
.pl-15__wave span:nth-child(1) {
animation: pl-15-bar .6s ease-in-out -.2s infinite;
}
.pl-15__wave span:nth-child(2) {
animation: pl-15-bar .9s ease-in-out -.5s infinite;
}
.pl-15__wave span:nth-child(3) {
animation: pl-15-bar .7s ease-in-out -.1s infinite;
}
.pl-15__wave span:nth-child(4) {
animation: pl-15-bar .8s ease-in-out -.4s infinite;
}
.pl-15__wave span:nth-child(5) {
animation: pl-15-bar .5s ease-in-out 0s infinite;
}
@keyframes pl-15-bar {
0%,
100% {
transform: scaleY(.3);
}
50% {
transform: scaleY(1);
}
}
.pl-15__hint {
font-size: .88rem;
color: var(--sub);
text-align: center;
line-height: 1.4;
margin-bottom: 14px;
}
.pl-15__hint em {
color: var(--white);
font-style: normal;
font-weight: 600;
}
.pl-15__status {
padding: 8px 14px;
border-radius: 8px;
background: rgba(0,0,0,.15);
color: var(--sub);
font-size: .72rem;
font-weight: 600;
letter-spacing: .04em;
}
@media (prefers-reduced-motion: reduce) {
.pl-15__mic::before,
.pl-15__mic::after,
.pl-15__wave span {
animation: none!important;
transform: none!important;
}
.pl-15__mic::before,
.pl-15__mic::after {
opacity: 0!important;
}
.pl-15__wave span {
transform: scaleY(.7)!important;
}
}.pl-15 {
--brand-a: #4c1d95;
--brand-b: #7c3aed;
--brand-glow: rgba(167,139,250,.5);
--ink: #f5f3ff;
--sub: #c4b5fd;
--white: #ffffff;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Rounded','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: grid;
place-items: center;
padding: 32px;
background: radial-gradient(60% 100% at 20% 0%, rgba(167,139,250,.3) 0%, transparent 55%), radial-gradient(70% 100% at 100% 100%, rgba(124,58,237,.35) 0%, transparent 55%), linear-gradient(135deg,var(--brand-a) 0%,var(--brand-b) 100%);
color: var(--ink);
}
.pl-15 *,
.pl-15 *::before,
.pl-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pl-15 ::selection {
background: var(--white);
color: var(--brand-a);
}
.pl-15__modal {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: min(420px,100%);
padding: 36px 28px 28px;
border-radius: 20px;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.14);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 30px 80px -20px rgba(76,29,149,.6);
}
.pl-15__close {
position: absolute;
top: 14px;
right: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
background: rgba(255,255,255,.1);
color: var(--ink);
cursor: pointer;
transition: background .15s ease;
}
.pl-15__close:hover {
background: rgba(255,255,255,.2);
}
.pl-15__close:focus-visible {
outline: 2px solid var(--white);
outline-offset: 2px;
}
.pl-15__eyebrow {
font-size: .72rem;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--sub);
margin-bottom: 8px;
}
.pl-15__title {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -.02em;
line-height: 1.1;
margin-bottom: 32px;
}
/* Mic button with concentric outward rings */
.pl-15__mic {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: none;
border-radius: 50%;
background: var(--white);
color: var(--brand-a);
cursor: pointer;
box-shadow: 0 10px 30px -8px rgba(255,255,255,.4);
margin-bottom: 22px;
z-index: 1;
transition: transform .15s ease;
}
.pl-15__mic:hover {
transform: scale(1.03);
}
.pl-15__mic:focus-visible {
outline: 3px solid var(--white);
outline-offset: 6px;
}
.pl-15__mic svg {
position: relative;
z-index: 1;
}
.pl-15__mic::before,
.pl-15__mic::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--brand-glow);
pointer-events: none;
z-index: 0;
animation: pl-15-ring 1.8s ease-out infinite;
}
.pl-15__mic::after {
animation-delay: .9s;
}
@keyframes pl-15-ring {
0% {
transform: scale(1);
opacity: .55;
}
70% {
transform: scale(2.3);
opacity: 0;
}
100% {
transform: scale(2.4);
opacity: 0;
}
}
/* Waveform bars — 5 vertical bars with coprime durations */
.pl-15__wave {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 36px;
margin-bottom: 20px;
}
.pl-15__wave span {
display: inline-block;
width: 4px;
height: 100%;
border-radius: 2px;
background: var(--white);
transform-origin: center;
}
.pl-15__wave span:nth-child(1) {
animation: pl-15-bar .6s ease-in-out -.2s infinite;
}
.pl-15__wave span:nth-child(2) {
animation: pl-15-bar .9s ease-in-out -.5s infinite;
}
.pl-15__wave span:nth-child(3) {
animation: pl-15-bar .7s ease-in-out -.1s infinite;
}
.pl-15__wave span:nth-child(4) {
animation: pl-15-bar .8s ease-in-out -.4s infinite;
}
.pl-15__wave span:nth-child(5) {
animation: pl-15-bar .5s ease-in-out 0s infinite;
}
@keyframes pl-15-bar {
0%,
100% {
transform: scaleY(.3);
}
50% {
transform: scaleY(1);
}
}
.pl-15__hint {
font-size: .88rem;
color: var(--sub);
text-align: center;
line-height: 1.4;
margin-bottom: 14px;
}
.pl-15__hint em {
color: var(--white);
font-style: normal;
font-weight: 600;
}
.pl-15__status {
padding: 8px 14px;
border-radius: 8px;
background: rgba(0,0,0,.15);
color: var(--sub);
font-size: .72rem;
font-weight: 600;
letter-spacing: .04em;
}
@media (prefers-reduced-motion: reduce) {
.pl-15__mic::before,
.pl-15__mic::after,
.pl-15__wave span {
animation: none!important;
transform: none!important;
}
.pl-15__mic::before,
.pl-15__mic::after {
opacity: 0!important;
}
.pl-15__wave span {
transform: scaleY(.7)!important;
}
}(function(){
// Demo cycle: pulses through listening → thinking → done states so gallery
// + preview show meaningful motion. In production, replace with real Web
// Speech API integration:
// const recog = new webkitSpeechRecognition();
// recog.continuous = true; recog.onresult = ...
var root = document.querySelector('.pl-15');
if (!root) return;
var title = root.querySelector('#pl-15-title');
var status = root.querySelector('.pl-15__status');
if (!title || !status) return;
var states = [
{ title: 'Listening…', status: 'Say something now', delay: 3500 },
{ title: 'Thinking…', status: 'Processing your request', delay: 1500 },
{ title: 'Here you go', status: 'Weather · sunny, 72°F', delay: 2500 },
];
var i = 0;
function next(){
var s = states[i % states.length];
title.textContent = s.title;
status.textContent = s.status;
i++;
setTimeout(next, s.delay);
}
setTimeout(next, 3500);
})();(function(){
// Demo cycle: pulses through listening → thinking → done states so gallery
// + preview show meaningful motion. In production, replace with real Web
// Speech API integration:
// const recog = new webkitSpeechRecognition();
// recog.continuous = true; recog.onresult = ...
var root = document.querySelector('.pl-15');
if (!root) return;
var title = root.querySelector('#pl-15-title');
var status = root.querySelector('.pl-15__status');
if (!title || !status) return;
var states = [
{ title: 'Listening…', status: 'Say something now', delay: 3500 },
{ title: 'Thinking…', status: 'Processing your request', delay: 1500 },
{ title: 'Here you go', status: 'Weather · sunny, 72°F', delay: 2500 },
];
var i = 0;
function next(){
var s = states[i % states.length];
title.textContent = s.title;
status.textContent = s.status;
i++;
setTimeout(next, s.delay);
}
setTimeout(next, 3500);
})();Here's a working CSS Pulse Animation 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 Search Mic Pulse
Source: https://codefronts.com/motion/css-pulse-animation/voice-search-mic-pulse/
A voice-assistant modal showing an active listening state — microphone icon at the center with expanding concentric rings pulsing outward + a live waveform of vertical bars responding to "voice input." Deep purple gradient background (#4c1d95 to #7c3aed) with white mic icon and glassmorphic modal card. Google Assistant / Alexa / Siri modal aesthetic. The pulse conveys "I'm actively listening, keep speaking."
## HTML
```html
<div class="pl-15">
<div class="pl-15__modal" role="dialog" aria-modal="true" aria-labelledby="pl-15-title">
<button class="pl-15__close" aria-label="Cancel voice input">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="18" y1="6" x2="6" y2="18"/>
</svg>
</button>
<p class="pl-15__eyebrow">Northlight Assistant</p>
<h2 class="pl-15__title" id="pl-15-title">Listening…</h2>
<button class="pl-15__mic" aria-label="Stop listening">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</button>
<div class="pl-15__wave" aria-hidden="true">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<p class="pl-15__hint">Say something like <em>"What's the weather today?"</em></p>
<p class="pl-15__status" aria-live="polite">Listening now · release to send</p>
</div>
</div>
```
## CSS
```css
.pl-15 {
--brand-a: #4c1d95;
--brand-b: #7c3aed;
--brand-glow: rgba(167,139,250,.5);
--ink: #f5f3ff;
--sub: #c4b5fd;
--white: #ffffff;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Rounded','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: grid;
place-items: center;
padding: 32px;
background: radial-gradient(60% 100% at 20% 0%, rgba(167,139,250,.3) 0%, transparent 55%), radial-gradient(70% 100% at 100% 100%, rgba(124,58,237,.35) 0%, transparent 55%), linear-gradient(135deg,var(--brand-a) 0%,var(--brand-b) 100%);
color: var(--ink);
}
.pl-15 *,
.pl-15 *::before,
.pl-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pl-15 ::selection {
background: var(--white);
color: var(--brand-a);
}
.pl-15__modal {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: min(420px,100%);
padding: 36px 28px 28px;
border-radius: 20px;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.14);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 30px 80px -20px rgba(76,29,149,.6);
}
.pl-15__close {
position: absolute;
top: 14px;
right: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
background: rgba(255,255,255,.1);
color: var(--ink);
cursor: pointer;
transition: background .15s ease;
}
.pl-15__close:hover {
background: rgba(255,255,255,.2);
}
.pl-15__close:focus-visible {
outline: 2px solid var(--white);
outline-offset: 2px;
}
.pl-15__eyebrow {
font-size: .72rem;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--sub);
margin-bottom: 8px;
}
.pl-15__title {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -.02em;
line-height: 1.1;
margin-bottom: 32px;
}
/* Mic button with concentric outward rings */
.pl-15__mic {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: none;
border-radius: 50%;
background: var(--white);
color: var(--brand-a);
cursor: pointer;
box-shadow: 0 10px 30px -8px rgba(255,255,255,.4);
margin-bottom: 22px;
z-index: 1;
transition: transform .15s ease;
}
.pl-15__mic:hover {
transform: scale(1.03);
}
.pl-15__mic:focus-visible {
outline: 3px solid var(--white);
outline-offset: 6px;
}
.pl-15__mic svg {
position: relative;
z-index: 1;
}
.pl-15__mic::before,
.pl-15__mic::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--brand-glow);
pointer-events: none;
z-index: 0;
animation: pl-15-ring 1.8s ease-out infinite;
}
.pl-15__mic::after {
animation-delay: .9s;
}
@keyframes pl-15-ring {
0% {
transform: scale(1);
opacity: .55;
}
70% {
transform: scale(2.3);
opacity: 0;
}
100% {
transform: scale(2.4);
opacity: 0;
}
}
/* Waveform bars — 5 vertical bars with coprime durations */
.pl-15__wave {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 36px;
margin-bottom: 20px;
}
.pl-15__wave span {
display: inline-block;
width: 4px;
height: 100%;
border-radius: 2px;
background: var(--white);
transform-origin: center;
}
.pl-15__wave span:nth-child(1) {
animation: pl-15-bar .6s ease-in-out -.2s infinite;
}
.pl-15__wave span:nth-child(2) {
animation: pl-15-bar .9s ease-in-out -.5s infinite;
}
.pl-15__wave span:nth-child(3) {
animation: pl-15-bar .7s ease-in-out -.1s infinite;
}
.pl-15__wave span:nth-child(4) {
animation: pl-15-bar .8s ease-in-out -.4s infinite;
}
.pl-15__wave span:nth-child(5) {
animation: pl-15-bar .5s ease-in-out 0s infinite;
}
@keyframes pl-15-bar {
0%,
100% {
transform: scaleY(.3);
}
50% {
transform: scaleY(1);
}
}
.pl-15__hint {
font-size: .88rem;
color: var(--sub);
text-align: center;
line-height: 1.4;
margin-bottom: 14px;
}
.pl-15__hint em {
color: var(--white);
font-style: normal;
font-weight: 600;
}
.pl-15__status {
padding: 8px 14px;
border-radius: 8px;
background: rgba(0,0,0,.15);
color: var(--sub);
font-size: .72rem;
font-weight: 600;
letter-spacing: .04em;
}
@media (prefers-reduced-motion: reduce) {
.pl-15__mic::before,
.pl-15__mic::after,
.pl-15__wave span {
animation: none!important;
transform: none!important;
}
.pl-15__mic::before,
.pl-15__mic::after {
opacity: 0!important;
}
.pl-15__wave span {
transform: scaleY(.7)!important;
}
}
```
## JavaScript
```js
(function(){
// Demo cycle: pulses through listening → thinking → done states so gallery
// + preview show meaningful motion. In production, replace with real Web
// Speech API integration:
// const recog = new webkitSpeechRecognition();
// recog.continuous = true; recog.onresult = ...
var root = document.querySelector('.pl-15');
if (!root) return;
var title = root.querySelector('#pl-15-title');
var status = root.querySelector('.pl-15__status');
if (!title || !status) return;
var states = [
{ title: 'Listening…', status: 'Say something now', delay: 3500 },
{ title: 'Thinking…', status: 'Processing your request', delay: 1500 },
{ title: 'Here you go', status: 'Weather · sunny, 72°F', delay: 2500 },
];
var i = 0;
function next(){
var s = states[i % states.length];
title.textContent = s.title;
status.textContent = s.status;
i++;
setTimeout(next, s.delay);
}
setTimeout(next, 3500);
})();
```Here's a working CSS Pulse Animation 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 Search Mic Pulse
Source: https://codefronts.com/motion/css-pulse-animation/voice-search-mic-pulse/
A voice-assistant modal showing an active listening state — microphone icon at the center with expanding concentric rings pulsing outward + a live waveform of vertical bars responding to "voice input." Deep purple gradient background (#4c1d95 to #7c3aed) with white mic icon and glassmorphic modal card. Google Assistant / Alexa / Siri modal aesthetic. The pulse conveys "I'm actively listening, keep speaking."
## HTML
```html
<div class="pl-15">
<div class="pl-15__modal" role="dialog" aria-modal="true" aria-labelledby="pl-15-title">
<button class="pl-15__close" aria-label="Cancel voice input">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="18" y1="6" x2="6" y2="18"/>
</svg>
</button>
<p class="pl-15__eyebrow">Northlight Assistant</p>
<h2 class="pl-15__title" id="pl-15-title">Listening…</h2>
<button class="pl-15__mic" aria-label="Stop listening">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</button>
<div class="pl-15__wave" aria-hidden="true">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<p class="pl-15__hint">Say something like <em>"What's the weather today?"</em></p>
<p class="pl-15__status" aria-live="polite">Listening now · release to send</p>
</div>
</div>
```
## CSS
```css
.pl-15 {
--brand-a: #4c1d95;
--brand-b: #7c3aed;
--brand-glow: rgba(167,139,250,.5);
--ink: #f5f3ff;
--sub: #c4b5fd;
--white: #ffffff;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Rounded','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: grid;
place-items: center;
padding: 32px;
background: radial-gradient(60% 100% at 20% 0%, rgba(167,139,250,.3) 0%, transparent 55%), radial-gradient(70% 100% at 100% 100%, rgba(124,58,237,.35) 0%, transparent 55%), linear-gradient(135deg,var(--brand-a) 0%,var(--brand-b) 100%);
color: var(--ink);
}
.pl-15 *,
.pl-15 *::before,
.pl-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pl-15 ::selection {
background: var(--white);
color: var(--brand-a);
}
.pl-15__modal {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: min(420px,100%);
padding: 36px 28px 28px;
border-radius: 20px;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.14);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 30px 80px -20px rgba(76,29,149,.6);
}
.pl-15__close {
position: absolute;
top: 14px;
right: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border: none;
border-radius: 50%;
background: rgba(255,255,255,.1);
color: var(--ink);
cursor: pointer;
transition: background .15s ease;
}
.pl-15__close:hover {
background: rgba(255,255,255,.2);
}
.pl-15__close:focus-visible {
outline: 2px solid var(--white);
outline-offset: 2px;
}
.pl-15__eyebrow {
font-size: .72rem;
font-weight: 700;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--sub);
margin-bottom: 8px;
}
.pl-15__title {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -.02em;
line-height: 1.1;
margin-bottom: 32px;
}
/* Mic button with concentric outward rings */
.pl-15__mic {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: none;
border-radius: 50%;
background: var(--white);
color: var(--brand-a);
cursor: pointer;
box-shadow: 0 10px 30px -8px rgba(255,255,255,.4);
margin-bottom: 22px;
z-index: 1;
transition: transform .15s ease;
}
.pl-15__mic:hover {
transform: scale(1.03);
}
.pl-15__mic:focus-visible {
outline: 3px solid var(--white);
outline-offset: 6px;
}
.pl-15__mic svg {
position: relative;
z-index: 1;
}
.pl-15__mic::before,
.pl-15__mic::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--brand-glow);
pointer-events: none;
z-index: 0;
animation: pl-15-ring 1.8s ease-out infinite;
}
.pl-15__mic::after {
animation-delay: .9s;
}
@keyframes pl-15-ring {
0% {
transform: scale(1);
opacity: .55;
}
70% {
transform: scale(2.3);
opacity: 0;
}
100% {
transform: scale(2.4);
opacity: 0;
}
}
/* Waveform bars — 5 vertical bars with coprime durations */
.pl-15__wave {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 36px;
margin-bottom: 20px;
}
.pl-15__wave span {
display: inline-block;
width: 4px;
height: 100%;
border-radius: 2px;
background: var(--white);
transform-origin: center;
}
.pl-15__wave span:nth-child(1) {
animation: pl-15-bar .6s ease-in-out -.2s infinite;
}
.pl-15__wave span:nth-child(2) {
animation: pl-15-bar .9s ease-in-out -.5s infinite;
}
.pl-15__wave span:nth-child(3) {
animation: pl-15-bar .7s ease-in-out -.1s infinite;
}
.pl-15__wave span:nth-child(4) {
animation: pl-15-bar .8s ease-in-out -.4s infinite;
}
.pl-15__wave span:nth-child(5) {
animation: pl-15-bar .5s ease-in-out 0s infinite;
}
@keyframes pl-15-bar {
0%,
100% {
transform: scaleY(.3);
}
50% {
transform: scaleY(1);
}
}
.pl-15__hint {
font-size: .88rem;
color: var(--sub);
text-align: center;
line-height: 1.4;
margin-bottom: 14px;
}
.pl-15__hint em {
color: var(--white);
font-style: normal;
font-weight: 600;
}
.pl-15__status {
padding: 8px 14px;
border-radius: 8px;
background: rgba(0,0,0,.15);
color: var(--sub);
font-size: .72rem;
font-weight: 600;
letter-spacing: .04em;
}
@media (prefers-reduced-motion: reduce) {
.pl-15__mic::before,
.pl-15__mic::after,
.pl-15__wave span {
animation: none!important;
transform: none!important;
}
.pl-15__mic::before,
.pl-15__mic::after {
opacity: 0!important;
}
.pl-15__wave span {
transform: scaleY(.7)!important;
}
}
```
## JavaScript
```js
(function(){
// Demo cycle: pulses through listening → thinking → done states so gallery
// + preview show meaningful motion. In production, replace with real Web
// Speech API integration:
// const recog = new webkitSpeechRecognition();
// recog.continuous = true; recog.onresult = ...
var root = document.querySelector('.pl-15');
if (!root) return;
var title = root.querySelector('#pl-15-title');
var status = root.querySelector('.pl-15__status');
if (!title || !status) return;
var states = [
{ title: 'Listening…', status: 'Say something now', delay: 3500 },
{ title: 'Thinking…', status: 'Processing your request', delay: 1500 },
{ title: 'Here you go', status: 'Weather · sunny, 72°F', delay: 2500 },
];
var i = 0;
function next(){
var s = states[i % states.length];
title.textContent = s.title;
status.textContent = s.status;
i++;
setTimeout(next, s.delay);
}
setTimeout(next, 3500);
})();
```How this works
The modal shows a large circular microphone button (80×80px) at the center with an inline SVG mic icon. Two ::before and ::after pseudo-elements sit around the button (inset: 0), each animated via @keyframes pl-15-ring — transform: scale(1) → scale(2.4) with opacity: .5 → 0 over 1.8 seconds. Staggered delays (0s and 0.9s) create a continuous concentric outward wave — the classic "listening now" signal used by every voice UI.
Below the mic, a row of 5 vertical waveform bars responds to "voice input" — each bar animates transform: scaleY(0.3) → scaleY(1) → scaleY(0.3) with different animation-duration (0.6s, 0.9s, 0.7s, 0.8s, 0.5s) so the bars move independently, mimicking real audio-level responsiveness. Because the durations are all coprime to each other, the pattern never visually repeats within a short window — feels genuinely reactive rather than looped.
Deep purple gradient background (linear-gradient(135deg, #4c1d95, #7c3aed)) fills the modal surface, matching the assistant-modal convention. Glassmorphic card with backdrop-filter: blur(20px) and translucent white overlay gives the mystical / conversational feel. Status text below ("Listening…") uses SF Pro Rounded-style typography for the friendly conversational feel. A close button in the top-right corner allows the user to cancel the listening state.
JS auto-cycles the listening state on demo load — starts with mic active + waveform bars pulsing, then simulates a wake / listen / respond cycle by toggling classes. This gives the gallery + preview capture something meaningful to show. In production, replace the timer-based simulation with real Web Speech API integration (webkitSpeechRecognition).
Make it yours
- Change the number of waveform bars from 5 to 3 or 7 — 3 feels minimalist ("OK Google" simplicity), 7 feels rich ("Alexa" full assistant), 5 is the sweet spot for most contexts.
- For real Web Speech API integration, replace the CSS-only waveform with a
<canvas>element that responds to actual microphone audio levels viaAudioContext.createAnalyser(). Same visual pattern, real-time responsiveness. - Change the state colors — purple for listening, amber for "thinking / processing," green for "understood, responding." A three-state visual machine matches user expectations from Alexa / Google Assistant.
- For accessibility, add a status text region with
aria-live="polite"announcing state changes ("Listening," "Processing," "How can I help?") — screen-reader users can't see the pulse. - For quiet contexts (library, meeting rooms), replace the microphone icon with a text-input option — voice UI must degrade gracefully when audio input isn't appropriate.
Gotchas — read before shipping
- Real Web Speech API access REQUIRES explicit user permission via the browser prompt. Do NOT auto-request microphone access on page load — users will deny and remember the permission was denied. Only prompt AFTER an explicit user gesture (click / tap).
- For iOS Safari, microphone access requires a user gesture within the SAME event loop — you can't chain
awaitthen request permission, the browser blocks it. Structure your code to request permission synchronously after the click. - Voice recognition results are FALLIBLE — always provide a text-input fallback and a way to manually correct the transcribed text before submitting. Users need to trust the system won't act on misrecognitions.
- For accessibility, the modal MUST be keyboard-accessible even for a voice-first UI. Tab focus should cycle through Cancel / Retry / Manual input. Escape should always close the modal (matches WCAG dialog pattern).
- For screen-reader users, the visual pulse + waveform conveys "listening now," but they need
aria-live="polite"text announcements: "Listening now," "Say something," "I heard: [transcript]". - The Web Speech API's browser support is inconsistent (webkit prefix in Safari, none in Firefox). For cross-browser production, use a service like Google Cloud Speech or OpenAI Whisper via a backend API.
- For prefers-reduced-motion: keep the mic icon VISIBLE + status text ("Listening…"), disable the concentric rings + waveform pulse. Information is conveyed by text; motion is decorative here.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 76+ | 9+ | 103+ | 76+ |
Floor set by backdrop-filter as this demo is written. The core technique itself goes back further — Chrome 45+.
transform + opacity + @keyframes + backdrop-filter. Universal, GPU-accelerated.