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.
| Part | Example | Default |
|---|---|---|
| transition-property | transform, opacity | all |
| transition-duration | 200ms | 0s |
| transition-timing-function | ease-out or cubic-bezier(.2,.8,.2,1) | ease |
| transition-delay | 50ms | 0s |
| transition-behavior | allow-discrete | normal |
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.
| transition | @keyframes animation | |
|---|---|---|
| Starts when | A property value changes | The animation is applied |
| Steps | Start and end only | As many as you write |
| Repeat | No | animation-iteration-count, including infinite |
| Reverses smoothly mid-way | Yes | Only with extra work |
| Best for | Hover, focus, open and selected states | Loaders, attention cues, entrance effects |
| Property | Layout | Paint | Composite only |
|---|---|---|---|
| transform, opacity | No | No | Usually |
| color, background-color | No | Yes | No |
| box-shadow, filter | No | Yes | No |
| width, height, padding, margin | Yes | Yes | No |
| top, left (positioned) | Yes | Yes | No |
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;
}
}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; }
}Hover or focus each result to see it run. Every snippet ends with a reduced-motion rule.
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>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>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>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>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>→</span></button>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 iconThe 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>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>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>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>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>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>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>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>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>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>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 nowLinear 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>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>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>
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.
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.
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.
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.
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.
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.
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.
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.
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.