Chapter 04 of 11Part 2 · CSS
CSS transitions: hover, focus and entry
The transition shorthand, hover and focus states for every input, and how to animate elements in from display: none.
8 minBy Zuzo StudioUpdated
In this chapter you will
- write the transition shorthand and give entering and leaving different timings
- build hover, focus and press states that work for mouse, keyboard and touch
- animate an element in from display: none with @starting-style
Before you startThis builds on Staggered animation and sequencing.
Most of the motion on a good website is small: a button that lifts when the pointer reaches it, a link whose underline grows, a menu that fades in rather than snapping open. Nearly all of it is built with CSS transitions. A transition tells the browser that when a property changes value, it should move between the old and new values over time instead of jumping. No JavaScript, no keyframes, just one declaration. This chapter covers the transition shorthand, which properties to transition, hover and focus states that work for mouse, touch and keyboard, and the two newer features that finally let elements animate in from display: none.
01How a transition works
A transition needs two things: a property whose value changes, and a transition declaration on the element that says how to animate that change. The change can come from anything that alters the computed style, such as a :hover state, a class added by JavaScript or a media query.
.button {
background-color: #1f2a37;
transition: background-color 150ms ease-out;
}
.button:hover {
background-color: #3b4c63;
}
When the pointer enters, the colour moves to the hover value over 150ms. When it leaves, it moves back the same way. That reverse happens because the transition is declared on the base state, so it applies in both directions.
You can use that to give entering and leaving different timings. The transition that runs is the one declared on the state you are moving to. Put a slower one on the hover state and a faster one on the base, and the hover arrives gently and lets go quickly.
.card { transition: transform var(--dur-fast) var(--ease-in); }
.card:hover {
transform: translateY(-4px);
transition: transform var(--dur-base) var(--ease-out);
}
02The CSS transitions shorthand
transition is a shorthand for five properties. MDN's transition reference lists them and their defaults:
| Property | Sets | Default |
|---|---|---|
transition-property |
Which properties animate | all |
transition-duration |
How long it takes | 0s |
transition-timing-function |
The easing curve | ease |
transition-delay |
How long to wait before starting | 0s |
transition-behavior |
Whether discrete properties animate | normal |
In the shorthand, the order of most values does not matter, with one exception: the first time value is always the duration and the second is always the delay. transition: opacity 200ms 100ms lasts 200ms and starts after 100ms.
Separate several transitions with commas, each with its own timing:
.button {
transition:
background-color var(--dur-fast) var(--ease-out),
transform var(--dur-base) var(--ease-out);
}
Because the default duration is 0s, a transition with no duration does nothing at all. That is the first thing to check when one seems not to work.
03Which properties to transition
A property can be animated if the browser knows how to calculate the values in between. Lengths, colours, numbers and transforms all interpolate smoothly. MDN's guide to using CSS transitions has the basics and links the full list.
Being animatable is not the same as being a good idea. Some properties are cheap for the browser to change every frame and some are expensive:
transformandopacityare the cheapest. Usetranslate,scaleandrotateto move, size and turn things.- Colours, such as
color,background-colorandborder-color, make the browser repaint the element. On a button that is fine. - Layout properties, such as
width,height,top,marginandpadding, make the browser recalculate the layout of the page on every frame. Avoid transitioning them on anything large.
We look at why in compositor only animation, and our website animation performance article has a full table of what to animate instead of what.
Some properties have no in between values at all. display is either none or block, never halfway. These are called discrete properties, and by default they simply switch at the start or end of a transition. That used to make animating an element in from display: none impossible in CSS alone. The last section shows the fix.
04Hover and focus states that work for everyone
Hover only exists for people using a mouse or trackpad. Keyboard users move through a page with Tab, and touch users have no pointer hovering at all. Good interactive states cover all three.
Hover, only where it exists. On touch screens, a tap can trigger :hover and leave the element stuck in its hover state. Wrap hover motion in a media query that matches only devices with a real pointer:
@media (hover: hover) {
.button:hover { transform: translateY(-2px); }
}
Focus, always visible. The :focus-visible pseudo-class matches when the browser decides focus should be shown, typically for keyboard users, and not after a mouse click. Style it at least as clearly as hover, and do not delay it: the focus ring tells a keyboard user where they are, so it should appear the moment focus moves. It is fine to transition the outline offset or colour, but never hide the outline while a transition runs. Our accessibility basics chapter covers why focus matters.
Press, for confirmation. :active matches while an element is being pressed. A very short scale down gives the press feedback from the first chapter without any JavaScript.
05Animating in from display: none
Dropdown menus, dialogs and popovers are often hidden with display: none. Two problems used to stop them animating in CSS. A transition only runs when a value changes, and an element that was not displayed has no previous value to change from. And display itself cannot be animated, so on the way out the element disappeared before any fade could run.
Two features fix this, and MDN marks both as Baseline 2024, newly available across current browsers since August 2024.
@starting-style sets the values to transition from when an element gets its first style, such as when it is added to the page or its display changes from none. MDN notes that without it, transitions are not triggered on that first style update.
transition-behavior: allow-discrete lets discrete properties like display take part in a transition. For display, the browser switches to the visible value at the start of the entry and to none at the very end of the exit, so the element stays visible while it fades.
.menu {
opacity: 0;
transform: translateY(-0.5rem);
display: none;
transition: opacity var(--dur-base) var(--ease-out),
transform var(--dur-base) var(--ease-out),
display var(--dur-base) allow-discrete;
}
.menu.is-open {
display: block;
opacity: 1;
transform: none;
@starting-style { opacity: 0; transform: translateY(-0.5rem); }
}
In browsers without support, the menu still opens and closes, it just does not animate. That is a fine fallback.
06The project: buttons and links
Your hero buttons already rise in on load from the stagger chapter. Because that animation uses the backwards fill mode, it stops applying once it ends, so a hover transition on transform works afterwards without the two fighting.
.button, .scroll-cue::after {
transition: transform var(--dur-fast) var(--ease-out),
background-color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) {
.button:hover { transform: translateY(-2px); }
.scroll-cue:hover::after { transform: scaleX(1); }
}
.button:active { transform: scale(0.97); }
.button:focus-visible, .scroll-cue:focus-visible {
outline: 2px solid currentColor; outline-offset: 4px;
}
.scroll-cue { position: relative; }
.scroll-cue::after { content: ""; position: absolute; inset: auto 0 -2px;
height: 1px; background: currentColor; transform: scaleX(0); transform-origin: left; }
Check your understanding
3 questions
Key takeaways
- A transition animates a property between its old and new values when the computed style changes.
- The first time value in the shorthand is the duration and the second is the delay, and the default duration of 0s means nothing animates.
- Name the properties you transition instead of using all, and prefer transform and opacity for anything large.
- Wrap hover motion in @media (hover: hover) and make :focus-visible at least as clear as hover, with no delay.
- @starting-style and transition-behavior: allow-discrete let elements animate in and out of display: none.
Sources
- transitionMDN Web Docs, checked
- Using CSS transitionsMDN Web Docs, checked
- :focus-visibleMDN Web Docs, checked
- @starting-styleMDN Web Docs, checked
- transition-behaviorMDN Web Docs, checked