skip to content

Custom Properties and Animations

The dynamic layer of CSS: variables that cascade and resolve at runtime, and the transition/animation engine that moves things. Interviewers pair them because both are how you build theming and motion without JavaScript, and both carry real performance and accessibility consequences.

part ofCSSoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In CSS, how do you declare a custom property and use it in a value with var(), and what does the second argument in var(--name, fallback) do?

level: juniorimportance: must knowfreq 72%

answer

  1. two dashes make it a property
  2. cascades and inherits like color
  3. comma introduces the fallback
  4. everything after first comma counts
  5. covers missing, not wrong

basics

~20 s

Declare a custom property as a normal declaration whose name starts with two dashes, such as --gap: 12px, and read it back with var(--gap). The optional second argument, var(--gap, 8px), applies only when --gap is not defined.

solid answer

~50 s

A custom property is any declaration whose name begins with two dashes: `.card { --gap: 12px; }`. It is a real CSS property, so it takes part in the cascade and it inherits by default, which means every descendant of `.card` sees `--gap`. You consume it with `var(--gap)` anywhere a value is allowed, including inside `calc()` and inside shorthands: `padding: var(--gap)`. Names are case-sensitive, so `--Gap` and `--gap` are two different properties. `var()` accepts an optional fallback after the first comma — `var(--gap, 8px)` resolves to `8px` when `--gap` has not been set on that element — and everything after that first comma counts as the fallback, so `var(--font, Georgia, serif)` is legal. The fallback covers the *missing* case only, not the case where the property exists but holds something the target property cannot use.

code

css · 18 lines
css
:root {
  --gap: 12px;
  --accent: #1a73e8;
}

.card {
  padding: var(--gap);
  border: 1px solid var(--accent);
}

.card--tight {
  --gap: 4px;
}

.sidebar {
  /* --gap is not set here, so the fallback applies */
  margin: var(--sidebar-gap, 16px);
}

go deeper

for a junior

Be ready to write the syntax from memory: --gap: 12px to declare, var(--gap) to use, var(--gap, 8px) for a fallback when the property is missing. Say plainly that the name needs both dashes.

for a middle

Explain that a custom property is an ordinary inherited property that cascades, so where you declare it decides which elements see it, and that everything after the first comma in var() is the fallback.

for a senior

Show the failure modes you have debugged: a fallback that never fires because the property is defined but wrong, a bare number that will not concatenate with a unit, and a case-mismatched name that silently resolves to nothing.

for a principal

Own the convention: where defaults live, whether values carry units, and how deep var() chains are allowed to nest before a stylesheet becomes untraceable for the next reader.

