GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Cute CSS Animations: 9 Kawaii Character Demos

Cute CSS animations are characters that blink, bounce, and blush from nothing but shapes and keyframes — no image files — and across these nine demos every

Auto-plays · click a tile to jump to its section · all nine in one zip

The order is neither difficulty nor popularity — it is how close each character stands to its audience. First they pull faces on their own (01 through 03), then they make their own entrance (04 and 05), then two of them hold a conversation (06), then they wait for a visitor and react (07 and 08), and the last one hands you part of its cookie (09). Every body here is a handful of circles, pills, and crescents, so chasing nice css animations takes geometry rather than a drawing tablet — shift the placement ratios and the expression changes. Jointed mascots that wave and doze live one step over in Mascot Animation: 9 CSS Parts from Wave to Sleep, and a heavier rain of hearts belongs in Canvas Particle Animation: 9 Engine Patterns.

01Heart-eyes blink

A round face with hearts for eyes that squeeze shut every few seconds — the character for a welcome-back screen after login.

Heart-eyes blink
Open in a new tab

01 · Heart-eyes blink — Measured area 2.374% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform-originscaleYanimation-iteration-count

The blink is a vertical squash of the eye

.hb.is-demo .hb__eye {
  animation-name: hbBlink;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
}
@keyframes hbBlink {
  0%, 40% { transform: scaleY(1); }
  47% { transform: scaleY(.08); }
  54%, 100% { transform: scaleY(1); }
}

An eyelid overlay would stack two layers and read as a ghost, so the single eye element squashes to scaleY(.08), and the blink occupies only the short stretch around the 47% mark of a 2-second loop.

Heartbeats and blush fill the quiet stretch

@keyframes hbBeat {
  0%, 12% { transform: scale(1); }
  17% { transform: scale(1.14); }
  22% { transform: scale(1); }
  27% { transform: scale(1.1); }
  32%, 100% { transform: scale(1); }
}
@keyframes hbBlush {
  0%, 61% { opacity: 0; }
  62%, 87% { opacity: .55; }
  88%, 100% { opacity: 0; }
}

Two heartbeats early and a blush late fill the gap between blinks — 11 of 23 sampled frames moved, cumulative changed area 2.374% — and a real press flips aria-pressed so the same delighted look holds still, with cheeks at .55 and hearts at 1.16.

02Squishy jelly bounce

A jelly that crouches, stretches tall into a hop, and wobbles side to side on landing — the mascot for a game's start button.

Squishy jelly bounce
Open in a new tab

02 · Squishy jelly bounce — Measured area 12.578% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

cubic-bezierscaletranslateY

Squash and stretch inside one transform

@keyframes jlHop {
  0% { transform: translateX(-50%) scale(1, 1); }
  8% { transform: translateX(-50%) scale(1.16, .66); }
  16% { transform: translateX(-50%) translateY(-52px) scale(.9, 1.16); }
  24% { transform: translateX(-50%) translateY(-18px) scale(.98, 1.05); }
  30% { transform: translateX(-50%) scale(1.12, .78); }
  38% { transform: translateX(-50%) scale(.96, 1.06); }
  46% { transform: translateX(-50%) scale(1.06, .93); }
  54% { transform: translateX(-50%) scale(.97, 1.04); }
  62% { transform: translateX(-50%) scale(1.03, .97); }
  70%, 100% { transform: translateX(-50%) scale(1, 1); }
}

Every squash and stretch rides one scale, so width and height never move and the layout never recalculates frame to frame, while the landing wobble decays through four passes on its way to 17 of 23 moving frames.

Every press adds one bigger bounce

document.querySelector('.jl__hit').addEventListener('click', function () {
  jl.classList.remove('is-demo');
  hits = hits + 1;
  count.textContent = String(hits);
  body.classList.remove('jl__body--boing');
  void body.offsetWidth;
  body.classList.add('jl__body--boing');
});

An animation class that is already attached will not restart, so the handler strips it, forces one layout read with void body.offsetWidth, and re-adds it to replay the taller one-shot bounce from the top.

03Belly-laugh bean

A bean buddy whose crescent eyes fold shut while the laugh tips its body and shakes its belly — a fit for the badge that appears once a comment posts.

Belly-laugh bean
Open in a new tab

03 · Belly-laugh bean — Measured area 8.074% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotatescaleXanimation-duration

The tilt pivots on the sole

.bn__bean {
  --tilt: 4deg;
  position: absolute; bottom: 4px; left: 50%; width: 52%; aspect-ratio: 1 / 1.18;
  background: $color; border: 3px solid $subject-ink;
  border-radius: 50% 50% 46% 46% / 42% 42% 58% 58%;
  transform: translateX(-50%);
  transform-origin: 50% 100%; // attach point: the sole — the body tips, the feet stay planted
}

Park the origin at the body's center and every laugh shoves the feet sideways off the floor, but 50% 100% at the sole keeps them grounded no matter how far the body tips.

One variable is the whole laugh amplitude

.bn[data-level="1"] .bn__bean { --tilt: 8deg; }
.bn[data-level="2"] .bn__bean { --tilt: 13deg; }
.bn[data-level="3"] .bn__bean {
  --tilt: 4deg;
  animation: bnFold 700ms cubic-bezier(.2, 1.4, .4, 1) 1 forwards;
}
@keyframes bnTilt {
  0%, 100% { transform: translateX(-50%) rotate(calc(var(--tilt) * -1)); }
  50% { transform: translateX(-50%) rotate(var(--tilt)); }
}

Each press raises data-level, only --tilt changes, and the third stage swaps the loop for a one-shot fold that doubles the body over — 22 of 23 frames carried motion in that capture.

04Doodle-line walk cycle

A thin-line character that walks in from off screen, drawing its own stroke — the scroll guide for an intro page.

Doodle-line walk cycle
Open in a new tab

04 · Doodle-line walk cycle — Measured area 12.113% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

stroke-dashoffsetstroke-linecaptranslateX

The entrance draws its own stroke

.dw__draw {
  fill: none; stroke: $subject-ink; stroke-width: 4;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 100; stroke-dashoffset: 0;
}
@keyframes dwDraw {
  0% { stroke-dashoffset: 100; }
  38%, 100% { stroke-dashoffset: 0; }
}

Every path carries pathLength="100", so body, legs, and arms draw at one speed regardless of their real lengths, and the drawing finishes inside the first 38% of the 2-second loop.

Legs and arms pivot in turn at the joints

// Legs and arms swing in turn around the hip and shoulder joints where they attach.
// A child of the SVG needs transform-box: view-box before transform-origin lands in viewBox pixels
.dw__leg { transform-box: view-box; transform-origin: 200px 148px; }
.dw__arm { transform-box: view-box; transform-origin: 200px 104px; }
.dw__leg--l, .dw__arm--r { animation-name: dwSwingA; }
.dw__leg--r, .dw__arm--l { animation-name: dwSwingB; }
.dw__leg, .dw__arm {
  animation-duration: .5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes dwSwingA {
  0%, 100% { transform: rotate(16deg); }
  50% { transform: rotate(-16deg); }
}
@keyframes dwSwingB {
  0%, 100% { transform: rotate(-16deg); }
  50% { transform: rotate(16deg); }
}

The left leg shares its keyframes with the right arm while the right leg runs the mirrored set, and a press flips an inner layer with scaleX(-1) so the walker turns around without touching the walk transform.

05Sticker pop-in

A white-bordered sticker that pops out of nothing, overshoots, and settles — a part for a profile-decorating sticker sheet.

Sticker pop-in
Open in a new tab

05 · Sticker pop-in — Measured area 27.173% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform-origincubic-bezierbox-shadow

Popping from zero, overshooting, settling

@keyframes stPop {
  0% { transform: rotate(-10deg) scale(0); }
  14% { transform: rotate(0deg) scale(1.16); }
  24% { transform: rotate(-4deg) scale(.96); }
  32%, 58% { transform: rotate(-4deg) scale(1); }
  64% { transform: rotate(-4deg) scale(1.05); }
  72%, 100% { transform: rotate(-4deg) scale(1); }
}

Overshooting past full size to 1.16 at the 14% mark and sagging back to .96 is what makes the landing feel stuck on, and the short quiet stretch afterward still holds the cumulative changed area at 27.173%.

The expression swap is a one-frame cut

.st__eyeWink { border-top: 4px solid $subject-ink; border-radius: 50%; visibility: hidden; }
.st__eyeWink--l { left: 26%; }
.st__eyeWink--r { right: 26%; }
.st--wink .st__eyeOpen--r { visibility: hidden; }
.st--wink .st__eyeWink--r { visibility: visible; }
sticker.classList.toggle('st--wink', hits % 5 === 0 && hits > 0);
sticker.classList.remove('st--nudge');
void sticker.offsetWidth;
sticker.classList.add('st--nudge');

Two full eye sets sit stacked in the same slot, and visibility picks one, so the fifth press swaps the face in a single cut where a crossfade would print both faces half-transparent on one frame.

06Speech-bubble chatter

Two characters facing each other, trading speech bubbles — the greeting pair for a support chat's first hello.

Speech-bubble chatter
Open in a new tab

06 · Speech-bubble chatter — Measured area 27.268% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

scaleYscaleanimation-delay

The tail pokes out before the body stretches

.ch.is-demo .ch__tail { animation-name: chTail; animation-duration: $duration; animation-delay: -60ms; animation-timing-function: $easing; animation-iteration-count: infinite; }
@keyframes chPopL {
  0% { transform: scale(0); }
  10% { transform: scale(1.08); }
  16%, 44% { transform: scale(1); }
  45%, 100% { transform: scale(0); }
}

A negative animation-delay: -60ms runs the tail's keyframes a step ahead of the body on the same 2-second loop, and each bubble stretches out from the corner its tail touches.

Pressing pins your own bubble

.ch.mine .ch__bubble--l { animation: none; transform: scale(1); }
.ch.mine .ch__bubble--r { animation: none; transform: scale(0); }
document.querySelector('.ch__hit').addEventListener('click', function () {
  ch.classList.remove('is-demo');
  var on = ch.classList.toggle('mine');
  this.setAttribute('aria-pressed', String(on));
});

The press drops the preview loop, and the .mine class pins your side's bubble open at scale(1) while the state rides aria-pressed alone — 16 of 23 moving frames and 27.268% cumulative area on the measured run.

07Tail-wag waiting dog

A dog holding its spot with the tail sweeping and the ears swaying — the buddy for an upload-wait screen.

Tail-wag waiting dog
Open in a new tab

07 · Tail-wag waiting dog — Measured area 4.646% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotatetransform-originanimation-timing-function

Tail, ears, and head each pivot where they attach

.dg__tail {
  position: absolute; bottom: 38%; right: 16%; width: 6%; height: 24%;
  background: $subject-pink; border: 3px solid $subject-ink; border-radius: $r-pill;
  transform-origin: 50% 100%;
  animation-name: dgWag;
  animation-duration: .5s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
@keyframes dgWag {
  0%, 100% { transform: rotate(-22deg); }
  50% { transform: rotate(22deg); }
}

The tail pivots at the root where it meets the body, the ear at the top edge where it joins the head, the head at the neck, and the three run without a gap for a full 23 of 23 moving frames.

More pets, a faster tail

.dg[data-speed="2"] .dg__tail { animation-duration: .36s; }
.dg[data-speed="3"] .dg__tail { animation-duration: .26s; }
pets = pets + 1;
dg.dataset.speed = String(Math.min(1 + Math.floor(pets / 3), 3));
dg.classList.toggle('dg--love', pets === 5);

Every third pet shortens the tail cycle from 0.5s through 0.36s to 0.26s, and the fifth pet cuts the eyes to crescents while one heart rises.

08Sparkle-eyes heart particles

A character with stars glinting inside its eyes while hearts bloom from wherever you press — the reaction face for a like button.

Sparkle-eyes heart particles
Open in a new tab

08 · Sparkle-eyes heart particles — Measured area 4.313% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateYscaleanimation-fill-mode

Three preview hearts rise on offsets

.sp__auto {
  animation-name: spRise;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
}
.sp__auto--1 { left: 30%; bottom: 34%; animation-delay: 0s; }
.sp__auto--2 { left: 56%; bottom: 30%; animation-delay: .66s; }
.sp__auto--3 { left: 42%; bottom: 44%; animation-delay: 1.32s; }

animation-fill-mode: backwards keeps each heart invisible during its delay window, and the offsets of 0s, .66s, and 1.32s keep at least one heart in flight across the whole 2 seconds — every one of the 23 frame diffs moved.

A heart blooms exactly where you press

var rect = scene.getBoundingClientRect();
var heart = document.createElement('span');
heart.className = 'sp__fly';
heart.setAttribute('aria-hidden', 'true');
heart.innerHTML = HEART_SVG;
heart.style.left = ((e.clientX || rect.left + rect.width / 2) - rect.left) + 'px';
heart.style.top = ((e.clientY || rect.top + rect.height / 2) - rect.top) + 'px';
scene.appendChild(heart);
heart.addEventListener('animationend', function () { heart.remove(); });

The handler converts the press point into scene coordinates with getBoundingClientRect(), plants a heart span at exactly that spot, and removes it on animationend, so nothing translucent is ever left on the stage.

09Cookie shared bite by bite

A character holding a cookie that loses a mouthful on every press through four stages, until the character finally sulks — a four-a-day check-in screen.

Cookie shared bite by bite
Open in a new tab

09 · Cookie shared bite by bite — Measured area 11.737% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-pathscaletranslateY

A bite is a clipped circle

.ck__cookie {
  position: absolute; inset: 0;
  background: $subject-cream; border: 5px solid $subject-orange-deep; border-radius: 50%;
  clip-path: circle(50% at 50% 50%);
}
.ck[data-bite="1"] .ck__cookie { clip-path: circle(44% at 60% 38%); }
.ck[data-bite="2"] .ck__cookie { clip-path: circle(36% at 66% 44%); }
.ck[data-bite="3"] .ck__cookie { clip-path: circle(26% at 70% 52%); }
.ck[data-bite="4"] .ck__cookie { clip-path: circle(15% at 72% 58%); }

Shrinking the radius while pushing the center toward the upper right leaves a lower-left crescent that reads as bite marks, and the preview cuts from whole to three bites at 20% intervals on steps(1, end).

Cheeks puff one variable per bite

.ck__cheek {
  position: absolute; top: 46%; width: 22%; aspect-ratio: 1;
  background: $subject-pink; border-radius: 50%; opacity: .75;
  transform: scale(calc(1 + var(--puff)));
  transform-origin: 50% 50%;
}
.ck[data-bite="1"] .ck__char { --puff: .05; }
.ck[data-bite="2"] .ck__char { --puff: .1; }
.ck[data-bite="3"] .ck__char, .ck[data-bite="4"] .ck__char { --puff: .16; }

--puff grows by .05 per bite and feeds straight into the cheek's calc(), while three falling crumbs bridge the quiet frames to hold the cumulative changed area at 11.737%.

Where do cool css animations examples like these work best?

The first spot that pays off is the moment a finished task needs confirming. Heart eyes after a login (01), the belly laugh under a freshly posted comment (03), the sticker pop when a decoration lands (05) — a character on a success screen says the job is done with a face instead of a line of copy, and a few shapes changing expression carry that whole message.

The second is anywhere a wait needs softening. The upload dog (07) reports progress as tail speed and the pre-start jelly (02) unwinds tension with its wobble, which is the same information a percentage would deliver, handed over as a mood instead of a number.

The third is anywhere the screen talks back. The walking scroll guide (04), the support-chat greeting pair (06), the like-button hearts (08), and the four-a-day cookie (09) step forward as conversation partners rather than ornaments, and the moment a character answers you, cool css animations stop being decoration and start carrying the page's tone. One rule holds across all three spots: a single character per screen, because nine on one page turns cute into noise and blurs what the page is for.

Where it breaks — the trap

A blink alone leaves a static poster

The blink fills only the short stretch between 40% and 54% of the 2-second loop, so a face that only blinks spends most of the captured frames wide awake and frozen. 01 plugs the gap with two heartbeats up front and a blush at the back, which is how the run reached 11 of 23 frames with motion, and the representative image on each grid card comes from that same capture.

A wrong origin slides the character across the floor

Set the laughing bean's tilt origin at the center of its body, and every laugh pushes its feet out from under it; the single line 50% 100% at the sole ends the slide. The dog's tail, ears, and head swing naturally only once each origin sits at the root, the attachment, and the neck, and the walker's SVG legs additionally need transform-box: view-box before their pivot lands in viewBox pixels.

An opacity fade leaves translucent ghosts

Swapping expressions through opacity prints two half-transparent faces onto one frame, and the same ghosting catches a bubble mid-disappear. The sticker and the chatter pair cut between stacked eye sets and stacked bubbles with visibility instead, and everything that has to end — the hearts in 07 and 08, the crumbs in 09 — shrinks to scale(0) rather than fading out. The password that opens the nine-part bundle is v8539tgs, and every one of the fixes above is already inside the files it unlocks.

Three variants and accessibility

Turn three variables and the temperament of all nine shifts together.

variable new value what changes
duration $dur-loop 2s → 1s blinks and wobbles run twice as fast, reading as more easily startled
easing $ease-pop → $ease-spring the leap loses its pop and settles with a glide
color $subject-pink → $subject-mint the candy-pink cast trades its clothes for sharp mint

Every pressable area is a real <button> carrying an aria-label, pressed state rides aria-pressed on 01 and 06 and a single data-level, data-speed, or data-bite attribute on the others, and the counters on the jelly, the sticker, and the hearts are decorative aria-hidden output while the button label carries the news. Under prefers-reduced-motion: reduce, all nine loops stop and each leaves the still frame that reads best — the heart-eyed face awake, the walker fully drawn, the dog seated and waiting — with !important on those declarations so the preview class cannot out-rank them. Nothing on any of these screens is an image file, so what you unpack opens and runs in a browser exactly as delivered.

Characters with joints that wave and doze continue in Mascot Animation, and filling a whole stage with drifting hearts is the job of Canvas Particle Engine. The pivot math rests on MDN's transform-origin page, the bounce numbers on animation-timing-function, the self-drawing stroke on stroke-dashoffset, the cookie bites on clip-path, and the pressed-state announcements on aria-pressed.

FAQ

Do I need drawing skills for cool css animations like these

No — the face in 01 is one circle wearing two hearts, two cheeks, and a half-circle mouth, and a crescent eye is an arc drawn with nothing but border-top. Drawing ability never enters the picture, because nudging the same few shapes a few percent over changes the entire expression.

Why not just embed a GIF or a short video

A GIF cannot be clicked. Each of the nine answers a press with a stage change, an expression, or a particle, respects the visitor's reduced-motion setting because that choice lives in the code, and adds zero image weight since there is not a single picture file in the set.

Will several characters on one page slow it down

All motion here is transform and opacity, so nothing forces a layout recalculation — building the jelly's wobble on width and height instead would relayout every frame, which is precisely the cost these nine refuse to pay. Even so, the one-character-per-screen rule above matters more for clarity than the performance question does.

Enter the archive password

The password is inside this article. You will find it as you read.