Entrances

Elastic Entrance Animation

A springy scale-in that wobbles slightly before settling.

Edit this in the studio

Opens with this animation loaded. Nothing is uploaded — the scene travels in the link.

animation.css
.elastic-in {
  transform: translate3d(180px, 100px, 0);
  width: 120px;
  height: 120px;
  background: #6366f1;
  border-radius: 20px;
  animation: elastic-in-anim 1000ms linear 0ms infinite both;
  will-change: transform, opacity;
}

@keyframes elastic-in-anim {
  0% {
    transform: translate3d(180px, 100px, 0) scale(0.2, 0.2);
    opacity: 0;
    animation-timing-function: ease-out;
  }
  2.5% {
    transform: translate3d(180px, 100px, 0) scale(0.417, 0.417);
    opacity: 0.161;
  }
  5% {
    transform: translate3d(180px, 100px, 0) scale(0.717, 0.717);
    opacity: 0.308;
  }
  7.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 0.445;
  }
  10% {
    transform: translate3d(180px, 100px, 0) scale(1.2, 1.2);
    opacity: 0.571;
  }
  12.5% {
    transform: translate3d(180px, 100px, 0) scale(1.291, 1.291);
    opacity: 0.685;
  }
  15% {
    transform: translate3d(180px, 100px, 0) scale(1.283, 1.283);
    opacity: 0.785;
  }
  17.5% {
    transform: translate3d(180px, 100px, 0) scale(1.206, 1.206);
    opacity: 0.87;
  }
  20% {
    transform: translate3d(180px, 100px, 0) scale(1.1, 1.1);
    opacity: 0.938;
  }
  22.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 0.983;
  }
  25% {
    transform: translate3d(180px, 100px, 0) scale(0.929, 0.929);
    opacity: 1;
  }
  27.5% {
    transform: translate3d(180px, 100px, 0) scale(0.897, 0.897);
    opacity: 1;
  }
  30% {
    transform: translate3d(180px, 100px, 0) scale(0.9, 0.9);
    opacity: 1;
  }
  32.5% {
    transform: translate3d(180px, 100px, 0) scale(0.927, 0.927);
    opacity: 1;
  }
  35% {
    transform: translate3d(180px, 100px, 0) scale(0.965, 0.965);
    opacity: 1;
  }
  37.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 1;
  }
  40% {
    transform: translate3d(180px, 100px, 0) scale(1.025, 1.025);
    opacity: 1;
  }
  42.5% {
    transform: translate3d(180px, 100px, 0) scale(1.036, 1.036);
    opacity: 1;
  }
  45% {
    transform: translate3d(180px, 100px, 0) scale(1.035, 1.035);
    opacity: 1;
  }
  47.5% {
    transform: translate3d(180px, 100px, 0) scale(1.026, 1.026);
    opacity: 1;
  }
  50% {
    transform: translate3d(180px, 100px, 0) scale(1.012, 1.012);
    opacity: 1;
  }
  52.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 1;
  }
  55% {
    transform: translate3d(180px, 100px, 0) scale(0.991, 0.991);
    opacity: 1;
  }
  57.5% {
    transform: translate3d(180px, 100px, 0) scale(0.987, 0.987);
    opacity: 1;
  }
  60% {
    transform: translate3d(180px, 100px, 0) scale(0.988, 0.988);
    opacity: 1;
  }
  62.5% {
    transform: translate3d(180px, 100px, 0) scale(0.991, 0.991);
    opacity: 1;
  }
  65% {
    transform: translate3d(180px, 100px, 0) scale(0.996, 0.996);
    opacity: 1;
  }
  67.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 1;
  }
  70% {
    transform: translate3d(180px, 100px, 0) scale(1.003, 1.003);
    opacity: 1;
  }
  72.5% {
    transform: translate3d(180px, 100px, 0) scale(1.005, 1.005);
    opacity: 1;
  }
  75% {
    transform: translate3d(180px, 100px, 0) scale(1.004, 1.004);
    opacity: 1;
  }
  77.5% {
    transform: translate3d(180px, 100px, 0) scale(1.003, 1.003);
    opacity: 1;
  }
  80% {
    transform: translate3d(180px, 100px, 0) scale(1.002, 1.002);
    opacity: 1;
  }
  82.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 1;
  }
  85% {
    transform: translate3d(180px, 100px, 0) scale(0.999, 0.999);
    opacity: 1;
  }
  87.5% {
    transform: translate3d(180px, 100px, 0) scale(0.998, 0.998);
    opacity: 1;
  }
  90% {
    transform: translate3d(180px, 100px, 0) scale(0.998, 0.998);
    opacity: 1;
  }
  92.5% {
    transform: translate3d(180px, 100px, 0) scale(0.999, 0.999);
    opacity: 1;
  }
  95% {
    transform: translate3d(180px, 100px, 0) scale(0.999, 0.999);
    opacity: 1;
  }
  97.5% {
    transform: translate3d(180px, 100px, 0);
    opacity: 1;
  }
  100% {
    transform: translate3d(180px, 100px, 0);
    opacity: 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .elastic-in {
    animation: none;
    transition: none;
  }
}

Why it is built this way

Same principle as the bounce: the oscillation is baked into keyframes rather than expressed as an easing function. Use it sparingly — elastic motion draws a lot of attention, which is the point once per screen and a nuisance twice.

What it animates

3 properties across 2 CSS declarations. The bar shows when each one is moving.

Opacity
opacity
0 1
3 stopscompositor
Scale X
transform
0.2 1
baked2 stopscompositor
Scale Y
transform
0.2 1
baked2 stopscompositor

What it costs to run

Opacity, scale x and scale y are handled by the compositor: no layout, no repaint, and the work can run off the main thread. This is the cheap end of CSS animation, which is why it stays smooth on a slow phone where other properties will not.

It runs for 1.00s across 7 keyframe stops, and most of those are not hand-written. The easing here oscillates, and no single cubic-bezier can — so the curve is sampled into extra stops on export. That is why the CSS is longer than you would write yourself, and why it needs no JavaScript.

3 curves in play — Ease Out, Linear and Elastic Out on a plain box. Different properties easing differently is how a movement gets texture: the eye reads the mismatch as weight rather than as a mistake.

Accessibility

Starting at opacity 0 makes the reduced-motion guard load-bearing rather than polite: strip it and a reader who asked for less movement gets no content at all, because nothing ever fades it in. It fades over 1.00s alongside scale x and scale y, and the guard skips all of it together. The generated CSS ends on the resting state, so skipping straight there is safe.

Related animations

elasticspringentrancescale