Engrain by ByteRush

WEB.08.03 Mid-level· 11 minCore — asked often

Custom properties and modern selectors: var(), @property, :is, :where, :has and nesting

How CSS custom properties inherit and resolve at use time, fallbacks, typed properties with @property, the difference between :is() and :where(), what :has() makes possible, and native CSS nesting.

Comes up in: Web fundamentals

Learn#

Rekha’s design system has 14 button classes: blue, red, dark blue, dark red, and so on. Adding dark mode means 14 more. With custom properties, one button rule can follow the theme.

A custom property is a CSS variable you name with two dashes, such as --accent. It inherits like color. You read it with var(--accent), and the value is looked up where it is used:

:root { --accent: #1f5fd1; }
.promo { --accent: #d97706; }
button { background: var(--accent, black); }
form:has(input:user-invalid) button[type="submit"] { opacity: 0.5; }
.card {
  padding: 1rem;
  & h2 { margin-top: 0; }
  &:hover { outline: 2px solid var(--accent); }
}

The :has() selector matches an element by what it contains. CSS could not do this before. The form above dims its submit button while any field is invalid.

Nesting writes child rules inside a parent rule, with & meaning the parent, as in Sass but native.

One button rule, two themesStep 1 of 4

State

  • :root --accent = #1f5fd1

:root sets --accent: #1f5fd1. Every element inherits it.

Read the steps as text
  1. :root sets --accent: #1f5fd1. Every element inherits it.
  2. .promo sets --accent: #d97706. Its children now inherit the new value.
  3. A button inside .promo uses background: var(--accent). The value is looked up on the button itself, at use time.
  4. A button outside .promo gets the root value. One rule, two results, no extra selectors.

Predict#

Predict first

--size: 20px is set on :root, and .box { width: var(--size) }. Later, JavaScript sets --size: 50px on one .box. What width does that box get?

Interview angle#

Say it like this — about 30 seconds

“Custom properties are real CSS properties. They inherit and are resolved on each element when used. So I can theme one part of the page or change them from JavaScript, unlike Sass variables. The var function accepts a fallback. Registering one with @property gives it a type, so it can animate. The :is and :where selectors both match any selector in a list. But :where always has zero specificity, while :is takes its strongest argument. The :has selector picks an element by what it contains.”

Stays on your device. Nothing is uploaded.
Follow-up What is the difference between :is() and :where()?
Both match any selector in the list. :is() takes the specificity of its most specific argument; :where() always has zero.
Follow-up What can :has() do that was impossible before?
Style an element based on its children or following siblings, such as a card that contains an image, or a form with an invalid field.
Follow-up Why register a custom property with @property?
It gives a type, an initial value and inheritance control, so the browser can animate it, for example a gradient angle.

Practice#

On your own

Resolve var() the way the browser does: fallbacks, nested var() and variables that point to other variables.

Done when

  • Known variables are replaced, inside longer values too.
  • Fallbacks are used for missing variables, and may contain var().
  • A missing variable without a fallback makes the result null.
Your solution
Ctrl/⌘ + Enter

Remember#

Try to answer each card before you open it. Then add them to your reviews.

When is var(--x) resolved?

At use time, on the element, after inheritance.

What is the specificity of :where(.a, #b)?

Zero.

What does :has() let you select?

An element based on what it contains or what follows it.

What does & mean in nested CSS?

The parent rule's selector.

Go deeper

Four layers. Stop at the one your interview needs; each ends with a quick check.

The core ideaEvery round
var(--x, fallback) uses the fallback if --x is not set. Without a fallback, the declaration becomes invalid when the value is computed. Then the property acts as unset. It does not fall back to an earlier rule.
Check yourself: What happens to a declaration whose var() is missing and has no fallback?

It becomes invalid at computed-value time, so the property acts as unset.

How it works underneathSenior rounds
@property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } registers a typed property, which can then transition and animate: Chrome 85, Firefox 128, Safari 16.4 Source: MDN: @property.
Check yourself: Why can't a plain custom property be animated smoothly?

The browser does not know its type, so it can only switch values, not interpolate.

Edge cases and trapsWhere people slip

:where() keeps library defaults easy to override. :is(#nav, .menu) a counts as an ID for every match, even .menu a. :has() support: Chrome 105, Firefox 121, Safari 15.4.

Check yourself: Why might :is() raise specificity unexpectedly?

It takes the most specific argument, so one #id inside makes the whole :is() count as an ID.

In productionStaff-level answers

Native nesting: Chrome 120, Firefox 117, Safari 17.2 Source: MDN: CSS nesting. Keep :has() arguments narrow, since broad ones can make style recalculation slower. Tokens as custom properties are the base of theming (QLT.12.01).

Check yourself: Name one cost of :has() to keep in mind.

A broad :has() can make the browser recheck many elements on each change.

Finished the five steps? Mark it, and its cards join your reviews.

Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

Interview questions this answers

  • What is the difference between :is() and :where()?
  • What can :has() do that was impossible before?