## What a custom property actually is A custom property is a CSS declaration whose property name starts with two hyphens, for example `--gap: 12px`. The standard properties are a fixed list defined by the specification; custom property names are yours to invent. The browser does not know or care what `--gap` means — it stores the declared value as a stream of tokens and hands that stream back wherever you ask for it. The important consequence is that this is a *property*, not a special build-time construct. It lives inside a rule with a selector, it competes in the cascade like any other declaration (later wins over earlier at equal specificity, higher specificity wins, `!important` wins), and it **inherits by default**, flowing down to descendants the way `color` does. ## Declaring one ```css :root { --gap: 12px; --brand: #1a73e8; } .card { --gap: 8px; /* applies to .card and everything inside it */ } ``` `:root` matches the document's root element, which is where people usually park global defaults. Nothing forces that: you can declare a custom property on any selector, and even on a single element through its `style` attribute. Two details bite beginners. First, names are **case-sensitive** — `--Brand` and `--brand` are unrelated properties, unlike `COLOR` and `color`. Second, the declared value is almost unrestricted: `--x: 1px solid;` and even `--x: hello world;` parse fine, because validity is not checked until something tries to use the value. ## Reading it back with var() ```css .card { padding: var(--gap); border: 1px solid var(--brand); width: calc(100% - var(--gap) * 2); } ``` `var()` can appear anywhere a value, or part of a value, is allowed. It can sit inside `calc()`, inside a shorthand, and inside the value of another custom property, which is how layered token sets are built: ```css :root { --space: 4px; --gap-lg: calc(var(--space) * 4); } ``` Three things `var()` cannot do, and interviewers like all three: - It cannot build a **property name**. `var(--prop): red` is not valid CSS. - It cannot appear in a **selector**. - It cannot appear in a **media query condition**; `@media (min-width: var(--bp))` does not work in browsers today. ## The fallback argument `var()` takes an optional second argument after the first comma: ```css .card { padding: var(--gap, 8px); } ``` If `--gap` is not defined on the element being styled, the substitution uses `8px`. "Not defined" is precise: the property has never been set, or was explicitly reset, so it holds the guaranteed-invalid value. Everything after the *first* comma is the fallback, commas included, which is why font stacks work: ```css font-family: var(--font, Georgia, "Times New Roman", serif); ``` Fallbacks nest, so you can chain preferences: `var(--a, var(--b, 4px))` tries `--a`, then `--b`, then `4px`. ## What the fallback does not do The fallback only covers the missing case. If `--gap` **is** defined but holds something the target property cannot accept — say `--gap: red` used in `padding: var(--gap, 8px)` — you do not get `8px`. The substitution happens, the result is nonsense for `padding`, and the declaration becomes invalid at computed-value time, which is a different and more surprising failure mode than a missing property. ## Tokens are spliced, not glued A classic trap: ```css :root { --size: 10; } .box { width: var(--size)px; } /* does not work */ .box { width: calc(var(--size) * 1px); } /* works */ ``` Substitution inserts the stored tokens next to the surrounding tokens; it does not concatenate text into a new dimension token, so `10` and `px` never become `10px`. Multiply by a unit inside `calc()` instead. This is also why storing unit-bearing values (`--size: 10px`) is usually simpler than storing bare numbers. ## The shape you will actually write Global defaults on `:root`, local overrides on the component or a variant class, and a single consuming rule that never has to change: ```css :root { --accent: #1a73e8; } .button { background: var(--accent); } .button--danger { --accent: #c5221f; } ``` That one-declaration-many-values shape is the whole point of custom properties, and it is only possible because they cascade and inherit like real properties.

  • If --size is 10, why does width: var(--size)px not produce a 10px width?
    Because substitution splices token streams rather than concatenating text: the `10` and the `px` never fuse into a single dimension token, so the value is not a valid length and the declaration is dropped at computed-value time. Multiply by a unit instead: `width: calc(var(--size) * 1px)`. Storing the unit in the property, `--size: 10px`, avoids the problem entirely.
  • Can you use var() to choose which property you are setting, like var(--prop): red?
    No. `var()` substitutes into values only, never into property names, selectors, or at-rule preludes. A custom property's value is a token stream the browser splices into a value position; property names are parsed before any substitution happens. If you need to switch which property changes, switch the rule or the class, not the property name.
  • Are custom property names case-sensitive?
    Yes, unlike standard property names. `--Brand` and `--brand` are two independent properties, and `var(--Brand)` will not find a value declared as `--brand`. This is a common source of silent failures because the result is not an error — the property is simply missing, so you get the `var()` fallback if you supplied one, and invalid-at-computed-value-time behaviour if you did not.

saying these in an interview costs you the question

  • Thinks the var() fallback also rescues invalid defined values
  • Writes var(gap) or var(-gap) instead of var(--gap)
  • Assumes --Gap and --gap are the same property
  • Expects var(--n)px to concatenate into a length
  • Believes var() works inside a media query condition

context

open as a page

In CSS, what can a @keyframes animation express that a transition cannot, and what starts each of them?

level: juniorimportance: must knowfreq 78%

basics

~20 s

A CSS transition interpolates between two states and only runs when a value actually changes, typically on :hover or a class toggle. A @keyframes animation declares many intermediate stops, starts on its own, and can loop, alternate direction, and be paused.

open as a page

In CSS, what does the declaration `transition: opacity 300ms ease-in 100ms;` set, and how does the browser decide which of the two time values is the duration and which is the delay?

level: juniorimportance: must knowfreq 78%

basics

~20 s

It animates only opacity over 300ms with an ease-in curve, starting after a 100ms delay. In the transition shorthand the components are order-free by type, except that the first time value is always the duration and the second is the delay.

open as a page

How do CSS custom properties differ from Sass variables, and what can you do with one that you cannot do with the other?

level: middleimportance: must knowfreq 66%

basics

~20 s

CSS custom properties are real values in the cascade: they inherit, can be redefined per selector or per element, and the browser resolves them at computed-value time. Sass variables are compile-time text that never reaches the browser.

open as a page

In CSS, what does animation-fill-mode do, and what are the practical differences between none, forwards, backwards, and both?

level: middleimportance: must knowfreq 62%

basics

~20 s

animation-fill-mode decides whether keyframe values apply outside the animation's active run. The default none applies nothing before or after; forwards holds the final keyframe after it ends; backwards applies the first keyframe during the delay; both does each.

open as a page

In CSS, what is the difference between putting motion behind @media (prefers-reduced-motion: no-preference) and declaring it normally then stripping it inside @media (prefers-reduced-motion: reduce), and what does each pattern get wrong?

level: middleimportance: must knowfreq 55%

basics

~20 s

Opt-in puts animation inside a no-preference query, so anything you forget simply never moves. Opt-out ships motion to everyone and removes it in a reduce query, so any animation you forget to undo still plays for people who asked for less.

open as a page

