CSS Transitions and Keyframes, With 20 Copyable Examples

A CSS transition animates a property smoothly when its value changes, for example on hover: transition: transform 200ms ease-out. It needs a start and end state and runs once per change. @keyframes animations define their own steps, can loop and start without any state change. Animate transform and opacity for the smoothest results.

transition, part by part
PartExampleDefault
transition-propertytransform, opacityall
transition-duration200ms0s
transition-timing-functionease-out or cubic-bezier(.2,.8,.2,1)ease
transition-delay50ms0s
transition-behaviorallow-discretenormal

Updated

The playground animates a real element between two states with the property, duration, delay and easing you pick, and draws the easing as a curve so you can see why ease-out feels snappy and ease-in feels sluggish. Further down are twenty hover and focus effects you can edit live and paste into any site.

Easing presets

Transition vs @keyframes at a glance

When to use which
transition@keyframes animation
Starts whenA property value changesThe animation is applied
StepsStart and end onlyAs many as you write
RepeatNoanimation-iteration-count, including infinite
Reverses smoothly mid-wayYesOnly with extra work
Best forHover, focus, open and selected statesLoaders, attention cues, entrance effects

What each property costs to animate

Rendering work per frame (general rule, varies by browser)
PropertyLayoutPaintComposite only
transform, opacityNoNoUsually
color, background-colorNoYesNo
box-shadow, filterNoYesNo
width, height, padding, marginYesYesNo
top, left (positioned)YesYesNo

A reduced-motion safety net

Keeps state changes but removes the travel. Put it near the end of the stylesheet.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Fade a popover in and out with @starting-style

An enhancement for browsers that support allow-discrete and @starting-style. Others show and hide instantly.

[popover] {
  opacity: 0;
  transition: opacity .2s, display .2s allow-discrete, overlay .2s allow-discrete;
}
[popover]:popover-open { opacity: 1; }
@starting-style {
  [popover]:popover-open { opacity: 0; }
}

20 micro-interactions to copy

Hover or focus each result to see it run. Every snippet ends with a reduced-motion rule.

Card lift

Raise a card and deepen its shadow.

