CSS Variables & Modern Features

CSS Variables & Modern Features

Modern CSS is much more powerful than older CSS. It now includes variables, logical properties, native nesting, parent selectors, container queries, cascade layers, advanced color functions, layout helpers, and better responsive tools.

This chapter introduces the features that make modern CSS easier to scale in real projects.


1. CSS Custom Properties

CSS custom properties are often called CSS variables.

:root {
  --color-brand: #2563eb;
  --color-surface: #fffdf8;
  --space-4: 1rem;
}

.button {
  background: var(--color-brand);
  padding: var(--space-4);
}

Custom properties start with -- and are read with var().

They are useful for:

  • colors,
  • spacing,
  • shadows,
  • typography,
  • border radii,
  • animation durations,
  • and theme values.
--brand: #2563eb--radius: 16pxOne token can style many components.

2. Scope and Inheritance

Custom properties inherit. A value declared on a parent can be used by children.

:root {
  --accent: #2563eb;
}

.warning-section {
  --accent: #d97706;
}

.link {
  color: var(--accent);
}

Links inside .warning-section become orange. Links elsewhere stay blue.

This makes custom properties excellent for local themes.

.card {
  --card-bg: #fffdf8;
  --card-border: #d6d3d1;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
}

3. Fallback Values

var() can include a fallback.

