# Part 5: Custom Properties, Responsive Design, and Modern CSS

## Introduction

This part covers the tooling layer that ties the rest of CSS together in a maintainable way: custom properties for design tokens, media queries for responsive layouts, and a set of modern CSS features that remove the need for JavaScript or pre-processors for things that used to require them. I use all of these in the stylesheets I write for documentation sites, tool dashboards, and web UIs.

***

## CSS Custom Properties (Variables)

Custom properties let you define a value once and reuse it throughout the stylesheet. Changes in one place propagate everywhere — this is how I manage design tokens (colours, spacing, typography) in CSS without a build step.

### Defining and Using

```css
/* Define at :root — available everywhere */
:root {
  --color-primary: #3b82f6;
  --color-primary-dark: #2563eb;
  --color-text: #1a202c;
  --color-muted: #718096;
  --color-bg: #ffffff;
  --color-bg-subtle: #f7fafc;
  --color-border: #e2e8f0;
  --color-danger: #e53e3e;
  --color-success: #38a169;

  --font-body: system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', monospace;

  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;

  --spacing-1: 0.25rem;
  --spacing-2: 0.5rem;
  --spacing-3: 0.75rem;
  --spacing-4: 1rem;
  --spacing-6: 1.5rem;
  --spacing-8: 2rem;
  --spacing-12: 3rem;

  --radius-sm: 0.25rem;
  --radius-md: 0.375rem;
  --radius-lg: 0.5rem;
  --radius-full: 9999px;

  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
```

```css
/* Use with var() */
.button {
  background: var(--color-primary);
  color: #fff;
  padding: var(--spacing-2) var(--spacing-4);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
}

.button:hover {
  background: var(--color-primary-dark);
}
```

### Fallback Values

```css
.element {
  color: var(--color-accent, #3b82f6);   /* fallback if --color-accent not defined */
}
```

### Scoped Custom Properties

Custom properties are scoped to the element they are defined on and all its descendants. This enables component-level theming:

```css
.card {
  --card-bg: var(--color-bg-subtle);
  --card-border: var(--color-border);

  background: var(--card-bg);
  border: 1px solid var(--card-border);
}

/* Override for a specific variant */
.card--featured {
  --card-bg: #ebf8ff;
  --card-border: #bee3f8;
}
```

### Dark Mode with Custom Properties

```css
:root {
  --color-text: #1a202c;
  --color-bg: #ffffff;
  --color-border: #e2e8f0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-text: #f7fafc;
    --color-bg: #1a202c;
    --color-border: #2d3748;
  }
}
```

All elements using `var(--color-text)` automatically switch without writing any additional selectors.

***

## Responsive Design

### Mobile-First Approach

I write styles for mobile first, then progressively enhance for wider screens. This means the default styles apply to narrow viewports and media queries add complexity for larger ones.

```css
/* Default (mobile) */
.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

/* Tablet and up */
@media (min-width: 640px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop and up */
@media (min-width: 1024px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
```

### My Breakpoint Tokens

```css
:root {
  --bp-sm:  640px;
  --bp-md:  768px;
  --bp-lg:  1024px;
  --bp-xl:  1280px;
  --bp-2xl: 1536px;
}
```

Note: CSS custom properties cannot be used inside media query conditions (a current limitation), so I reference the raw values in `@media` rules and keep this as documentation.

### Media Query Syntax

```css
/* Width-based */
@media (min-width: 768px) { }       /* viewport at least 768px wide */
@media (max-width: 767px) { }       /* viewport up to 767px wide */
@media (min-width: 640px) and (max-width: 1023px) { }  /* range */

/* Orientation */
@media (orientation: landscape) { }
@media (orientation: portrait) { }

/* User preferences */
@media (prefers-color-scheme: dark) { }
@media (prefers-color-scheme: light) { }
@media (prefers-reduced-motion: reduce) { }
@media (prefers-contrast: high) { }

/* Print */
@media print {
  .no-print { display: none; }
  body { font-size: 12pt; }
}
```

### `prefers-reduced-motion`

Always respect this preference for transitions and animations:

```css
.button {
  transition: background 0.2s, transform 0.1s;
}

@media (prefers-reduced-motion: reduce) {
  .button {
    transition: none;
  }
}
```

Or more broadly:

```css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
```

### Container Queries

Container queries respond to the size of a **parent element** rather than the viewport. This is a significant addition that makes truly reusable components possible.

```css
.card-container {
  container-type: inline-size;
  container-name: card;
}

.card {
  display: grid;
  grid-template-columns: 1fr;
}

@container card (min-width: 500px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}
```

The `.card` switches layout based on how wide its container is — regardless of the viewport. The same component renders differently when placed in a wide sidebar versus a narrow panel, without separate media queries.

***

## Modern CSS Features

### CSS Nesting

Native CSS nesting — no pre-processor needed:

```css
.nav {
  display: flex;
  gap: 1rem;

  & a {
    color: var(--color-text);
    text-decoration: none;

    &:hover {
      color: var(--color-primary);
    }

    &.active {
      font-weight: 600;
      color: var(--color-primary);
    }
  }

  @media (max-width: 640px) {
    flex-direction: column;
  }
}
```

Nesting is supported in all modern browsers as of 2024.

### `:is()` and `:where()`

`:is()` matches any element in the list. Its specificity equals the most specific argument.

```css
/* Without :is() */
h1 a, h2 a, h3 a, h4 a {
  color: var(--color-primary);
}

/* With :is() */
:is(h1, h2, h3, h4) a {
  color: var(--color-primary);
}
```

`:where()` works the same way but has **zero specificity** — useful for resets:

```css
:where(ul, ol) {
  list-style: none;
  padding: 0;
  margin: 0;
}
```

### `:has()` — The Parent Selector

Selects an element based on its descendants:

```css
/* Card that contains an image gets a different layout */
.card:has(img) {
  display: grid;
  grid-template-columns: 120px 1fr;
}

/* Form group that contains an invalid input */
.form-group:has(input:invalid) label {
  color: var(--color-danger);
}

/* Nav item that contains the active link */
.nav-item:has(> a.active) {
  background: var(--color-bg-subtle);
}
```

`:has()` is one of the most powerful additions to recent CSS — things previously requiring JavaScript can now be done in CSS.

### `clamp()` for Fluid Typography

```css
/* font-size scales smoothly between 1rem (at 320px) and 1.5rem (at 1280px) */
h1 {
  font-size: clamp(1.5rem, 4vw + 0.5rem, 3rem);
}

p {
  font-size: clamp(1rem, 1.5vw + 0.5rem, 1.25rem);
}

.container {
  width: clamp(320px, 90%, 1200px);
  margin: 0 auto;
}
```

`clamp(min, value, max)`: uses the middle value but never goes below `min` or above `max`.

### `min()` and `max()`

```css
.container {
  width: min(90%, 1200px);   /* whichever is smaller */
}

.hero {
  height: max(50vh, 400px);  /* whichever is larger */
}
```

### CSS Transitions

```css
.button {
  background: var(--color-primary);
  transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
}

.button:hover {
  background: var(--color-primary-dark);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.button:active {
  transform: translateY(0);
}
```

`transition` syntax: `property duration easing-function delay`

Common easing functions:

* `ease` — slow → fast → slow (default)
* `ease-in` — slow start
* `ease-out` — slow end (best for exits)
* `ease-in-out` — slow start and end (best for enters)
* `linear` — constant speed
* `cubic-bezier(x1, y1, x2, y2)` — custom curve

### CSS Animations

```css
@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.page-content {
  animation: fade-in 0.3s ease-out forwards;
}

/* Spinner */
@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 1.5rem;
  height: 1.5rem;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-primary);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
```

### `scroll-behavior`