<style>
.c{padding:16px;border-radius:12px;background:#fff;box-shadow:0 1px 3px #0002;transition:transform .2s ease-out,box-shadow .2s ease-out}.c:hover{transform:translateY(-4px);box-shadow:0 10px 24px #0002}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<div class="c">Hover the card</div>

Button grow

A small scale on hover and a press-in on click.

<style>
.b{padding:10px 18px;border:0;border-radius:99px;background:#4F98FB;color:#fff;transition:transform .15s ease-out}.b:hover{transform:scale(1.05)}.b:active{transform:scale(.97)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<button class="b">Book now</button>

Sliding underline

The line grows from the left using a background size.

<style>
a{color:#111;text-decoration:none;background:linear-gradient(#4F98FB,#4F98FB) 0 100%/0 2px no-repeat;transition:background-size .25s ease-out}a:hover,a:focus-visible{background-size:100% 2px}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<a href="#">Read the menu</a>

Colour fill

Background and text colours swap together.

<style>
.b{padding:10px 18px;border:2px solid #111;background:#fff;color:#111;transition:background-color .2s,color .2s}.b:hover{background:#111;color:#fff}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<button class="b">Subscribe</button>

Arrow nudge

The icon moves while the label stays still.

<style>
.b span{display:inline-block;transition:transform .2s ease-out}.b:hover span{transform:translateX(4px)}.b{padding:10px 16px;border:1px solid #ccc;background:#fff}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<button class="b">Next step <span>&rarr;</span></button>

Icon rotate

A plus turns into a cross.

<style>
.i{display:inline-grid;place-items:center;width:40px;height:40px;border-radius:50%;background:#eef2ff;font-size:24px;transition:transform .25s ease-in-out}.i:hover{transform:rotate(45deg)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<span class="i">+</span> Hover the icon

Focus ring fade

The outline offset animates for keyboard users.

<style>
input{padding:8px;border:1px solid #999;border-radius:8px;outline:2px solid transparent;outline-offset:6px;transition:outline-offset .15s,outline-color .15s}input:focus-visible{outline-color:#4F98FB;outline-offset:2px}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<label>Email <input></label>

Image zoom in frame

The picture scales but the frame clips it.

<style>
.f{width:180px;height:100px;overflow:hidden;border-radius:10px}.f div{height:100%;background:linear-gradient(135deg,#f59e0b,#ef4444);transition:transform .4s ease-out}.f:hover div{transform:scale(1.12)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<div class="f"><div></div></div>

Caption reveal

The caption slides up from the bottom edge.

<style>
.f{position:relative;width:200px;height:110px;overflow:hidden;border-radius:10px;background:#0f172a}.f p{position:absolute;inset:auto 0 0;margin:0;padding:8px;background:#fffd;transform:translateY(100%);transition:transform .25s ease-out}.f:hover p{transform:none}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<div class="f"><p>Harbour at dusk</p></div>

Soft glow

A coloured shadow fades in.

<style>
.b{padding:10px 18px;border:0;border-radius:10px;background:#4F98FB;color:#fff;box-shadow:0 0 0 #4F98FB00;transition:box-shadow .3s}.b:hover{box-shadow:0 0 20px #4F98FBaa}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<button class="b">Get the guide</button>

Subtle tilt

A few degrees of rotation make a sticker feel loose.

<style>
.s{display:inline-block;padding:12px 18px;background:#fde68a;transition:transform .2s ease-out}.s:hover{transform:rotate(-3deg) scale(1.03)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<span class="s">New in stock</span>

Border draw

The border colour changes from grey to brand blue.

<style>
.c{padding:14px;border:2px solid #e5e7eb;border-radius:10px;transition:border-color .2s}.c:hover{border-color:#4F98FB}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<div class="c">Choose this plan</div>

Dim the others

Hovering one item fades its siblings.

<style>
.l{display:flex;gap:8px}.l span{padding:8px 12px;background:#dbeafe;transition:opacity .2s}.l:hover span:not(:hover){opacity:.4}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<div class="l"><span>Home</span><span>Work</span><span>Contact</span></div>

Shine sweep

A light band sweeps across on hover.

<style>
.b{position:relative;overflow:hidden;padding:10px 18px;border:0;background:#111;color:#fff}.b::after{content:"";position:absolute;inset:0;background:linear-gradient(110deg,transparent 40%,#fff6 50%,transparent 60%);transform:translateX(-100%);transition:transform .6s}.b:hover::after{transform:translateX(100%)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<button class="b">Join the list</button>

Switch thumb

A checkbox styled as a switch with a sliding thumb.

<style>
input{appearance:none;width:44px;height:24px;border-radius:99px;background:#cbd5e1;position:relative;transition:background-color .2s}input::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;transition:transform .2s ease-out}input:checked{background:#4F98FB}input:checked::after{transform:translateX(20px)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<label><input type="checkbox"> Notifications</label>

Accordion chevron

The marker rotates when a details element opens.

<style>
summary{cursor:pointer;list-style:none}summary::before{content:"\25B8";display:inline-block;margin-right:6px;transition:transform .2s}details[open] summary::before{transform:rotate(90deg)}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<details><summary>Delivery times</summary><p>Two to four working days.</p></details>

Pulse dot (keyframes)

A looping status dot built with @keyframes.

<style>
@keyframes p{0%{box-shadow:0 0 0 0 #22c55e88}100%{box-shadow:0 0 0 12px #22c55e00}}.d{display:inline-block;width:10px;height:10px;border-radius:50%;background:#22c55e;animation:p 1.5s ease-out infinite}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<span class="d"></span> Open now

Spinner (keyframes)

Linear timing keeps the rotation even.

<style>
@keyframes s{to{transform:rotate(1turn)}}.s{width:28px;height:28px;border:3px solid #e5e7eb;border-top-color:#4F98FB;border-radius:50%;animation:s .8s linear infinite}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<div class="s" role="status" aria-label="Loading"></div>

Fade up on load (keyframes)

fill-mode both keeps it hidden during the delay.

<style>
@keyframes up{from{opacity:0;transform:translateY(8px)}}.m{animation:up .5s ease-out .2s both}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<p class="m">Thanks, your table is booked.</p>

Error shake (keyframes)

A short horizontal shake that plays once on hover here.

<style>
@keyframes k{25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}input{padding:8px;border:2px solid #dc2626}input:hover{animation:k .3s 2}
@media (prefers-reduced-motion: reduce) { * { transition-duration: 1ms !important; animation: none !important } }
</style>
<label>Code <input value="1234"></label>

Reading an easing curve

The horizontal axis of an easing curve is time and the vertical axis is progress. A steep start means most of the movement happens early, which is how ease-out gives that responsive feeling. A flat start means the element hesitates first, which is why ease-in on a hover feels laggy.

linear is a straight diagonal and looks mechanical for movement, though it suits spinners and colour cycles. steps(4) jumps in four equal hops instead of gliding, handy for sprite sheets or a typing effect.

Transition or @keyframes?

Choose a transition when there are two states and the user causes the change: hover, focus, open, selected. The browser works out the in-between frames and even reverses smoothly if the state flips back halfway through.

Choose @keyframes when the motion has more than two steps, should loop, or must play on its own when the page loads. animation-fill-mode: both keeps the first frame applied during any delay and the last frame after it ends, which avoids a flash.

Animating to and from display: none

Newer browsers let display and overlay take part in transitions when you add transition-behavior: allow-discrete, and @starting-style gives an element a style to animate from on its first render. Together they allow fade-in and fade-out of popovers and dialogs without script.

Support for these arrived during 2023 and 2024 and is uneven in older versions, so treat them as an enhancement: without support the element simply appears and disappears instantly.

How it works

  1. Pick which property to animate: movement, scale, rotation, colour or opacity.
  2. Set the duration and delay with the sliders, and choose an easing preset or drag the four bezier numbers.
  3. Press Play to toggle between the two states and watch the result next to its curve.
  4. Copy the transition line, then grab any of the twenty micro-interactions below.

Tips

Frequently asked questions

What is the difference between a transition and an animation?

A transition reacts to a change of value, such as a class being added or a hover starting, and only goes from the old value to the new one. An animation uses @keyframes, can have many steps, can repeat or alternate, and runs as soon as it is applied. Use transitions for state changes and keyframes for loaders and attention cues.

Why does my transition not run?

Common causes: the property is not animatable (display was not, until allow-discrete), the start value is auto (height: auto cannot be interpolated without newer features), the element was just inserted so there is no previous style, or the transition is declared only on the hover state so it snaps back on mouse out.

Which properties are cheap to animate?

transform and opacity can usually be handled by the compositor without recalculating layout or repainting, so they stay smooth. Animating width, height, top, left or margin forces layout on every frame, and box-shadow or filter cause repaints, which can stutter on slower phones.

What does cubic-bezier mean?

It describes the easing curve with two control points, cubic-bezier(x1, y1, x2, y2). The x values are time and must stay between 0 and 1; the y values are progress and may go outside that range, which creates an overshoot or a little bounce back.

How do I respect reduced motion?

Use the media query @media (prefers-reduced-motion: reduce) to shorten or remove movement. A simple approach is to keep colour and opacity changes but drop transforms, or set transition-duration and animation-duration to a tiny value inside that query.

Related: CSS animation generator, CSS transforms, Scroll animations, SVG animation, CSS cursor and opacity.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap