GODRICH

9 Micro Interactions CSS — One Line, Copy-Paste

Search for micro interactions css and the answer is always the same shape: a small, self-contained bit of motion — a button press, an icon swap, a toast

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

The grid above cycles through all nine on its own — watch it rather than trying to click anything, since each preview sits inside a fixed, non-scrollable iframe built only to loop. The nine below move from a single element noticing its own click (01–03), through state changes an interface makes on its own (04–06), into interactions that coordinate two or more elements at once (07–09). Each group adds one more moving part, so this is not the same trick repeated nine times.

01Button press scale

The button scales down to 0.96 the instant it is pressed and springs back on release, so a tap reads as physically registered before any server response arrives. Reach for it on submit buttons, clickable cards, and icon buttons — anywhere a plain hover color change is not enough proof that the click landed.

scale(0.96)스프링 이징active 상태
.btn {
  transform: scale(1);
  transition: transform $duration $easing, box-shadow $duration $easing;

  &:active {
    transform: scale(.96);
    box-shadow: $shadow-press;
  }
}

02Save-to-check animation

Clicking the save icon morphs the diskette outline into a checkmark while a colored ring pops in behind it, so the confirmation lives inside the icon itself instead of a separate toast. Form autosave buttons and note-app save indicators both suit it, since the icon is already where someone is looking when they press save.

clip-path 모프색상 전환0.3초
&.is-saved &__ring  { animation: ring-pop $duration $easing forwards; }
&.is-saved &__disk  { animation: disk-out $duration $easing forwards; }
&.is-saved &__check-bar--short { animation: check-short $duration $easing forwards; animation-delay: $duration * .35; }
&.is-saved &__check-bar--long  { animation: check-long $duration $easing forwards; animation-delay: $duration * .5; }

@keyframes ring-pop  { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: scale(1); } }
@keyframes disk-out  { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(.5); } }
@keyframes check-short { to { transform: rotate(45deg) scaleX(1); } }
@keyframes check-long  { to { transform: rotate(-52deg) scaleX(1); } }

03Copy-done feedback

Pressing the copy icon swaps it for a checkmark and floats a small tooltip up above the button before fading it out 1.2 seconds later. Code-block copy buttons and API-key copy buttons both need it, since the icon alone sits too small and too far from the cursor for a plain color change to register.

아이콘 스왑툴팁 페이드1.2초 유지
&.is-copied &__sheet--front { opacity: 0; transform: scale(.6); transition-delay: 0s; }
&.is-copied &__check { animation: check-in $duration $easing forwards; }
&.is-copied &__tip { animation: tip-float 1.4s $easing forwards; }

@keyframes tip-float {
  0%   { opacity: 0; transform: translate(-50%, 4px); }
  15%  { opacity: 1; transform: translate(-50%, -2px); }
  85%  { opacity: 1; transform: translate(-50%, -2px); }
  100% { opacity: 0; transform: translate(-50%, -10px); }
}

04Optimistic like with rollback

Tapping the heart fills it in red and bumps the count instantly, without waiting on a network response, and the loop below simulates a failed request by quietly rolling both back 0.8 seconds later. Social apps use this same fill-then-maybe-rollback shape for every like and bookmark toggle, since waiting on the server first would make every tap feel a beat slow.

낙관적 업데이트롤백 애니메이션0.8초
var b = document.querySelector('.like');
b.addEventListener('click', function () {
  b.classList.remove('is-demo');
  var liked = b.classList.toggle('is-liked');
  b.setAttribute('aria-pressed', liked);
  if (liked) setTimeout(function () { b.classList.remove('is-liked'); b.setAttribute('aria-pressed', 'false'); }, 800);
});

05Skeleton to content crossfade

The gray skeleton blocks and the real avatar, title, and body text sit stacked in the same card and simply swap opacity, so the loading state resolves without any layout jump. It fits a card list's first load or a profile page's initial render — anywhere a skeleton needs to hand off to real content in place.

opacity 크로스페이드겹친 레이어0.4초
.card__layer--skeleton {
  flex-direction: row;
  opacity: 1;
  transform: scale(1);
  transition: opacity $duration $easing, transform $duration $easing;
}
.card__layer--content {
  flex-direction: row;
  align-items: flex-start;
  opacity: 0;
  transform: scale(.98);
  transition: opacity $duration $easing, transform $duration $easing;
}

06Empty-state icon bounce

The icon shown above an empty list gently bounces up and down on a two-second loop, using nothing more than a translateY keyframe on the icon itself. It tells someone a search with no results or an empty cart is the intended state, not a broken page they should refresh.

translateY 바운스ease-in-out무한 반복
&__icon {
  position: relative;
  width: 52px;
  height: 40px;
  transform: translateY(0);
}

&.is-demo &__icon { animation: bounce $duration $easing infinite; }

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-10px); }
}