In CSS, what does animation-timeline: scroll() do to an animation, and how does that differ from the default document timeline?

level: middleimportance: must knowfreq 42%

basics

~20 s

animation-timeline: scroll() drives an animation by a scroll container's scroll position instead of by elapsed time. Progress 0% is scrolled fully to the start, 100% fully to the end, and scrolling back runs the animation backwards.

open as a page

In CSS, why are `transform` and `opacity` recommended as animation targets instead of `left`, `top`, or `width`?

level: middleimportance: must knowfreq 72%

basics

~20 s

Animating transform and opacity changes neither an element's layout box nor the pixels painted inside it, so engines can run those animations on the compositor. Animating left, top, or width re-runs layout on every frame.

open as a page

Why does a CSS transition on `height` do nothing when the target value is `height: auto`, and which techniques actually produce a smooth open and close?

level: middleimportance: must knowfreq 64%

basics

~20 s

A transition interpolates between two computed values of the same type, and auto is a keyword, not a length, so there is nothing to interpolate and the height jumps. Common fixes are animating max-height, animating a wrapper's grid-template-rows from 0fr to 1fr, or opting keywords in with interpolate-size.

open as a page

In CSS, where does the prefers-reduced-motion media feature get its value from, what are its two possible values, and what does the bare query @media (prefers-reduced-motion) match?

level: juniorimportance: should knowfreq 45%

basics

~20 s

The browser reads an operating-system accessibility setting and exposes it to CSS as one of two values: reduce when the user asked for less motion, and no-preference otherwise. The bare query without a value matches only reduce, because no-preference evaluates as false in boolean context.

open as a page

In CSS, what does the percentage in `transform: translate(-50%, -50%)` resolve against, and why does that make it a centering idiom?

level: juniorimportance: should knowfreq 52%

basics

~20 s

Percentages in translate() resolve against the element's own border box — X against its own width, Y against its own height. That lets an element pull itself back by half its own size without anyone knowing that size in advance.

open as a page

In CSS, a single rule .card { border-color: var(--accent); } renders a different border color on each card. How can one declaration produce different values, and on which element is var(--accent) resolved?

level: middleimportance: should knowfreq 52%

basics

~20 s

Custom properties are inherited properties resolved separately for every element. var(--accent) is substituted with whatever --accent computes to on the element being styled, so each card that inherits or declares a different --accent gets a different border color from the same rule.

open as a page

Which events does a CSS keyframe animation dispatch, and when does animationend fail to fire?

level: middleimportance: should knowfreq 36%

basics

~20 s

A CSS animation dispatches animationstart, animationiteration between iterations, animationend on completion, and animationcancel if it stops early. animationend never fires when animation-iteration-count is infinite, or when the animation is removed or the element stops being rendered.

open as a page

In a CSS @keyframes rule, what supplies the starting value if you only declare a to stop, and what happens to a property that appears in some keyframes but not others?

level: middleimportance: should knowfreq 42%

basics

~20 s

The browser synthesises the missing 0% or 100% stop from the element's own computed value for each animated property. A property declared in only some keyframes interpolates between the stops that do declare it, using that computed value as the implicit endpoint.

open as a page

In the CSS animation shorthand, how are two time values interpreted, and what happens to the longhands you leave out?

level: middleimportance: should knowfreq 46%

basics

~20 s

In the animation shorthand the first time value is animation-duration and the second is animation-delay, regardless of their size. Every longhand you omit is reset to its initial value, so a shorthand written later silently wipes an earlier animation-fill-mode or animation-play-state.

open as a page

When @media (prefers-reduced-motion: reduce) matches, which motion in an interface should actually be removed or substituted, and which motion is reasonable to keep?

level: middleimportance: should knowfreq 40%

basics

~20 s

Remove large-area movement — long translations, zooms, parallax, spinning or bouncing content — because that is what triggers vestibular symptoms. Keep small, localised, essential motion such as a progress indicator, and substitute a short opacity crossfade or an instant state change where the animation was carrying feedback.

open as a page

In CSS scroll-driven animations, what is the difference between the scroll() and view() values of animation-timeline, and when would you pick each?

level: middleimportance: should knowfreq 34%

basics

~20 s

scroll() tracks how far a scroll container has been scrolled, so progress is the same for every element using it. view() tracks one specific element's journey through the scrollport, so each element progresses on its own schedule.

open as a page

In CSS, why does `transform: translateX(100px) rotate(45deg)` place an element differently from `transform: rotate(45deg) translateX(100px)`?

level: middleimportance: should knowfreq 50%

basics

~10 s

Transform functions apply left to right, each operating in the coordinate system the previous one left behind. Rotating first tilts the axes, so the following translateX moves along a diagonal instead of straight right.

open as a page

In CSS, what do the four numbers in `cubic-bezier(0.68, -0.55, 0.27, 1.55)` represent, and why can a transition using it overshoot its final value?

level: middleimportance: should knowfreq 44%

basics

~20 s

They are the x and y coordinates of the two control points of a cubic Bezier curve whose endpoints are fixed at (0,0) and (1,1), mapping elapsed time to progress. The x values must stay within 0 to 1, but y may go below 0 or above 1, which produces anticipation and overshoot.

open as a page

In CSS transitions, what does the `transitionend` event fire for on an element that transitions several properties at once, and name two situations where it never fires at all.

level: middleimportance: should knowfreq 42%

basics

~20 s

transitionend fires once per transitioned longhand property, not once per element, and it bubbles — so a three-property transition dispatches three events, distinguished by the event's propertyName. It never fires when no transition was generated (zero duration, or no computed-value change) or when the transition is cancelled, which dispatches transitioncancel instead.

open as a page

A CSS rule contains color: black; color: var(--brand); and --brand holds the value 12px. The text renders in the color inherited from its parent, not black. Why does the earlier declaration not win?

level: seniorimportance: should knowfreq 42%

basics

~20 s

var() is checked when values compute, not when CSS parses. The var() declaration looks valid at parse time, so it wins the cascade and discards color: black; substituting 12px then makes it invalid at computed-value time, so color falls back to the inherited value.

open as a page

Removing a CSS animation class from an element and immediately re-adding it in the same script task does not replay the animation. Why, and what actually restarts it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A keyframe animation is created when an element's computed animation-name gains a value and destroyed when it loses one. Remove and re-add in one task and the computed value is identical at the next style recalculation, so the browser sees no change and no new animation starts.

open as a page

A stylesheet handles reduced motion with a universal rule that sets animation-duration and transition-duration to 0.01ms !important instead of using animation: none. Why the near-zero duration rather than none, and what does that blanket reset still get wrong?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A near-zero duration still runs the animation, so animationend and transitionend fire and script that waits on them keeps working; animation: none suppresses them and can leave such code stalled forever. The reset is a safety floor, not a design: it flattens motion instead of substituting something gentler.

open as a page

What does the CSS animation-range property control on a scroll-driven animation, and what do named ranges like entry and cover mean?

level: seniorimportance: should knowfreq 24%

basics

~20 s

animation-range picks which slice of a progress timeline the animation occupies. On a view timeline you express the endpoints with named ranges — cover, contain, entry, exit — plus optional percentage offsets, instead of using a time-based duration.

open as a page

In CSS, how do scroll-timeline-name and timeline-scope let an element be animated by a scroll container that is not one of its ancestors?

level: seniorimportance: should knowfreq 20%

basics

~20 s

scroll-timeline-name gives a scroller's timeline a --dashed-ident that descendants can reference from animation-timeline. When the animated element is not a descendant, timeline-scope on a common ancestor hoists the name so both sides can see it.

open as a page

What does the CSS `will-change` property do, and why can putting `will-change: transform` on many elements make a page slower?

level: seniorimportance: should knowfreq 42%

basics

~20 s

will-change tells the browser which properties are about to change so it can prepare, usually by giving the element its own composited layer. Each layer costs memory, so applying it broadly wastes resources and slows the page.

open as a page

A CSS class toggle takes an element from `display: none; opacity: 0` to `display: block; opacity: 1` with `transition: opacity 300ms`. Why is there no fade-in, why does the fade-out also fail, and which CSS fixes each?

level: seniorimportance: should knowfreq 32%

basics

~20 s

There is no fade-in because an element leaving display: none is rendered for the first time and has no before-change value to transition from — @starting-style supplies one. The fade-out fails because display is a discrete property that flips immediately, hiding the element; transition-behavior: allow-discrete defers that flip to the end.

open as a page

How would you ship a scroll-driven CSS animation on a production site knowing some visitors' browsers do not support animation-timeline?

level: principalimportance: should knowfreq 26%

basics

~10 s

Author the finished, static state as the base style and layer the animation inside @supports (animation-timeline: view()). Unsupported browsers get the correct end state with no motion, and no fallback script is needed.

open as a page

In CSS, what is the difference between setting `perspective` on a parent element and using the `perspective()` function inside a child's own `transform`?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

The perspective property on a parent gives all its children one shared vanishing point, so they read as objects in a single scene. The perspective() function inside a child's own transform gives that child its own vanishing point, projected from its own origin.

open as a page

What does registering a custom property with the CSS @property at-rule give you that a plain --name: declaration does not?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

@property registers a custom property with a syntax type, an inherits flag and an initial-value. That buys type checking, a real fallback instead of an unset value, the ability to opt out of inheritance, and values the browser can interpolate.

open as a page

showing 1–30 of 31