States and interaction
A page that answers back: hover, focus and active states, a visible focus ring, and motion that helps instead of distracting.
Things change when touched
A button that looks identical before, during and after a press feels dead. CSS has a selector for each moment: :hover while the pointer is over it, :active while it is pressed, :focus-visible when it has keyboard focus, and :disabled when it cannot be used.
These are not decoration. They are the page telling the reader what is under the pointer, and confirming that a click landed. A control with no states is a control nobody trusts.
Four states, one button
.button {
background: #059669;
color: white;
transition: background 0.2s ease, transform 0.2s ease;
}
.button:hover { background: #047857; }
.button:active { transform: translateY(1px); }
.button:focus-visible { outline: 3px solid #065f46; outline-offset: 2px; }
.button:disabled { opacity: 0.5; cursor: not-allowed; }Focus is not optional
The outline a browser draws when you Tab to a link is the only thing telling a keyboard user where they are. Removing it — outline: none — without a replacement is the most common accessibility mistake in CSS.
:focus-visible is the modern version: it matches when the browser judges that the focus should be visible, which in practice means keyboard focus and not a mouse click. Style it deliberately: a ring offset from the control, in a colour with enough contrast to see.
A group can carry the focus too. :focus-within matches a container while anything inside it has focus, which is how a search field and its button can light up together, or a whole card can show that one of its links is active. It is a selector on the parent, not a property, and it saves adding a class from script just to style the container.
States that are not colour
Colour is the obvious signal, but it is not the only one and it is not always enough. A pressed button that moves down by a pixel, a link that gains an underline, a disabled control that loses its shadow — each says something without relying on a hue the reader may not distinguish. Pair the colour change with a second cue, and the state survives a monochrome screen and a colour-blind reader.
The cursor is part of the signal too: pointer on a link, not-allowed on a disabled control, text over an input. It is not a substitute for a visible state, because a touch screen has no cursor at all, but on a desktop it confirms what the element is before the click.
Motion with a purpose
A transition makes a change glide: name the properties, a duration around 0.15–0.3s, and an easing curve. Longer feels sluggish; transitioning everything (all) is how a card starts animating its shadow when you only meant its colour.
Some people turn animation off at the system level. @media (prefers-reduced-motion: reduce) is where you honour that, by removing movement while keeping the colour changes that carry meaning.
The order of the states
Pseudo-classes on the same element usually have the same specificity, so source order decides which one wins when two states are true at once. For links the old order is :link, :visited, :hover, :active — later states are meant to override earlier ones while the interaction happens.
Modern code replaces :link and :visited with a base a rule and puts :hover and :active after it. :focus-visible is separate: it can be true at the same time as :hover, so write it after the hover rule if both set the same property and the ring should survive a pointer resting on the control.
What a transition can actually interpolate
A transition needs two values of the same kind to move between. Colour, length, opacity and transform interpolate smoothly; display does not, because it has no middle value — it flips at the end. That is why a panel that “animates open” usually animates its height, opacity or transform instead.
The cheap properties are transform and opacity: the browser can composite them without recalculating the layout. Animating width, height or margin makes it measure and re-place the page on every frame, which is fine for one small element and expensive for a long list. Keep the motion short and name the property you mean.
Three state mistakes: giving an element a hover style but no :focus-visible equivalent, so keyboard users never see the same feedback; putting a transition on display, which snaps rather than glides; and changing a pressed state by a few percent of colour, which is technically a change and practically invisible.
Try these next
- Tab through a page with the default ring, then replace it with an offset outline and check the contrast.
- Add
:disabledstyling andcursor: not-allowedto a button that cannot be used. - Wrap the movement in
prefers-reduced-motion: reduceand keep only the colour change.
A button that answers
The button looks the same whatever happens to it. Give it a hover colour, a focus ring for keyboard users, and a short transition so the change glides.
3 left to do
- The button has a hover state
- Keyboard focus is visible
- The change glides
:hover for the colour, :focus-visible with an outline, and transition: background 0.2s ease; on the button itself.
Links that answer the keyboard
Style the three links so hover, keyboard focus and the pressed moment each look deliberate. Keep a visible focus ring, and let the colour change glide without moving the layout.
4 left to do
- The links have a hover state
- The links have a visible keyboard focus
- The links have a pressed state
- The colour change glides
:hover for the pointer, :focus-visible with outline: 3px solid #065f46; outline-offset: 2px; for the keyboard, :active for the press, and transition: color 0.2s ease; on the link itself.
If a hover effect does something important — a menu appearing, a tooltip — it needs a click and a focus path too. Phones have no hover, and neither does a keyboard.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Which selector styles a button while the pointer is over it?