07Input error shake

Submitting an invalid value turns the input's border red through a separate overlay ring and gives the whole field a quick left-right shake, so the mistake cannot be scrolled past unnoticed. Login password fields and checkout card-number inputs both lean on the pairing of color and motion, since a red border alone is easy to miss on a long form.

translateX 흔들림빨강 테두리0.4초
&.is-error &__frame { animation: shake $duration $easing both; }
&.is-error &__ring  { opacity: 1; }

@keyframes shake {
  10%, 90% { transform: translateX(-$amount * .5); }
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX(-$amount); }
  40%, 60% { transform: translateX($amount * .7); }
  50%      { transform: translateX(-$amount * .7); }
}

08Tab indicator slide

Clicking a tab writes its index into a single CSS custom property, and the underline indicator's transform reads that property back to slide over with both width and position already interpolated. Settings-page tabs and dashboard category tabs both prefer it over animating left or width directly, since a transform never triggers layout.

transform 슬라이드width 보간0.25초
&__indicator {
  position: absolute;
  left: 4px; bottom: 4px;
  width: calc((100% - 8px) / 3);
  height: 3px;
  border-radius: $r-pill;
  background: $color;
  transform: translateX(calc(var(--i, 0) * 100%));
  transition: transform $duration $easing;
}

09Toast stacking

Each new toast pushes the ones already on screen up by one slot and shrinks them slightly, so several notifications stack visibly instead of piling on top of each other. Save-confirmation toasts and multi-file upload alerts both need the stacking, since a second notification arriving mid-animation would otherwise just cover the first one.

translateY 밀림scale 축소순차 스택
.toasts__list.is-demo {
  .toast--3 { animation: slot-3 $duration $easing infinite; }
}

@keyframes slot-3 {
  0%, 70%   { opacity: 0; transform: translateY(16px) scale(1); }
  80%, 92%  { opacity: 1; transform: translateY(0) scale(1); }
  98%, 100% { opacity: 0; transform: translateY(0) scale(1); }
}

Where it breaks — the trap

Only one of these nine ships with a genuinely fixed budget baked into its CSS, and it is the one most likely to get copied into a real app with extra content added on top. Item 09's toast list sits inside a container that is exactly 118px tall — one 34px slot plus two 42px slots for the pair of toasts a new arrival pushes upward. That height, along with the -42px and -84px offsets inside each slot's own keyframes, is sized for exactly three toasts at once. Stack a fourth without adjusting all three numbers together, and the oldest toast either gets clipped by the container's bottom edge or drifts up past the newest one instead of fading out cleanly above it. The archive folder carries the full nine with their variables already broken out, so a fourth slot becomes one more offset to add rather than math to re-derive from scratch. Unzip it with dawuywyc typed exactly as it reads here, no spaces added, no characters swapped.

Accessibility

All nine drop their motion under prefers-reduced-motion, but each keeps a different amount of the interaction rather than only switching transitions off. 01, 06, 07, and 08 — the ones built from a single transform loop — just stop moving and hold whichever end state was already showing. A shaken field still turns red, and a slid tab indicator still lands under the active tab, only without the trip getting there. 02, 03, 05, and 09 are multi-step reveals, so instead of skipping the transition they jump straight to the finished frame. The save icon shows its checkmark immediately, the skeleton never shows at all, and the toast stack renders already stacked. Item 04's rollback keeps working exactly the same either way, because the 0.8-second delay comes from a JavaScript setTimeout rather than a CSS animation. Reduced motion only removes the easing on the heart's fill itself. MDN's reference for prefers-reduced-motion covers the operating-system settings that turn the media query on.

Browse the rest of the collection on the CSS category page, and see what's actually inside the zip versus what's free to read here on the about page.

FAQ

What is a micro interaction in CSS?

A micro interaction in CSS is a short, single-purpose animation triggered by one user action or app-state change — pressing a button, saving a file, an empty search. It confirms something happened without needing any explanatory text nearby. The animation is usually built from a handful of properties like transform and opacity animating for well under a second, since anything longer starts to read as a delay rather than feedback.

Do these micro interactions need JavaScript?

A few of the nine do, and the demos above are honest about which ones. Item 04's optimistic-like rollback and item 08's tab indicator both need a few lines of JavaScript to toggle a class or write a CSS custom property on click. The other seven run on CSS alone once a class like is-saved or is-error gets added, whether that class comes from a framework's state binding or a plain addEventListener.

Will nine looping animations slow a page down?

Not meaningfully, since only the one actually shown to a visitor plays at any moment. Every keyframe here animates transform, opacity, or a registered custom property rather than a layout-triggering value like width or top. The grid at the top of this page runs all nine at once inside iframes specifically to prove that out; a real page only ever runs the one or two a person is currently looking at.

Enter the archive password

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