Skip to content
· 16 min read · 0 views

CSS Loading Animations Part 2 — 14 More Spinners & Indicators

14 more CSS loading animations with full code: wave bars, pulse ring, DNA helix, cogs, breathe, typing indicator, hamburger morph, ellipsis, clock, maze, infinity loop, jelly, gradient spinner, and fade dots.

// table of contents (29 sections)

Try them live — All 14 loading animations are available on CSSKit where you can customize parameters and copy the code. Source code on GitHub. See the full series: Loading Part 1, Hover Part 1, Hover Part 2.


In Loading Part 1 we built 10 loading animations — dots, rings, spinners, and skeletons. This post completes the loading category with 14 more effects, from DNA helix rotations to hamburger menu morphs. Together, that’s all 24 loading animations in CSSKit.

55. Wave Bars

Sine wave bar animation with cascading height — smooth and organic.

How It Works

Seven bars animate their height between 12px and 40px with staggered animation-delay (0s to 0.6s in 0.1s increments). The ease-in-out timing creates the smooth sine wave shape as bars rise and fall. The align-items: center centers bars vertically so they grow both up and down symmetrically.

.loading-wave {
  --lw-color: #3b82f6;
  --lw-speed: 1.2s;
  display: flex;
  align-items: center;
  gap: 3px;
  height: 48px;
}

.loading-wave-bar {
  width: 5px;
  height: 12px;
  background: var(--lw-color);
  border-radius: 3px;
  animation: lw-wave var(--lw-speed) ease-in-out infinite;
}

.loading-wave-bar:nth-child(1) { animation-delay: 0s; }
.loading-wave-bar:nth-child(2) { animation-delay: 0.1s; }
.loading-wave-bar:nth-child(3) { animation-delay: 0.2s; }
.loading-wave-bar:nth-child(4) { animation-delay: 0.3s; }
.loading-wave-bar:nth-child(5) { animation-delay: 0.4s; }
.loading-wave-bar:nth-child(6) { animation-delay: 0.5s; }
.loading-wave-bar:nth-child(7) { animation-delay: 0.6s; }

@keyframes lw-wave {
  0%, 100% { height: 12px; opacity: 0.4; }
  50% { height: 40px; opacity: 1; }
}
<div class="loading-wave"><div class="loading-wave-bar"></div><div class="loading-wave-bar"></div><div class="loading-wave-bar"></div><div class="loading-wave-bar"></div><div class="loading-wave-bar"></div><div class="loading-wave-bar"></div><div class="loading-wave-bar"></div></div>

56. Pulse Ring

Expanding rings pulse outward from a center dot — radio wave style.

How It Works

Two ::before and ::after pseudo-elements create the expanding ring illusion. Both share the same lpr-pulse animation but ::after has a calc(var(--lpr-speed) / 3) delay, creating staggered waves. Each ring starts at scale(0.3) with full opacity and expands to scale(1.2) while fading to opacity: 0. A <span> creates the solid center dot.

.loading-pulse-ring {
  --lpr-color: #3b82f6;
  --lpr-size: 40px;
  --lpr-speed: 1.5s;
  width: var(--lpr-size);
  height: var(--lpr-size);
  position: relative;
}

.loading-pulse-ring::before,
.loading-pulse-ring::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 2px solid var(--lpr-color);
  border-radius: 50%;
  animation: lpr-pulse var(--lpr-speed) ease-out infinite;
}

.loading-pulse-ring::after {
  animation-delay: calc(var(--lpr-speed) / 3);
}

.loading-pulse-ring span {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 8px;
  height: 8px;
  background: var(--lpr-color);
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

@keyframes lpr-pulse {
  0% { transform: scale(0.3); opacity: 1; }
  100% { transform: scale(1.2); opacity: 0; }
}
<div class="loading-pulse-ring"><span></span></div>

Tip: Add a third ring with animation-delay: calc(var(--lpr-speed) / 1.5) using an additional element for a triple-pulse effect.


57. DNA Helix

Double helix rotating strands — scientific and biotech themed.

How It Works

Eight dots share the same animation but with staggered negative delays (-0.25s increments). The animation moves each dot through translateY positions (-12px → -4px → 4px → 12px) while alternating scale and opacity. The negative delays mean each dot starts at a different phase, creating the helix illusion — two interleaving sine waves.

.loading-dna {
  --ld-color: #8b5cf6;
  --ld-speed: 2s;
  width: 40px;
  height: 50px;
  display: flex;
  align-items: center;
  gap: 3px;
}

.loading-dna .ld-strand {
  width: 6px;
  height: 6px;
  background: var(--ld-color);
  border-radius: 50%;
  animation: ld-helix var(--ld-speed) ease-in-out infinite;
}

.loading-dna .ld-strand:nth-child(1) { animation-delay: 0s; }
.loading-dna .ld-strand:nth-child(2) { animation-delay: -0.25s; }
.loading-dna .ld-strand:nth-child(3) { animation-delay: -0.5s; }
.loading-dna .ld-strand:nth-child(4) { animation-delay: -0.75s; }
.loading-dna .ld-strand:nth-child(5) { animation-delay: -1s; }
.loading-dna .ld-strand:nth-child(6) { animation-delay: -1.25s; }
.loading-dna .ld-strand:nth-child(7) { animation-delay: -1.5s; }
.loading-dna .ld-strand:nth-child(8) { animation-delay: -1.75s; }

@keyframes ld-helix {
  0%, 100% { transform: translateY(-12px) scale(0.6); opacity: 0.4; }
  25% { transform: translateY(-4px) scale(1); opacity: 1; }
  50% { transform: translateY(4px) scale(0.6); opacity: 0.4; }
  75% { transform: translateY(12px) scale(1); opacity: 1; }
}
<div class="loading-dna"><div class="ld-strand"></div><div class="ld-strand"></div><div class="ld-strand"></div><div class="ld-strand"></div><div class="ld-strand"></div><div class="ld-strand"></div><div class="ld-strand"></div><div class="ld-strand"></div></div>

Tip: Use two colors (even dots one color, odd dots another) for a true double-helix with visible strands.


58. Cogs

Two interlocking gears spinning in opposite directions — mechanical and industrial.

How It Works

Two circles with border-left-color and border-right-color set to transparent, creating a gear-like appearance with two open sides. The larger cog (30px) uses rotate(360deg) while the smaller one (24px) uses rotate(-360deg) — opposite directions like real meshing gears. The positioning offsets them so they appear interlocked.

.loading-cogs {
  --lc-color: #64748b;
  --lc-speed: 3s;
  position: relative;
  width: 60px;
  height: 50px;
}

.loading-cogs .lc-cog {
  position: absolute;
  width: 30px;
  height: 30px;
  border: 3px solid var(--lc-color);
  border-radius: 50%;
  border-left-color: transparent;
  border-right-color: transparent;
}

.loading-cogs .lc-cog:first-child {
  top: 2px;
  left: 2px;
  animation: lc-spin var(--lc-speed) linear infinite;
}

.loading-cogs .lc-cog:last-child {
  top: 16px;
  left: 24px;
  width: 24px;
  height: 24px;
  animation: lc-spin-reverse var(--lc-speed) linear infinite;
}

@keyframes lc-spin { to { transform: rotate(360deg); } }
@keyframes lc-spin-reverse { to { transform: rotate(-360deg); } }
<div class="loading-cogs"><div class="lc-cog"></div><div class="lc-cog"></div></div>

Tip: Add more cogs in a chain by positioning them along a diagonal and alternating rotation directions.


59. Breathe

Element breathes in and out — calming, zen-style loading animation.

How It Works

The simplest animation in this post — a single circle scales between scale(0.6) with opacity: 0.4 and scale(1) with opacity: 1. The ease-in-out timing and slow default speed (3s) create the meditative breathing rhythm. It’s subtle and calming — perfect for wellness, health, or meditation apps.

.loading-breathe {
  --lb-color: #10b981;
  --lb-size: 40px;
  --lb-speed: 3s;
  width: var(--lb-size);
  height: var(--lb-size);
  background: var(--lb-color);
  border-radius: 50%;
  animation: lb-breathe var(--lb-speed) ease-in-out infinite;
}

@keyframes lb-breathe {
  0%, 100% { transform: scale(0.6); opacity: 0.4; }
  50% { transform: scale(1); opacity: 1; }
}
<div class="loading-breathe"></div>

Tip: Use box-shadow: 0 0 20px var(--lb-color) for a soft glow that pulses with the breathing motion.


60. Typing Indicator

Three dots with a chat-bubble typing animation — like messaging apps.

How It Works

Three dots inside a rounded bubble container (background: #f1f5f9; border-radius: 18px). Each dot bounces up by 8px at the 30% mark with staggered delays (0s, 0.2s, 0.4s). The 60% hold at translateY(0) keeps each dot grounded while the others bounce, creating the sequential typing feel. The container padding and rounded corners create the chat bubble look.

.loading-typing {
  --lt-color: #94a3b8;
  --lt-speed: 1.4s;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 12px 18px;
  background: #f1f5f9;
  border-radius: 18px;
}

.loading-typing span {
  width: 8px;
  height: 8px;
  background: var(--lt-color);
  border-radius: 50%;
  animation: lt-dot var(--lt-speed) ease-in-out infinite;
}

.loading-typing span:nth-child(2) { animation-delay: 0.2s; }
.loading-typing span:nth-child(3) { animation-delay: 0.4s; }

@keyframes lt-dot {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
  30% { transform: translateY(-8px); opacity: 1; }
}
<div class="loading-typing"><span></span><span></span><span></span></div>

Tip: Add a left-aligned tail with border-bottom-left-radius: 4px for a received-message bubble style.


61. Hamburger

CSS hamburger menu morphs into an X and back — smooth line transitions.

How It Works

Three lines in a flex column. The top line animates translateY(9.5px) rotate(45deg) — moving down to the center and rotating to form the top half of the X. The bottom line does the inverse: translateY(-9.5px) rotate(-45deg). The middle line fades to opacity: 0 during the X state. All three animate with the same duration but at different 40%/90% hold points to create a pause in each state.

.loading-hamburger {
  --lh-color: #334155;
  --lh-speed: 2s;
  width: 30px;
  height: 22px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.loading-hamburger span {
  display: block;
  width: 100%;
  height: 3px;
  background: var(--lh-color);
  border-radius: 2px;
}

.loading-hamburger span:nth-child(1) { animation: lh-top var(--lh-speed) ease-in-out infinite; }
.loading-hamburger span:nth-child(2) { animation: lh-mid var(--lh-speed) ease-in-out infinite; }
.loading-hamburger span:nth-child(3) { animation: lh-bot var(--lh-speed) ease-in-out infinite; }

@keyframes lh-top {
  0%, 40% { transform: translateY(0); }
  50%, 90% { transform: translateY(9.5px) rotate(45deg); }
  100% { transform: translateY(0); }
}

@keyframes lh-mid {
  0%, 40% { opacity: 1; }
  50%, 90% { opacity: 0; }
  100% { opacity: 1; }
}

@keyframes lh-bot {
  0%, 40% { transform: translateY(0); }
  50%, 90% { transform: translateY(-9.5px) rotate(-45deg); }
  100% { transform: translateY(0); }
}
<div class="loading-hamburger"><span></span><span></span><span></span></div>

Tip: Use this as a real menu toggle by removing the infinite loop and toggling a class with JavaScript on click.


62. Ellipsis

Dots appear one by one — simple, clean loading for text interfaces.

How It Works

Three period characters start at opacity: 0. Each fades in at 30% and stays visible until 60%, then fades out. With staggered delays (0s, 0.3s, 0.6s), the dots appear sequentially: ., .., ..., then reset. The large font size (1.8rem) makes the dots clearly visible.

.loading-ellipsis {
  --le-color: #64748b;
  --le-speed: 1.5s;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 1.8rem;
  color: var(--le-color);
}

.loading-ellipsis span {
  opacity: 0;
  animation: le-dot var(--le-speed) ease-in-out infinite;
}

.loading-ellipsis span:nth-child(1) { animation-delay: 0s; }
.loading-ellipsis span:nth-child(2) { animation-delay: 0.3s; }
.loading-ellipsis span:nth-child(3) { animation-delay: 0.6s; }

@keyframes le-dot {
  0%, 100% { opacity: 0; }
  30%, 60% { opacity: 1; }
}
<div class="loading-ellipsis"><span>.</span><span>.</span><span>.</span></div>

Tip: Prefix with “Loading” text: <span>Loading</span> before the dots for a complete loading message.


63. Clock

Animated clock with spinning hands — time-based loading indicator.

How It Works

A circular border creates the clock face. ::before is the hour hand (14px tall, slower rotation) and ::after is the minute hand (20px tall, 6x faster). Both are positioned at bottom: 50%; left: 50% so they pivot from the center, with transform-origin: bottom center making them rotate around their base. The translateX(-50%) centers them horizontally.

.loading-clock {
  --lcl-color: #334155;
  --lcl-accent: #3b82f6;
  --lcl-speed: 2s;
  width: 50px;
  height: 50px;
  border: 3px solid var(--lcl-color);
  border-radius: 50%;
  position: relative;
}

.loading-clock::before,
.loading-clock::after {
  content: '';
  position: absolute;
  bottom: 50%;
  left: 50%;
  width: 2px;
  background: var(--lcl-accent);
  transform-origin: bottom center;
  border-radius: 1px;
}

.loading-clock::before {
  height: 14px;
  animation: lcl-hour var(--lcl-speed) linear infinite;
}

.loading-clock::after {
  height: 20px;
  width: 1.5px;
  animation: lcl-minute calc(var(--lcl-speed) / 6) linear infinite;
}

@keyframes lcl-hour { to { transform: translateX(-50%) rotate(360deg); } }
@keyframes lcl-minute { to { transform: translateX(-50%) rotate(360deg); } }
<div class="loading-clock"></div>

Tip: Add small tick marks with additional box-shadow values on the border for a more realistic clock face.


64. Maze

A dot navigating through a grid maze path — animated routing indicator.

How It Works

A 5x5 CSS Grid creates the maze background. Specific cells are set to background: transparent to form corridors. An absolutely-positioned dot traces a path through the corridors using translate() keyframes that move in right-angle steps (no diagonals). The ease-in-out timing makes the dot slow down at corners.

.loading-maze {
  --lm-color: #3b82f6;
  --lm-wall: #e2e8f0;
  --lm-speed: 3s;
  width: 50px;
  height: 50px;
  display: grid;
  grid-template-columns: repeat(5, 10px);
  grid-template-rows: repeat(5, 10px);
  gap: 0;
}

.loading-maze span {
  width: 10px;
  height: 10px;
  background: var(--lm-wall);
  border-radius: 2px;
}

.loading-maze span:nth-child(2),
.loading-maze span:nth-child(8),
.loading-maze span:nth-child(12),
.loading-maze span:nth-child(18),
.loading-maze span:nth-child(22) {
  background: transparent;
}

.loading-maze .lm-dot {
  position: absolute;
  width: 8px;
  height: 8px;
  background: var(--lm-color);
  border-radius: 50%;
  animation: lm-path var(--lm-speed) ease-in-out infinite;
}

@keyframes lm-path {
  0% { transform: translate(1px, 1px); }
  20% { transform: translate(21px, 1px); }
  40% { transform: translate(21px, 21px); }
  60% { transform: translate(31px, 21px); }
  80% { transform: translate(31px, 41px); }
  100% { transform: translate(41px, 41px); }
}
<div class="loading-maze" style="position:relative"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><div class="lm-dot"></div></div>

Tip: Customize the maze by changing which cells have background: transparent and adjusting the dot’s path keyframes accordingly.


65. Infinity

Figure-8 infinity loop — smooth continuous orbital motion using CSS offset-path.

How It Works

This uses the CSS Motion Path API (offset-path). A cubic bezier path() defines a figure-8 shape. The dot animates offset-distance from 0% to 100%, automatically following the path. The path uses two mirrored cubic curves that cross at the center, creating the infinity symbol. This is a modern CSS feature with excellent browser support.

.loading-infinity {
  --li-color: #8b5cf6;
  --li-speed: 2s;
  width: 50px;
  height: 30px;
  position: relative;
}

.loading-infinity span {
  position: absolute;
  width: 10px;
  height: 10px;
  background: var(--li-color);
  border-radius: 50%;
  animation: li-loop var(--li-speed) linear infinite;
  offset-path: path('M 5 15 C 5 0 20 0 25 15 C 30 30 45 30 45 15 C 45 0 30 0 25 15 C 20 30 5 30 5 15');
}

@keyframes li-loop {
  0% { offset-distance: 0%; }
  100% { offset-distance: 100%; }
}
<div class="loading-infinity"><span></span></div>

Tip: Add a trail effect with multiple dots at different animation-delay values following the same path.


66. Jelly

Squishy jelly blob bouncing — fun and elastic with deformation.

How It Works

Five keyframe stages create the jelly physics. At 20% it squashes on impact (scaleX(1.2) scaleY(0.8) with downward translate). At 40% it stretches while bouncing up (scaleX(0.85) scaleY(1.15) with upward translate). The remaining stages create the settling wobble as it returns to rest. The alternating scaleX/scaleY creates realistic soft-body deformation.

.loading-jelly {
  --lj-color: #f43f5e;
  --lj-speed: 1s;
  width: 40px;
  height: 40px;
  background: var(--lj-color);
  border-radius: 50%;
  animation: lj-squish var(--lj-speed) ease-in-out infinite;
}

@keyframes lj-squish {
  0%, 100% { transform: scaleX(1) scaleY(1) translateY(0); }
  20% { transform: scaleX(1.2) scaleY(0.8) translateY(10px); }
  40% { transform: scaleX(0.85) scaleY(1.15) translateY(-20px); }
  60% { transform: scaleX(1.05) scaleY(0.95) translateY(0); }
  80% { transform: scaleX(0.95) scaleY(1.05) translateY(-5px); }
}
<div class="loading-jelly"></div>

Tip: Use box-shadow: 0 0 20px rgba(244, 63, 94, 0.3) for a glowing jelly effect.


67. Gradient Spinner

Spinner with a gradient trail using conic-gradient — modern and smooth.

How It Works

A conic-gradient goes from transparent (0% to 70%) to the accent color (100%), creating a pie-slice trail. The whole element rotates. A ::after pseudo-element covers the center with white (inset: 4px), cutting out the donut hole. The result is a smooth gradient arc that appears to chase itself.

.loading-spinner-gradient {
  --lsg-color: #3b82f6;
  --lsg-size: 40px;
  --lsg-speed: 1s;
  width: var(--lsg-size);
  height: var(--lsg-size);
  border-radius: 50%;
  background: conic-gradient(from 0deg, transparent 0%, transparent 70%, var(--lsg-color) 100%);
  animation: lsg-spin var(--lsg-speed) linear infinite;
  position: relative;
}

.loading-spinner-gradient::after {
  content: '';
  position: absolute;
  inset: 4px;
  background: #ffffff;
  border-radius: 50%;
}

@keyframes lsg-spin { to { transform: rotate(360deg); } }
<div class="loading-spinner-gradient"></div>

Tip: Change the ::after background to match your page background for a seamless look on non-white surfaces.


68. Fade Dots

Dots fade in and out in sequence — simple and clean.

How It Works

Four dots share the same opacity and scale animation with staggered delays (0s to 0.6s in 0.2s increments). At the midpoint, each dot is at opacity: 1; scale(1), and at the endpoints it’s at opacity: 0.2; scale(0.8). The gentle scale change adds a subtle pulse without the dramatic movement of bounce animations.

.loading-fade-dots {
  --lfd-color: #64748b;
  --lfd-speed: 1.2s;
  display: flex;
  gap: 8px;
  align-items: center;
}

.loading-fade-dots span {
  width: 10px;
  height: 10px;
  background: var(--lfd-color);
  border-radius: 50%;
  animation: lfd-fade var(--lfd-speed) ease-in-out infinite;
}

.loading-fade-dots span:nth-child(1) { animation-delay: 0s; }
.loading-fade-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-fade-dots span:nth-child(3) { animation-delay: 0.4s; }
.loading-fade-dots span:nth-child(4) { animation-delay: 0.6s; }

@keyframes lfd-fade {
  0%, 100% { opacity: 0.2; transform: scale(0.8); }
  50% { opacity: 1; transform: scale(1); }
}
<div class="loading-fade-dots"><span></span><span></span><span></span><span></span></div>

What’s Next?

That completes all 24 loading animations from CSSKit. Next up: background effects — aurora, floating particles, ripple, confetti, and more. These are full-page atmospheric effects that transform any section.

Explore all 310 animations live at CSSKit or star the repo on GitHub.

You might also like

Enjoyed This Post?

Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.

Discussion