```css
html {
  scroll-behavior: smooth;
}
```

Anchor links (`#section-id`) will scroll smoothly. Respect `prefers-reduced-motion`:

```css
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}
```

### `scroll-margin-top`

Prevents anchor-linked sections from scrolling under a sticky header:

```css
:target,
[id] {
  scroll-margin-top: 5rem;   /* height of sticky header + some buffer */
}
```

### `accent-color`

Styles native form controls (checkboxes, radio buttons, range inputs) with a brand colour:

```css
:root {
  accent-color: var(--color-primary);
}
```

### `color-scheme`

Tells the browser the page supports light/dark modes — affects browser-native elements like scrollbars and form controls:

```css
:root {
  color-scheme: light dark;
}
```

***

## Typography

A consistent typographic scale makes pages readable without much CSS:

```css
:root {
  --font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
  --line-height-body: 1.6;
  --line-height-heading: 1.2;
}

body {
  font-family: var(--font-body);
  font-size: var(--font-size-base);
  line-height: var(--line-height-body);
  color: var(--color-text);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: 700;
  line-height: var(--line-height-heading);
  color: var(--color-text);
}

code, pre, kbd, samp {
  font-family: var(--font-mono);
  font-size: 0.9em;   /* slightly smaller than body text */
}
```

`system-ui` uses the OS's default UI font (San Francisco on macOS/iOS, Segoe UI on Windows, Roboto on Android) — good performance, no font loading, familiar to users.

***

## Utility Classes I Keep in Every Project

```css
/* Layout */
.container {
  width: min(90%, 1200px);
  margin-inline: auto;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Text */
.text-muted  { color: var(--color-muted); }
.text-center { text-align: center; }
.font-mono   { font-family: var(--font-mono); }
.truncate    { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Visibility */
.hidden { display: none; }
```

***

## Summary

| Feature                    | Key Point                                                          |
| -------------------------- | ------------------------------------------------------------------ |
| CSS custom properties      | Define design tokens at `:root`, re-use with `var()`               |
| Scoped variables           | Override variables at component level for theming                  |
| Dark mode                  | Override custom properties inside `prefers-color-scheme: dark`     |
| Mobile-first               | Default styles for small screens, enhance with `min-width` queries |
| Container queries          | Respond to parent size — makes truly reusable components           |
| CSS nesting                | Group related rules — no pre-processor needed                      |
| `:is()` / `:where()`       | Match lists of selectors; `:where()` has zero specificity          |
| `:has()`                   | Select based on descendants — replaces many JS patterns            |
| `clamp(min, val, max)`     | Fluid sizing between fixed bounds                                  |
| `transition`               | Smooth state changes                                               |
| `@keyframes` + `animation` | Repeating or triggered motion                                      |
| `scroll-margin-top`        | Fix anchor links under sticky headers                              |
| `accent-color`             | Brand colour for native form controls                              |
| `prefers-reduced-motion`   | Always provide motion-safe fallback                                |

***

## Series Complete

This series covered CSS from first principles through to modern features:

| Part                                                                                      | Topic                                                |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [Part 1](https://blog.htunnthuthu.com/getting-started/programming/css-101/css-101-part-1) | Selectors, Specificity, and the Cascade              |
| [Part 2](https://blog.htunnthuthu.com/getting-started/programming/css-101/css-101-part-2) | Box Model, Display, and Positioning                  |
| [Part 3](https://blog.htunnthuthu.com/getting-started/programming/css-101/css-101-part-3) | Flexbox                                              |
| [Part 4](https://blog.htunnthuthu.com/getting-started/programming/css-101/css-101-part-4) | CSS Grid                                             |
| [Part 5](https://blog.htunnthuthu.com/getting-started/programming/css-101/css-101-part-5) | Custom Properties, Responsive Design, and Modern CSS |

With HTML and CSS covered, the next step is JavaScript — handling behaviour, interactivity, and DOM manipulation.
