Styles, Interaction States, and Motion
Introduction
This is the third lesson of chapter 4, aligned with course 4 of the Meta Front-End Developer pathway ("HTML and CSS in Depth"). You can now structure forms and lay out pages. This lesson is about making the interface feel alive and stay maintainable: how the browser decides which CSS rule wins, how to style hover, focus and disabled states, how to animate with restraint, and how to organize styles so a team can reuse them.
Many beginner problems come from one root cause: not understanding the cascade. A rule you wrote "does nothing," so you add !important, and soon nobody can change anything. Once you understand specificity and inheritance, most of these problems disappear. The practice scenario is original: a reusable button system for Harbor Workshops with visible keyboard focus and calm, optional motion.
Learning objectives
- Explain the cascade: origin, specificity, and source order, and calculate the specificity of selectors.
- Use inheritance,
inherit,initial, and custom properties (CSS variables). - Write reusable class patterns, with BEM naming and modifier classes.
- Style states with
:hover,:focus-visible,:active,:disabled,:checked, and:invalid. - Provide visible focus indicators that meet accessibility needs.
- Create transitions and keyframe animations, preferring
transformandopacity. - Respect
prefers-reduced-motion. - Diagnose common layout and cross-browser assumptions.
1. The cascade
When several rules target the same property on the same element, the browser picks one using three criteria, in order:
- Origin and importance. Browser defaults < author styles < author
!important< user!important. (!importantin your stylesheet beats normal rules, which is why it spreads.) - Specificity. More specific selectors win.
- Source order. If specificity is equal, the later rule wins.
Calculating specificity
Think of specificity as three counters, compared left to right:
| Selector part | Counter |
|---|---|
Inline style="..." |
Beats all selectors below (not !important) |
ID selector #nav |
(1, 0, 0) |
Class .btn, attribute [type="text"], pseudo-class :hover |
(0, 1, 0) |
Element a, pseudo-element ::before |
(0, 0, 1) |
Universal *, combinators, :where() |
(0, 0, 0) |
Examples:
a { color: blue; } /* (0,0,1) */
.link { color: green; } /* (0,1,0) */
nav a { color: red; } /* (0,0,2) */
nav .link { color: orange; } /* (0,1,1) */
#menu .link { color: purple; } /* (1,1,0) */
For <nav id="menu"><a class="link">, the purple rule wins because one ID outweighs any number of classes. Counters are not a base-10 number: eleven classes (0,11,0) never beat one ID (1,0,0).
:is() takes the specificity of its most specific argument; :where() always contributes zero, which is useful for low-specificity defaults. :not() takes the specificity of its argument.
Practical rules
- Style with classes. Class selectors are specific enough to be predictable and low enough to override.
- Avoid IDs for styling (use them for anchors and label associations).
- Avoid long descendant chains such as
body div.page main ul li a. They are brittle and hard to override. - Avoid
!importantexcept for utilities intended to always win (for example a.visually-hiddenhelper). - Order your stylesheet from generic to specific: reset, base elements, layout, components, utilities.
2. Inheritance and custom properties
Some properties are inherited from parent to child: color, font-family, font-size, line-height, visibility. Others are not: margin, padding, border, background, width. That is why setting font-family on body styles the whole page but setting border on a div does not border its children.
Keywords control this: inherit (take the parent's value), initial (the CSS default), unset, and revert. A common use: button { font: inherit; } because buttons do not inherit fonts by default.
Custom properties
:root {
--color-primary: #0b5cad;
--color-primary-dark: #084480;
--color-text: #1b2733;
--radius: 0.5rem;
--space-2: 0.5rem;
--space-4: 1rem;
}
.btn {
background: var(--color-primary);
border-radius: var(--radius);
padding: var(--space-2) var(--space-4);
}
Custom properties cascade and inherit, so you can redefine them in a component or in a theme:
@media (prefers-color-scheme: dark) {
:root { --color-text: #e8eef4; --color-bg: #10171e; }
}
.btn--danger { --color-primary: #a4262c; --color-primary-dark: #7a1c21; }
var(--x, fallback) supplies a default when the variable is undefined. Unlike Sass variables, custom properties exist at runtime, so JavaScript and media queries can change them.
3. Reusable class patterns
A maintainable stylesheet is a small vocabulary of reusable pieces. One widely used convention is BEM (Block, Element, Modifier):
- Block: a standalone component:
.card - Element: a part of it:
.card__title - Modifier: a variant or state:
.card--featured
<article class="card card--featured">
<h3 class="card__title">Pottery for Beginners</h3>
<a class="btn btn--primary" href="/book/pottery">Book seats</a>
</article>
Each selector is a single class, so specificity stays flat at (0,1,0) and order alone decides conflicts. Benefits: no dependence on HTML structure, easy deletion of unused components, and names that explain intent.
Guidelines:
- Name by purpose, not appearance:
.btn--danger, not.btn--red. - Prefer composition (
class="btn btn--primary btn--large") over one class per combination. - Keep component CSS free of page layout (margins that place the card belong to the layout container).
- In React projects the same idea appears as components with their own styles, which you will meet in chapter 5.
4. Interaction states
Pseudo-classes
A pseudo-class selects an element in a particular state:
| Pseudo-class | Matches |
|---|---|
:hover |
Pointer over the element |
:focus |
Element has keyboard focus (any method) |
:focus-visible |
Focus that the browser decides should show an indicator (typically keyboard) |
:focus-within |
A container with a focused descendant |
:active |
Being pressed |
:disabled / :enabled |
Form control state |
:checked |
Selected checkbox or radio |
:invalid / :valid |
Constraint validation result |
:required / :optional |
Presence of required |
:first-child, :nth-child(2n), :not() |
Structural selection |
:target |
The element named by the URL fragment |
Order for links and buttons
If you style all of them, remember the order LVHA: :link, :visited, :hover, :active. Because these have equal specificity, later rules win, so :hover must come after :link and :active after :hover.
Focus, the most important state
Keyboard users navigate by focus. If you cannot see where focus is, you cannot use the page. Never write outline: none without a replacement.
.btn:focus-visible {
outline: 3px solid #ffbf47;
outline-offset: 2px;
}
:focus-visible shows the indicator for keyboard focus but usually not for mouse clicks on buttons, so designers keep a clean look without harming accessibility. The WCAG 2.2 guidance expects a focus indicator that is clearly visible with sufficient contrast against adjacent colors (see the WCAG "Focus Appearance" and "Non-text Contrast" criteria, and use an indicator at least as thick as a 2 CSS pixel perimeter as a practical target). Verify by pressing Tab through the page.
Hover is not enough
Touch devices have no hover. Never hide essential information or controls behind hover alone, and give focus the same treatment as hover:
.btn:hover,
.btn:focus-visible { background: var(--color-primary-dark); }
@media (hover: hover) {
.card:hover { box-shadow: 0 6px 16px rgb(0 0 0 / 15%); }
}
Disabled and invalid
.btn:disabled,
.btn[aria-disabled="true"] {
background: #c9d1d9;
color: #4b5560;
cursor: not-allowed;
}
input:user-invalid { border-color: #a4262c; }
:user-invalid only matches after the user has interacted with the field, unlike :invalid, which can flag an empty required field before anyone typed. Check current browser support on MDN before relying on it, and keep :invalid plus a class set by script as a fallback.
State must never be communicated by color alone. Pair the red border with error text (lesson 1) and an icon or a message.
5. Transitions and animation
Transitions
A transition animates a property change between two states:
.btn {
background: var(--color-primary);
transform: translateY(0);
transition: background-color 150ms ease, transform 150ms ease;
}
.btn:hover { transform: translateY(-1px); }
Specify properties explicitly. transition: all animates things you did not intend, and can animate expensive layout properties.
Keyframe animations
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.badge--new {
animation: pulse 1.2s ease-in-out 3; /* name, duration, timing, repeat count */
}
Animation properties: animation-name, -duration, -timing-function, -delay, -iteration-count (infinite for endless), -direction, -fill-mode.
Performance: animate cheap properties
Browsers can animate transform and opacity on the compositor without recalculating layout. Animating width, height, top, left, margin or box-shadow forces layout or paint every frame and can stutter on low-end phones. To move something, use transform: translateX(); to fade, use opacity.
Restraint and purpose
Motion should communicate: confirm that a click worked, show where a panel came from, draw attention to an important change. Keep durations short (roughly 100 to 300 ms for interface feedback). Avoid autoplaying, looping, or flashing content. Content that flashes more than three times per second can trigger seizures.
Reduced motion
Some users, particularly those with vestibular disorders, set their operating system to reduce motion. CSS exposes this preference:
@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;
}
}
A more careful approach is to wrap decorative animation in @media (prefers-reduced-motion: no-preference) so that it only runs for users who have not asked for less, and to keep essential state changes (a color change) that do not involve movement. MDN lists prefers-reduced-motion as widely available across browsers.
6. Debugging layout and cross-browser assumptions
Typical sources of "it looks different on another browser":
- Missing reset. Browsers have different default margins and form styles. Use a small reset (
box-sizing,margin: 0on body,font: inheriton form controls). - Assuming a feature exists. Check MDN's browser compatibility table and Baseline status before relying on new CSS. Use
@supportsfor progressive enhancement:
.event-grid { display: flex; flex-wrap: wrap; gap: 1rem; }
@supports (display: grid) {
.event-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }
}
- Margin collapse. Vertical margins of adjacent block elements combine to the larger one, not the sum. Flex and grid items do not collapse.
- Unexpected stacking.
z-indexonly works on positioned elements (or flex/grid items), and it is scoped to a stacking context. A hugez-indexinside a context cannot escape it. - Specificity surprises. A rule seems ignored: open DevTools and look for it crossed out (next lesson).
- Different form control rendering. Style with
appearance, but leave native controls alone when unsure; they are accessible by default.
Write standard CSS; avoid vendor prefixes unless a tool adds them for you. Test on at least one Chromium-based browser, Firefox, and Safari (or WebKit), and on a real phone when possible.
Worked example: a reusable button system
Brief. Create buttons for Harbor Workshops: a primary action ("Book seats"), a secondary one ("Details"), and a danger action ("Cancel booking"). They must work on <a> and <button>, show clear hover, focus and disabled states, and animate calmly.
Step 1: tokens. Define colors, radius and spacing as custom properties (see above). Check contrast: white text on #0b5cad has a ratio above 4.5:1, which meets the WCAG AA text requirement.
Step 2: the base class. Reset the differences between <a> and <button>.
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
border: 2px solid transparent;
border-radius: var(--radius);
font: inherit;
font-weight: 600;
line-height: 1.2;
text-decoration: none;
cursor: pointer;
background: var(--btn-bg, var(--color-primary));
color: var(--btn-fg, #fff);
transition: background-color 150ms ease, transform 150ms ease;
}
font: inherit aligns <button> text with <a> text. --btn-bg and --btn-fg are hooks that modifiers override with a fallback to the primary colors.
Step 3: modifiers. Only change the hooks.
.btn--secondary { --btn-bg: transparent; --btn-fg: var(--color-primary); border-color: currentColor; }
.btn--danger { --btn-bg: #a4262c; }
Step 4: states.
.btn:hover { filter: brightness(0.92); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid #ffbf47; outline-offset: 2px; }
.btn:disabled, .btn[aria-disabled="true"] {
opacity: 0.55;
cursor: not-allowed;
filter: none;
}
Step 5: reduced motion.
@media (prefers-reduced-motion: reduce) {
.btn { transition: none; }
.btn:active { transform: none; }
}
Step 6: use.
<a class="btn" href="/book/pottery">Book seats</a>
<a class="btn btn--secondary" href="/workshops/pottery">Details</a>
<button class="btn btn--danger" type="button">Cancel booking</button>
<button class="btn" type="submit" disabled>Book seats</button>
Why this design? Single-class selectors keep specificity flat, so a modifier or a page-level override with one class and later source order wins without !important. Custom properties make variants small. The focus ring uses :focus-visible with a high-contrast color, and the reduced-motion block removes movement. Alternatives are worse: an ID-based rule such as #book would be impossible to override with classes; outline: none would hide keyboard focus; transition: all would animate unrelated properties; and a div with a click handler would lose keyboard support and button semantics.
Common pitfalls
- Fighting specificity with
!importantor deeper selectors, instead of lowering specificity. - Using IDs and long selector chains that cannot be overridden cleanly.
- Removing the focus outline with no replacement.
- Hover-only interactions that are impossible on touch devices.
- Putting
:hoverbefore:link/:visitedso hover styling never applies. - Animating
width,top, ormarginand causing jank; usetransformandopacity. transition: allhiding accidental animation.- Ignoring reduced-motion settings.
- Naming classes by appearance (
.red-text) so a redesign makes names lie. - Expecting
marginorborderto inherit. - Relying on color alone to show an error or selected state.
Exam tip: For "which rule wins?" questions, compare in order:
!important, inline, IDs, classes/attributes/pseudo-classes, elements, then source order. Count each category separately; never add them across columns. For accessibility questions, the correct answer almost always keeps a visible focus indicator, supports keyboard use, and honorsprefers-reduced-motion.
Mini case study: the button that will not change
Situation. A teammate wants the "Cancel booking" button to be dark red, but it stays blue. The CSS is:
.btn { background: #0b5cad; } /* in components.css */
.btn--danger { background: #a4262c; } /* in components.css, but loaded earlier */
.booking-panel button { background: #0b5cad; }/* in layout.css */
HTML: <div class="booking-panel"><button class="btn btn--danger">Cancel booking</button></div>
The teammate suggests adding !important to .btn--danger.
Questions. (1) Why is the button blue? (2) Is !important the right fix? (3) What is the fix?
Solution.
- Specificity:
.booking-panel buttonis (0,1,1) (one class, one element)..btn--dangeris (0,1,0). The layout rule is more specific and wins regardless of source order. !importantwould work but is a trap: the next change will need another!important, and the real problem (a layout file styling a component's color) stays.- Remove the color rule from the layout file, since layout CSS should position, not theme, components. If the rule must stay, make it equal and order it correctly:
.booking-panel .btn { }is (0,2,0) and would beat.btn--dangeragain, so it should not setbackgroundat all. After removing it,.btn--danger(0,1,0) and.btn(0,1,0) have equal specificity and source order decides, so ensure the modifier comes after the base class in the file.
Verify in DevTools: select the button, find the crossed-out background declarations in the Styles pane, and confirm that .btn--danger is the one applied.
Key terms
- Cascade: the algorithm that resolves conflicting declarations.
- Specificity: a weight computed from a selector's parts.
- Inheritance: passing some property values from parent to child.
- Custom property: a
--namevariable read withvar(). - BEM: Block, Element, Modifier naming convention.
- Pseudo-class: a selector keyword for an element's state.
:focus-visible: focus styling shown when the browser judges it helpful, typically for keyboard.- Transition: an animation between two states of a property.
- Keyframes: the named steps of an animation.
- Compositor: the browser stage that can animate
transformandopacitycheaply. prefers-reduced-motion: a media feature reporting the user's motion preference.
Summary checklist
- I can compute specificity and predict which of two rules wins.
- I style with flat, single-class selectors and avoid IDs and
!important. - I know which properties inherit and use
inheritand custom properties deliberately. - I name components with a consistent convention such as BEM and compose modifiers.
- I style hover,
:focus-visible, active, and disabled states for every interactive component. - I never remove focus outlines without an equally visible replacement.
- I do not hide essential content behind hover.
- I animate
transformandopacity, keep durations short, and avoidtransition: all. - I support
prefers-reduced-motion. - I use
@supportsand compatibility tables for newer features. - I debug "ignored" rules by looking for specificity and order problems first.
Official sources
- Meta Front-End Developer Professional Certificate (opens in a new tab)
- Course 4: HTML and CSS in Depth (opens in a new tab)
- MDN: Cascade and inheritance (opens in a new tab)
- MDN: Specificity (opens in a new tab)
- MDN: prefers-reduced-motion (opens in a new tab)
- MDN: CSS transitions (opens in a new tab)
- W3C WCAG 2.2: Focus Visible (opens in a new tab)
