05 / 16
All 16 in 16 CSS Bounce Animations
- 01Scroll Down Arrow Bounce
- 02Bounce-In Entrance
- 03Success Checkmark Pop
- 04Toast Notification Bounce
- 05Bouncing Loading Dots
- 06Button Hover Bounce
- 07Card Hover Lift Bounce
- 08Add to Cart Cart Icon Bounce
- 09Notification Badge Pop
- 10Toggle Switch Elastic Bounce
- 11Physics Ball Squash Stretch
- 12Floating Hovering Element
- 13Bouncing Letters Wave
- 14Form Error Alert Bounce
- 15DVD Screensaver Viewport Bounce
- 16Tailwind animate-bounce Customization
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.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
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.