.button {
  background: var(--button-bg, #2563eb);
}

If --button-bg is not defined, the browser uses #2563eb.

Fallbacks can also be nested:

.alert {
  color: var(--alert-text, var(--color-text, #0f172a));
}

Use fallbacks for component-level variables that may or may not be customized.


4. Custom Properties and Themes

Themes are one of the best uses for variables.

:root {
  --bg: #fffdf8;
  --text: #0f172a;
  --surface: #f8fafc;
}

[data-theme="dark"] {
  --bg: #0f172a;
  --text: #f8fafc;
  --surface: #1e293b;
}

body {
  background: var(--bg);
  color: var(--text);
}

.card {
  background: var(--surface);
}

You can toggle data-theme="dark" with JavaScript or server-rendered HTML.

Light themeDark themeSame CSS structure, different variable values.

5. calc(), min(), max(), and clamp()

Modern CSS can calculate values.

calc()

.sidebar-layout {
  width: calc(100% - 2rem);
}

min()

Use the smaller value.

.container {
  width: min(100% - 2rem, 1200px);
}

max()

Use the larger value.

.button {
  min-height: max(44px, 2.75rem);
}

clamp()

Set a minimum, flexible preferred value, and maximum.

h1 {
  font-size: clamp(2rem, 6vw, 5rem);
}

These functions reduce the need for many breakpoints.


6. Modern Color Functions

Modern CSS supports more readable color syntax.

.overlay {
  background: rgb(0 0 0 / 45%);
}

HSL is useful for color scales:

:root {
  --brand-hue: 217;
  --brand: hsl(var(--brand-hue) 91% 60%);
  --brand-soft: hsl(var(--brand-hue) 91% 92%);
}

color-mix() can blend colors:

.button:hover {
  background: color-mix(in srgb, var(--brand) 85%, black);
}

Use modern color functions carefully if your target browsers require compatibility checks.


7. Logical Properties

Logical properties adapt to writing direction and writing mode.

Instead of:

.card {
  margin-left: auto;
  margin-right: auto;
  padding-top: 2rem;
  padding-bottom: 2rem;
}

Use:

.card {
  margin-inline: auto;
  padding-block: 2rem;
}

Common logical properties:

PhysicalLogical
margin-left and margin-rightmargin-inline
margin-top and margin-bottommargin-block
padding-left and padding-rightpadding-inline
padding-top and padding-bottompadding-block
widthinline-size
heightblock-size
border-leftborder-inline-start

Logical properties make internationalization easier.


8. The :has() Selector

:has() lets CSS style an element based on what it contains.

.card:has(img) {
  padding: 0;
}

This means: select .card elements that contain an img.

Useful examples:

form:has(input:invalid) {
  border-color: #dc2626;
}

.nav-item:has([aria-current="page"]) {
  font-weight: 700;
}

.field:has(input:focus-visible) {
  outline: 3px solid #93c5fd;
}
<img>.card:has(img) matches<p>No image, no match

Use :has() to simplify state-based parent styling, but keep selectors readable.


9. Container Queries

Media queries react to the viewport. Container queries react to a container.

.card-shell {
  container-type: inline-size;
}

.card {
  display: grid;
  gap: 1rem;
}

@container (min-width: 420px) {
  .card {
    grid-template-columns: 140px 1fr;
  }
}

This makes components portable. The same card can stack in a sidebar and become horizontal in a wider content area.

Container query units are also available:

.card-title {
  font-size: clamp(1.25rem, 8cqi, 2rem);
}

cqi means 1% of the container's inline size.


10. Cascade Layers

Cascade layers give explicit ordering to groups of CSS.

@layer reset, base, components, utilities;

@layer base {
  body {
    font-family: system-ui, sans-serif;
  }
}

@layer components {
  .button {
    background: #2563eb;
  }
}

@layer utilities {
  .bg-red {
    background: #dc2626;
  }
}

Later layers beat earlier layers when specificity and importance allow it.

This is useful for organizing large CSS systems:

  • reset
  • base
  • layout
  • components
  • utilities
resetbasecomponentsutilitiesLater layers can intentionally override earlier layers.

11. Native CSS Nesting

Modern CSS supports nesting in many environments.

.card {
  padding: 1rem;
  border: 1px solid #d6d3d1;

  &:hover {
    border-color: #94a3b8;
  }

  & h2 {
    margin-block: 0;
  }
}

Nesting keeps related styles close together.

Do not over-nest:

/* Too specific and hard to reuse */
.page {
  & .section {
    & .card {
      & h2 {
        color: #0f172a;
      }
    }
  }
}

Prefer shallow nesting.


12. :is(), :where(), and :not()

These selectors reduce repetition.

:is()

:is() groups selectors and keeps the highest specificity inside.

:is(h1, h2, h3) {
  line-height: 1.1;
}

:where()

:where() has zero specificity.

:where(article, section) :where(h1, h2, h3) {
  text-wrap: balance;
}

This is useful for low-specificity defaults.

:not()

:not() excludes matches.

button:not(:disabled) {
  cursor: pointer;
}

These selectors help keep CSS shorter and easier to override.


13. aspect-ratio

aspect-ratio keeps an element in a consistent shape.

.video {
  aspect-ratio: 16 / 9;
  width: 100%;
}

.avatar {
  aspect-ratio: 1;
  border-radius: 50%;
}

Use it for:

  • videos,
  • thumbnails,
  • cards,
  • profile images,
  • placeholders,
  • and responsive media.
16 / 91 / 1video thumbnailsquare media

14. Subgrid

subgrid lets a nested grid align to its parent grid tracks.

.pricing-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.pricing-card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
}

Without subgrid, nested cards often have headings, feature lists, and buttons that do not line up. With subgrid, the child can share the parent's row structure.

Use subgrid for:

  • pricing cards,
  • product comparison tables,
  • card layouts where internal rows should align,
  • and nested editorial layouts.

15. Scroll Snap

Scroll snap makes scrolling stop at defined points.

.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.slide {
  flex: 0 0 80%;
  scroll-snap-align: center;
}

This can be useful for carousels and horizontal content sections.

Do not trap users. Keep scrolling predictable and keyboard accessible.


16. Modern Text Features

text-wrap: balance

Balances heading line breaks.

h1,
h2 {
  text-wrap: balance;
}

text-wrap: pretty

Improves paragraph wrapping where supported.

p {
  text-wrap: pretty;
}

line-clamp

Clamp text to a fixed number of lines.

.summary {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

Use clamping carefully because it hides content.


17. Accent Color and Color Scheme

accent-color styles browser-native form controls.

:root {
  accent-color: #2563eb;
}

It can affect checkboxes, radio buttons, range inputs, and progress elements.

color-scheme tells the browser which color schemes your UI supports.

:root {
  color-scheme: light;
}

[data-theme="dark"] {
  color-scheme: dark;
}

This helps native controls and browser UI match your theme.


18. CSS Preprocessors

Sass and SCSS existed because CSS used to lack variables, nesting, modules, and useful functions.

SCSS example:

$primary: #2563eb;

@mixin center {
  display: flex;
  align-items: center;
  justify-content: center;
}

.button {
  @include center;
  background: $primary;

  &:hover {
    background: darken($primary, 8%);
  }
}

Modern CSS now has many native features that reduce the need for preprocessors, but Sass can still be useful in legacy or large codebases.

Use Sass when your project already uses it or needs its tooling. Do not add it automatically if native CSS is enough.


19. CSS Frameworks

Frameworks speed up UI development and enforce consistency.

Bootstrap

Bootstrap is component-first.

<button class="btn btn-primary btn-lg">Click Me</button>

Pros:

  • fast prototypes,
  • many ready-made components,
  • strong documentation,
  • common patterns.

Cons:

  • can look generic,
  • overriding styles can be awkward,
  • large projects may fight the framework.

Tailwind CSS

Tailwind is utility-first.

<button class="rounded-full bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-700">
  Click Me
</button>

Pros:

  • flexible,
  • low unused CSS when configured correctly,
  • strong design-token workflow,
  • fast iteration in component systems.

Cons:

  • markup can become class-heavy,
  • team needs to learn the utility naming system,
  • design consistency still requires discipline.

20. Design Tokens

Design tokens are named decisions.

:root {
  --font-body: "Source Sans 3", system-ui, sans-serif;
  --color-bg: #fffdf8;
  --color-text: #0f172a;
  --color-brand: #2563eb;
  --radius-card: 24px;
  --shadow-card: 0 18px 40px rgb(15 23 42 / 8%);
  --space-page: clamp(1rem, 4vw, 3rem);
}

Tokens prevent every component from inventing its own spacing, colors, and radii.

.card {
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  background: var(--color-bg);
}

21. Complete Example: Modern Card System

@layer base, components, utilities;

@layer base {
  :root {
    --surface: #fffdf8;
    --text: #0f172a;
    --border: #d6d3d1;
    --accent: #2563eb;
    --radius: 24px;
    --shadow: 0 18px 40px rgb(15 23 42 / 8%);
  }
}

@layer components {
  .course-card {
    container-type: inline-size;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow);
    overflow: hidden;

    & a {
      color: var(--accent);
    }

    &:has(img) {
      padding-block-start: 0;
    }
  }

  @container (min-width: 440px) {
    .course-card {
      display: grid;
      grid-template-columns: 160px 1fr;
    }
  }
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

This example combines:

  • cascade layers,
  • design tokens,
  • custom properties,
  • nesting,
  • container queries,
  • :has(),
  • logical properties,
  • and utility classes.

22. Compatibility and Progressive Enhancement

Modern CSS features are powerful, but you should know your browser support needs.

Progressive enhancement means:

  1. Start with a usable baseline.
  2. Add modern enhancements for browsers that support them.
  3. Avoid making the page unusable if an enhancement fails.

Example with @supports:

.title {
  max-width: 16ch;
}

@supports (text-wrap: balance) {
  .title {
    max-width: none;
    text-wrap: balance;
  }
}

If text-wrap: balance is unsupported, the title still has a reasonable max width.


23. Debugging Checklist

When modern CSS does not work, check:

  1. Is the property supported in your target browsers?
  2. Is a custom property defined in the correct scope?
  3. Does var() need a fallback?
  4. Is a cascade layer overriding the rule?
  5. Is the selector specificity lower than expected because of :where()?
  6. Is :has() matching the structure you think it is?
  7. Does a container query have a container with container-type?
  8. Is nested CSS compiled or supported by your toolchain?
  9. Would @supports provide a safe fallback?
  10. Is a framework utility overriding your component style?
Check support, @supports, and fallbacksCheck variable scope and cascade layersCheck :has(), :where(), and specificityCheck container query setup

24. Summary and Next Steps

Modern CSS lets you build systems instead of isolated rules.

You now have tools for:

  • reusable values with custom properties,
  • scoped themes,
  • fluid sizing with clamp(),
  • logical spacing,
  • parent-aware styling with :has(),
  • component-aware responsiveness with container queries,
  • predictable cascade ordering with layers,
  • shallow native nesting,
  • modern selectors,
  • aspect ratios,
  • subgrid,
  • scroll snap,
  • and progressive enhancement.

The next step is practice. Build small components and ask:

  1. Which values should become tokens?
  2. Which styles should be component-specific?
  3. Which styles should be utilities?
  4. Can this layout respond without many breakpoints?
  5. Can this feature fail gracefully in older browsers?

Good modern CSS is not about using every new feature. It is about choosing the smallest set of features that makes the code clearer, more flexible, and easier to maintain.