16 CSS Bounce Animations05 / 16
Bouncing Loading Dots
The canonical chat-typing / loading indicator — 3 dots inside a rounded speech bubble bouncing sequentially, signaling "loading" or "the other person is typing." iMessage blue bubble (#007aff) on a soft-gray chat surface. The pattern users search for when building chat interfaces, AI streaming responses, or any loading state that needs to feel calm rather than urgent.
Published
The code
<div class="bn-05">
<p class="bn-05__label">Alex is typing…</p>
<div class="bn-05__bubble" aria-label="Loading indicator">
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
</div>
</div><div class="bn-05">
<p class="bn-05__label">Alex is typing…</p>
<div class="bn-05__bubble" aria-label="Loading indicator">
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
</div>
</div>.bn-05 {
--page-bg: #f2f2f7;
--bubble: #e5e5ea;
--ink: #1c1c1e;
--sub: #8e8e93;
--dot: #8e8e93;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 32px;
background: var(--page-bg);
color: var(--ink);
}
.bn-05 *,
.bn-05 *::before,
.bn-05 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-05 ::selection {
background: #007aff;
color: #fff;
}
.bn-05__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-05__bubble {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 22px 28px;
border-radius: 26px;
background: var(--bubble);
border-bottom-left-radius: 10px;
box-shadow: 0 4px 12px -4px rgba(28,28,30,.08);
}
.bn-05__dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--dot);
animation: bn-05-dot 1.4s ease-in-out infinite;
}
.bn-05__dot:nth-child(2) {
animation-delay: .16s;
}
.bn-05__dot:nth-child(3) {
animation-delay: .32s;
}
@keyframes bn-05-dot {
0%,
60%,
100% {
transform: translateY(0);
opacity: .4;
}
30% {
transform: translateY(-8px);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.bn-05__dot {
animation: none!important;
transform: none!important;
opacity: .7!important;
}
}.bn-05 {
--page-bg: #f2f2f7;
--bubble: #e5e5ea;
--ink: #1c1c1e;
--sub: #8e8e93;
--dot: #8e8e93;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 32px;
background: var(--page-bg);
color: var(--ink);
}
.bn-05 *,
.bn-05 *::before,
.bn-05 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-05 ::selection {
background: #007aff;
color: #fff;
}
.bn-05__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-05__bubble {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 22px 28px;
border-radius: 26px;
background: var(--bubble);
border-bottom-left-radius: 10px;
box-shadow: 0 4px 12px -4px rgba(28,28,30,.08);
}
.bn-05__dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--dot);
animation: bn-05-dot 1.4s ease-in-out infinite;
}
.bn-05__dot:nth-child(2) {
animation-delay: .16s;
}
.bn-05__dot:nth-child(3) {
animation-delay: .32s;
}
@keyframes bn-05-dot {
0%,
60%,
100% {
transform: translateY(0);
opacity: .4;
}
30% {
transform: translateY(-8px);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.bn-05__dot {
animation: none!important;
transform: none!important;
opacity: .7!important;
}
}Here's a working CSS Bounce 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: Bouncing Loading Dots
Source: https://codefronts.com/motion/css-bounce-animation/bouncing-loading-dots/
The canonical chat-typing / loading indicator — 3 dots inside a rounded speech bubble bouncing sequentially, signaling "loading" or "the other person is typing." iMessage blue bubble (#007aff) on a soft-gray chat surface. The pattern users search for when building chat interfaces, AI streaming responses, or any loading state that needs to feel calm rather than urgent.
## HTML
```html
<div class="bn-05">
<p class="bn-05__label">Alex is typing…</p>
<div class="bn-05__bubble" aria-label="Loading indicator">
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
</div>
</div>
```
## CSS
```css
.bn-05 {
--page-bg: #f2f2f7;
--bubble: #e5e5ea;
--ink: #1c1c1e;
--sub: #8e8e93;
--dot: #8e8e93;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 32px;
background: var(--page-bg);
color: var(--ink);
}
.bn-05 *,
.bn-05 *::before,
.bn-05 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-05 ::selection {
background: #007aff;
color: #fff;
}
.bn-05__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-05__bubble {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 22px 28px;
border-radius: 26px;
background: var(--bubble);
border-bottom-left-radius: 10px;
box-shadow: 0 4px 12px -4px rgba(28,28,30,.08);
}
.bn-05__dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--dot);
animation: bn-05-dot 1.4s ease-in-out infinite;
}
.bn-05__dot:nth-child(2) {
animation-delay: .16s;
}
.bn-05__dot:nth-child(3) {
animation-delay: .32s;
}
@keyframes bn-05-dot {
0%,
60%,
100% {
transform: translateY(0);
opacity: .4;
}
30% {
transform: translateY(-8px);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.bn-05__dot {
animation: none!important;
transform: none!important;
opacity: .7!important;
}
}
```Here's a working CSS Bounce 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: Bouncing Loading Dots
Source: https://codefronts.com/motion/css-bounce-animation/bouncing-loading-dots/
The canonical chat-typing / loading indicator — 3 dots inside a rounded speech bubble bouncing sequentially, signaling "loading" or "the other person is typing." iMessage blue bubble (#007aff) on a soft-gray chat surface. The pattern users search for when building chat interfaces, AI streaming responses, or any loading state that needs to feel calm rather than urgent.
## HTML
```html
<div class="bn-05">
<p class="bn-05__label">Alex is typing…</p>
<div class="bn-05__bubble" aria-label="Loading indicator">
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
<span class="bn-05__dot" aria-hidden="true"></span>
</div>
</div>
```
## CSS
```css
.bn-05 {
--page-bg: #f2f2f7;
--bubble: #e5e5ea;
--ink: #1c1c1e;
--sub: #8e8e93;
--dot: #8e8e93;
font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text','Inter','Segoe UI',sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 32px;
background: var(--page-bg);
color: var(--ink);
}
.bn-05 *,
.bn-05 *::before,
.bn-05 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.bn-05 ::selection {
background: #007aff;
color: #fff;
}
.bn-05__label {
font-size: .9rem;
font-weight: 600;
color: var(--sub);
letter-spacing: .02em;
}
.bn-05__bubble {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 22px 28px;
border-radius: 26px;
background: var(--bubble);
border-bottom-left-radius: 10px;
box-shadow: 0 4px 12px -4px rgba(28,28,30,.08);
}
.bn-05__dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--dot);
animation: bn-05-dot 1.4s ease-in-out infinite;
}
.bn-05__dot:nth-child(2) {
animation-delay: .16s;
}
.bn-05__dot:nth-child(3) {
animation-delay: .32s;
}
@keyframes bn-05-dot {
0%,
60%,
100% {
transform: translateY(0);
opacity: .4;
}
30% {
transform: translateY(-8px);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.bn-05__dot {
animation: none!important;
transform: none!important;
opacity: .7!important;
}
}
```How this works
Three <span> elements inside a speech bubble share the same 10px circular dot shape. Each dot has the same @keyframes bn-05-dot animation cycling transform: translateY(0) → translateY(-8px) → translateY(0) over 1.4 seconds. The three dots use staggered animation-delay: dot 1 at 0s, dot 2 at 0.16s, dot 3 at 0.32s — creating the classic wave.
Two design details separate this from beginner tutorials: (1) the stagger delay (160ms) is exactly half the individual dot's up-phase — this creates the wave-motion illusion (dot 2 rises just as dot 1 begins descending). (2) The keyframe uses opacity: .4 → 1 → .4 alongside translateY — dots not currently "active" fade to 40% opacity, making the active dot pop. This dual-property animation is what iMessage's indicator does; static-opacity 3-dot loaders feel flat.
The demo centers the typing bubble as the hero at ~180px wide with a small label above signaling context. No chat thread wrapping; the bubble IS the demo.
Make it yours
- Change the vertical bounce height by editing
-8px— 4px reads as subtle (Slack pattern), 12px reads as energetic (Messenger). Match your app's motion vocabulary. - Adjust the wave speed by editing
animation-duration: 1.4s. Faster (1s) reads as urgent, slower (1.8s) as calm. - Add a 4th dot for the Messenger pattern — same animation with
animation-delay: .48s. More than 4 dots reads as morse code, not typing. - For AI streaming responses (ChatGPT / Claude pattern), use lighter background (avoiding the "someone is typing" mental model). Not a bubble — just dots on transparent background.
- For Tailwind:
animate-[bn05_1.4s_ease-in-out_infinite]+ register keyframes intailwind.config.js. See FAQ #4.
Gotchas — read before shipping
- The stagger delay MUST be less than the animation duration divided by number of dots. For 3 dots at 1.4s cycle, stagger of 160ms works. Larger stagger and dots don't overlap — they read as 3 separate blinks, not a wave.
- For chat contexts, the typing indicator should hide when the other person stops typing. If your indicator persists after they stopped, users get confused. Show for max 5 seconds without additional signal, then hide.
- Do NOT use
opacity: 0at rest — .4 opacity keeps dots visible so users see the shape. Full 0-opacity dots vanish and the bubble looks empty half the cycle. - For accessibility, use
aria-labelon the bubble container announcing what's happening ("Loading", "Alex is typing") — not on individual dots. Screen readers get meaningful context, not "dot, dot, dot." - AI streaming responses are DIFFERENT from chat typing — AI uses a caret / cursor blink, not typing dots. Typing dots imply "a person is composing." AI streaming implies "text is being generated." Match the mental model.
- For dark-mode chat, swap the iOS system gray for the iOS dark chat surface (
#1c1c1e) + the iMessage dark-mode blue (#0a84ff). Never mechanically invert. - For
prefers-reduced-motion, show static dots at consistent opacity — screen readers announce viaaria-label. Information preserved, vestibular motion disabled.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 45+ | 10+ | 40+ | 45+ |
transform + opacity + @keyframes. Universal, GPU-accelerated.