← back to the section

Layout is covered — what remains is the decorative layer: the part that makes an interface feel "expensive". The good news: almost all modern decoration — gradients, glass, hover effects — is drawn in pure CSS, without images or JavaScript. Let's walk through the tools.

Gradients: images out of nowhere

A gradient is a generated image: it lives in background-image, not in background-color.

.hero  { background: linear-gradient(135deg, #2f5b4f, #8fd3c4); }
.badge { background: radial-gradient(circle at top left, #ffd54d, #ff8a3d); }
.ring  { background: conic-gradient(from 0deg, #2f5b4f 0 75%, #e5e5e5 0); } /* donut chart */

Direction — by angle or keywords (to right); color stops can be placed with percentages. Two stops at the same point produce a sharp edge instead of a transition — that's how gradients draw stripes, chevrons, and progress bars. There can be multiple backgrounds (comma-separated, the top one first): a semi-transparent gradient over a photo is the standard darkening under text:

.card {
  background:
    linear-gradient(rgba(0,0,0,.55), rgba(0,0,0,.15)),
    url(photo.jpg) center / cover no-repeat;
}

The shorthand center / cover no-repeat is background-position / background-size plus repeat: cover fills the box with cropping, contain fits the whole image inside.

Images in a frame: object-fit

What background-size does for backgrounds, object-fit does for content <img> and <video>:

.avatar img {
  width: 64px;
  height: 64px;
  object-fit: cover;   /* fill, cropping the excess, without distortion */
  border-radius: 50%;
}

Without it, an image in fixed dimensions gets squashed. cover — crop, contain — fit with letterboxing, object-position — which part to keep when cropping. Avatars, product cards, video thumbnails — object-fit: cover everywhere.

Filters: photoshop on the fly

filter processes the element itself with all its contents: blur(), brightness(), contrast(), grayscale(), saturate(), sepia(), hue-rotate(), invert(), drop-shadow(). They combine separated by spaces:

.logo-wall img { filter: grayscale(1) opacity(.7); }
.logo-wall img:hover { filter: none; }             /* the classic: gray logos, color on hover */
.sticker { filter: drop-shadow(0 4px 8px rgba(0,0,0,.3)); }

drop-shadow vs box-shadow: box-shadow draws the shadow along the box rectangle, drop-shadow — along the actual outline of the content, including PNG and SVG transparency. For icons and stickers — drop-shadow only.

backdrop-filter processes not the element but what's behind it — the frosted-glass effect from modern interfaces:

.glass-header {
  background: rgba(255,255,255,.6);
  backdrop-filter: blur(12px);
}

Both filters are not cheap to render — apply them with care on large areas and in long lists.

Transforms: moving without consequences

transform shifts, rotates, and scales an element visually, without touching the layout — neighbors don't move, the space is preserved:

.card:hover { transform: translateY(-4px) scale(1.02); }
.chevron.open { transform: rotate(180deg); }

The functions: translate(x, y) (percentages are from the element itself), rotate(deg), scale(), skew(); the order in the list matters — they apply right to left. The pivot point is set by transform-origin: an arrow rotating around its base is transform-origin: bottom center. There's a 3D family too (rotateY, perspective) — card-flip effects.

Two systemic consequences of transform that have already come up in this series: it creates a stacking context and breaks position: fixed on descendants. And a third, pleasant one: transform and opacity are the cheapest properties to animate — the browser runs them on the compositor without rebuilding the layout.

Animations: transition and @keyframes

The animation skeleton uses two tools:

transition — a smooth transition between two states (before → after, on hover or a class):

.card { transition: transform .2s ease, box-shadow .2s ease; }

@keyframes — a script with any number of steps, living its own life:

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}
.live-dot { animation: pulse 1.2s ease-in-out infinite; }

The animation shorthand bundles the name, duration, timing function, delay, iteration count (infinite), direction (alternate — back and forth), and fill-mode (forwards — stay on the final frame). The rule of choice: two states — transition, a script or an infinite loop — keyframes. Performance, the frame budget, and prefers-reduced-motion are covered in a separate article on animations.

In short

  • A gradient is a generated image in background-image; two stops at one point make a sharp edge; a gradient over a photo is darkening under text.
  • For content images in fixed dimensions — object-fit: cover, otherwise they get squashed.
  • filter processes the element, backdrop-filter — the background behind it ("glass"); drop-shadow follows the outline, box-shadow — the rectangle.
  • transform moves things visually without touching the layout; the pivot is transform-origin; as a side effect it creates a stacking context and breaks fixed on descendants.
  • Two states — transition; a script/loop — @keyframes + animation. Animate transform and opacity first.
  • Animations: CSS and optimizations — performance, patterns, and motion accessibility.
  • Positioning — the stacking contexts that transform creates.
  • Cascade and specificity — the start of the series, if you arrived here from the end.