CSS Cascade, Accessible States, and Reduced Motion
Introduction
Apply these component styles within responsive Flexbox and Grid layouts. This third lesson of chapter 4 is 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
- Resolve CSS cascade conflicts using origin, importance, layers, specificity, scope, and source order.
- 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 declarations compete, first discard rules whose selectors or media conditions do not apply. Then resolve origin/importance and cascade layers, specificity, scoping proximity for @scope, and finally source order. Specificity is not the first test.
For ordinary declarations in one author layer with no inline styles, animations, transitions, or scope differences, the more specific selector wins; equal specificity falls back to later source order. This is the setting used by the examples below.
Across origins, normal author styles outrank normal user and browser styles. Important declarations reverse that origin order. Keyframe animations outrank normal declarations; active transitions outrank even important declarations. Within an origin, normal unlayered styles outrank normal layered styles, and later layers beat earlier layers. Important layer priority reverses. Inline author styles also have special priority; an ordinary inline declaration beats normal author stylesheet rules.
Declare layers deliberately, for example @layer reset, components, utilities;, rather than using !important to hide a priority problem. Inspect the full cascade in DevTools before counting selectors.
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() and :has() likewise use the most specific selector in their argument list.
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 property's specification default), unset (inherit inherited properties, otherwise initial), and revert (roll back this origin's contribution). revert-layer rolls back the current cascade layer. 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);
}Unregistered custom properties cascade and inherit by default (@property can configure inheritance), 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 fallback when the custom property is missing or guaranteed-invalid. It does not rescue a defined value that is invalid for the consuming property: --x: red; width: var(--x, 10px) does not become 10px. 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 decides conflicts when origin, importance, layer, and scope also match. 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 focus, whether reached by keyboard, pointer, or script |
: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 #0b5cad;
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. WCAG 2.2 Focus Visible is Level AA; Non-text Contrast (AA) applies to relevant authored component/state visuals, with exceptions. Focus Appearance is Level AAA and adds a 2 CSS pixel perimeter-equivalent area and 3:1 focused/unfocused pixel contrast. These are different requirements. A gold outline on white can have insufficient contrast; verify every background, forced-colors mode, and clipping or obstruction.
Hover is not enough
Do not assume a touch device supports hover; hybrid devices may also have a mouse. 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 uses browser-defined interaction heuristics to decide when to show invalid state, unlike :invalid, which can flag an empty required field before anyone typed. Check current browser support on MDN before relying on it, and use a script-set error class as a fallback when needed; an unconditional :invalid rule reintroduces premature error styling.
State must never be communicated by color alone. Use accessible field errors and validation to pair the red border with persistent explanatory text.
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
transform and opacity are usually good candidates for compositing without layout on each frame, but promotion and performance are not guaranteed. Width, height, offsets, and margins can require layout; shadows commonly require paint. Profile the actual effect rather than assuming a property is free. 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. Avoid flashing effects; short duration does not make flashing safe.
Reduced motion
Some users request less motion through their system or browser preferences. Start with static presentation and enable decorative motion only when no reduction is requested:
/* Static by default; state changes remain usable without movement. */
@media (prefers-reduced-motion: no-preference) {
.btn { transition: background-color 150ms ease, transform 150ms ease; }
.btn:not(:disabled):not([aria-disabled="true"]):hover {
transform: translateY(-1px);
}
.btn:not(:disabled):not([aria-disabled="true"]):active {
transform: translateY(1px);
}
.badge--new { animation: pulse 1.2s ease-in-out 3; }
}When combining the examples, replace the earlier standalone transition, hover-transform, and .badge--new animation declarations with this gated version. A global near-zero-duration override can break components that rely on animation timing and still create abrupt motion. Scope reductions to the components you own, preserve essential feedback, and test both preference settings.
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. Eligible vertical block margins can collapse: positive margins use the largest, mixed signs combine the largest positive with the most negative, and all-negative margins use the most negative. Margins of flex and grid items do not collapse.
- Unexpected stacking.
z-indexgenerally controls positioned elements and flex/grid items within their stacking context; transforms, opacity, and other features can create contexts too. Top-layer content such as an open modal dialog is outside ordinary stacking. 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; native semantics help, but labels, contrast, and keyboard behavior still need review.
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 above. White on #0b5cad exceeds the 4.5:1 normal-text target. The examples assume a light page background; a dark theme needs separately checked text, borders, and focus tokens.
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);
}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:not(:disabled):not([aria-disabled="true"]):hover { filter: brightness(0.92); }
.btn:focus-visible { outline: 3px solid #0b5cad; outline-offset: 2px; }
.btn:disabled, .btn[aria-disabled="true"] {
opacity: 0.55;
cursor: not-allowed;
filter: none;
}Step 5: optional motion.
@media (prefers-reduced-motion: no-preference) {
.btn { transition: background-color 150ms ease, transform 150ms ease; }
.btn:not(:disabled):not([aria-disabled="true"]):active {
transform: translateY(1px);
}
}Use native disabled on unavailable buttons. aria-disabled="true" alone only announces a state: it does not prevent clicks, keyboard activation, or link navigation. Styling it does not implement disabled behavior. This example uses native disabled buttons and does not offer disabled links. Disabled controls are normally omitted from Tab order; explain why an action is unavailable in nearby text.
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 base and modifier selectors keep their specificity equal. With other cascade priorities tied, a later modifier wins without !important; state selectors can be more specific. Custom properties make variants small. The focus ring uses :focus-visible with a high-contrast color, and motion is enabled only when no reduction is requested. Alternatives are worse: an ID-based rule such as #book would defeat class-only rules of equal origin, importance, and layer; 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.
Study tip: Resolve origin, importance, and layers before comparing specificity. When those priorities and scope tie, compare IDs, classes/attributes/pseudo-classes, then elements; source order breaks the remaining tie. Verify focus, keyboard operation, and reduced motion through observable behavior.
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. Changing it to
.booking-panel .btnwould increase specificity to (0,2,0), making the problem worse; it should not set the component background. 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)
- MDN: CSS cascade priorities and layers (opens in a new tab)
- MDN: aria-disabled behavior (opens in a new tab)
- W3C: Focus Appearance (AAA) (opens in a new tab)
