Skip to content

CSS3 Aide-mémoire

Latest evolution of CSS with modules for styling, layout, and animation.

01

Selectors

Attribute & Pseudo-class Selectors

CSS3 introduced powerful attribute selectors and pseudo-classes. ^= matches prefix, $= matches suffix, *= matches substring. nth-child enables zebra striping without JavaScript.

css3
/* attribute selectors */
input[type="text"] { border: 1px solid blue; }
a[href^="https"] { color: green; }
a[href$=".pdf"] { color: red; }
a[href*="example"] { font-weight: bold; }

/* pseudo-classes */
a:visited { color: purple; }
li:first-child { font-weight: bold; }
li:nth-child(odd) { background: #f0f0f0; }
input:focus { outline: 2px solid blue; }

Structural Pseudo-classes

Structural pseudo-classes select elements by their position in the DOM. :first-child / :last-child target edges. nth-child(an+b) supports formulas: 2n is even, 2n+1 is odd, 3n is every third. :nth-of-type counts only siblings of the same element type. :empty matches elements with no children.

css3
/* positional pseudo-classes */
li:first-child { color: green; }
li:last-child { color: red; }
li:only-child { font-weight: bold; }

/* nth variants */
li:nth-child(3) { color: blue; }
li:nth-child(2n+1) { background: #eee; } /* odd */
li:nth-last-child(2) { color: orange; }

/* type-based */
p:first-of-type { font-size: 1.2em; }
p:nth-of-type(even) { color: gray; }
div:empty { display: none; }

:is, :where, and :has

:is() and :where() group selectors for brevity. The difference: :is() takes the highest specificity of its arguments, while :where() always has zero specificity — ideal for low-override base styles. :has() is the long-awaited parent/relational selector, letting you style an element based on its descendants.

css3
/* :is() - matches any selector in the list */
:is(h1, h2, h3) { color: navy; }
:is(.btn, .link):hover { opacity: 0.8; }

/* :where() - same as :is but zero specificity */
:where(article, section) p { line-height: 1.6; }

/* :has() - parent selector (selects element that has a match) */
div:has(> img) { padding: 0; }
card:has(.badge) { border-color: gold; }
form:has(input:invalid) button { opacity: 0.5; }

Negation & Form Pseudo-classes

:not() negates a selector and now accepts a list. Form pseudo-classes (:required, :valid, :invalid, :checked, :placeholder-shown, :in-range) enable styling based on input state without JavaScript — great for inline validation feedback and custom checkbox/radio styling.

css3
/* :not() excludes matching elements */
input:not([type="checkbox"]) { display: block; }
li:not(:last-child) { border-bottom: 1px solid #ddd; }

/* form state pseudo-classes */
input:required { border-color: red; }
input:optional { border-color: #ccc; }
input:valid { border-color: green; }
input:invalid { border-color: red; }
input:in-range { color: black; }
input:out-of-range { color: red; }
input:placeholder-shown { background: #fafafa; }
input:checked + label { font-weight: bold; }

Specificity & Cascade

Specificity decides which rule applies when selectors conflict. Count IDs, classes, and elements as (a,b,c). !important overrides the cascade but breaks maintainability — avoid it. When specificity ties, the later rule in source order wins. DevTools shows the computed cascade.

css3
/* specificity ranking: inline > ID > class/attr/pseudo > type/element */
#nav .item { color: red; }     /* 1,1,0 */
.menu .item { color: blue; }   /* 0,2,0 */
li.item { color: green; }      /* 0,1,1 */

/* !important overrides normal declarations */
.btn { color: white !important; }

/* source order wins on equal specificity */
.late { color: purple; }  /* wins over .early */
02

Box Model

box-sizing

box-sizing: border-box includes padding and border within the declared width/height, making sizing predictable. content-box (default) adds them on top. Applying border-box globally is the most important CSS reset — it eliminates the most common layout math headaches.

css3
/* default: width = content only */
.box-content { box-sizing: content-box; width: 200px; padding: 20px; } /* total 240px */

/* padding & border inside width */
.box-border { box-sizing: border-box; width: 200px; padding: 20px; border: 2px solid; } /* total 200px */

/* universal reset - apply everywhere */
*, *::before, *::after {
  box-sizing: border-box;
}

Margin & Padding

Padding is inside the border (inherits background); margin is outside (transparent). Vertical margins between adjacent block elements collapse to the larger value — horizontal margins never collapse. margin: 0 auto centers a block element with a defined width. Negative margins can pull content but use sparingly.

css3
.box {
  /* top right bottom left (clockwise) */
  margin: 10px 20px 15px 25px;
  /* 2 values: vertical horizontal */
  padding: 10px 20px;
  /* single value: all sides */
  margin: 0 auto; /* horizontal centering for fixed-width block */
}

/* margin collapse (vertical only) */
.a { margin-bottom: 30px; }
.b { margin-top: 20px; }
/* gap between .a and .b = 30px, not 50px (collapsed to larger) */

/* negative margins pull elements together */
.pull-left { margin-left: -10px; }

box-shadow

box-shadow syntax: offset-x offset-y blur-radius spread-radius color. Positive offsets push shadow right/down. inset creates an inner shadow. Multiple shadows layer with the first on top. Use rgba for translucent, natural shadows. Hard shadows (zero blur) create a flat, bold aesthetic.

css3
/* offset-x offset-y blur spread color */
.card { box-shadow: 0 4px 6px rgba(0,0,0,0.1); }

/* inset shadow (inside the box) */
.inset { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); }

/* multiple shadows (first = top layer) */
.layered { box-shadow: 0 1px 2px #000, 0 8px 24px rgba(0,0,0,0.15); }

/* no blur, hard shadow (neo-brutalism) */
.hard { box-shadow: 6px 6px 0 #000; }

/* glow effect */
.glow { box-shadow: 0 0 20px rgba(0,150,255,0.6); }

Display Types

display: none removes the element from layout (no space reserved). block elements take full width and stack vertically; inline elements flow with text and ignore width/height. inline-block combines inline flow with block sizing. flow-root clears floats without clearfix hacks. contents makes the box vanish while keeping children in the layout.

css3
/* block: full width, new line */
div, p { display: block; }

/* inline: flows with text, ignores width/height */
span, a { display: inline; }

/* inline-block: flows inline but accepts width/height */
.tag { display: inline-block; width: 100px; }

/* none: removed from layout entirely */
.hidden { display: none; }

/* modern display values */
.flow { display: flow-root; } /* new block formatting context */
.contents { display: contents; } /* element's box disappears, children participate */

Overflow

overflow controls how content exceeding the box is handled. visible (default) lets it spill; hidden clips it; auto adds scrollbars when needed; scroll always shows them. clip (newer) is like hidden but prevents any scrolling, even via JS. Use overflow-y/x to control axes independently. Setting overflow to anything but visible creates a new formatting context.

css3
.scrollable {
  overflow: auto;       /* scrollbars when needed */
  overflow-y: scroll;   /* always show vertical scrollbar */
  overflow-x: hidden;   /* hide horizontal overflow */
}

/* visible (default) - content spills out */
.spill { overflow: visible; }

/* clip - like hidden but no programmatic scroll */
.clipped { overflow: clip; }

/* clip to a shape */
.custom-clip {
  overflow: clip;
  overflow-clip-margin: 20px;
}

/* longhand for both axes */
.both { overflow-x: hidden; overflow-y: auto; }
03

Flexbox

Flex Container

display: flex turns an element into a flex container. justify-content aligns items along the main axis (horizontal in row); align-items aligns along the cross axis. flex-wrap lets items wrap to new lines. gap replaces the margin-based spacing hack. Flexbox excels at one-dimensional layouts (rows or columns).

css3
.container {
  display: flex;
  flex-direction: row;       /* row | row-reverse | column | column-reverse */
  justify-content: center;   /* main axis: flex-start | center | space-between | space-around | space-evenly */
  align-items: center;       /* cross axis: stretch | flex-start | center | baseline */
  flex-wrap: wrap;           /* nowrap | wrap | wrap-reverse */
  gap: 16px;                 /* spacing between items */
  align-content: space-between; /* multi-line spacing */
}

Flex Items

flex-grow controls how an item expands to fill extra space (1 = equal share). flex-shrink controls shrinking. flex-basis is the initial size. The shorthand flex: 1 means grow equally from zero basis — perfect for equal columns. align-self overrides the container's align-items for a single item. order reorders visually without changing DOM order.

css3
.item {
  flex-grow: 1;     /* grow to fill extra space (0 = don't grow) */
  flex-shrink: 1;   /* shrink when space is tight (0 = don't shrink) */
  flex-basis: 200px; /* initial size before growing/shrinking */
  
  /* shorthand: flex: grow shrink basis */
  flex: 1 1 200px;   /* flexible from 200px */
  flex: 1;            /* = 1 1 0% - equal distribution */
  flex: 0 0 250px;    /* fixed width, no grow/shrink */
  
  align-self: flex-end; /* override align-items for this item */
  order: 2;             /* visual reordering (default 0) */
}

Flex Alignment Patterns

Flexbox makes centering trivial: justify-content: center + align-items: center. A sticky footer uses a column flex body with min-height: 100vh and the main content set to flex: 1. For navigation bars, space-between pushes the first and last items to opposite edges. Auto margins (margin-left: auto) are a flexible alternative to justify-content for pushing items.

css3
/* perfect centering */
.center {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

/* sticky footer: body is flex column */
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; } /* grows to fill, pushing footer down */

/* navbar: space between */
.nav { display: flex; justify-content: space-between; }

/* auto margins absorb space */
.push-right { margin-left: auto; }

Flex Wrap & Responsive Grid

flex-wrap: wrap lets items flow to new lines when they run out of space. flex: 1 1 280px creates cards that start at 280px and grow to fill the row. This makes a responsive card grid without media queries. Beware: the last row's items stretch to fill, which may look uneven — for consistent card grids, CSS Grid is often a better choice.

css3
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.card {
  /* start at 280px, grow & shrink fluidly */
  flex: 1 1 280px;
  /* prevent stretching beyond readable width */
  max-width: 100%;
}

/* OR fixed-width cards that wrap */
.card-fixed {
  flex: 0 0 300px;
}

/* last row items stretch to fill - to avoid, use grid instead */
/* gap wraps with items automatically */

Flex Direction & Axes

flex-direction determines the main axis. In row (default), justify-content controls horizontal alignment; in column, it controls vertical alignment — the axes swap. Always ask 'which is the main axis?' when aligning. row-reverse and column-reverse flip item order, which also affects how justify-content distributes space.

css3
/* row: main axis = horizontal */
.row { display: flex; flex-direction: row; justify-content: center; }

/* column: main axis = vertical (axes swap!) */
.col { display: flex; flex-direction: column; align-items: center; }

/* reverse order */
.reverse { display: flex; flex-direction: row-reverse; }

/* centering in column: justify-content centers vertically */
.col-center {
  display: flex;
  flex-direction: column;
  justify-content: center; /* vertical center */
  align-items: center;     /* horizontal center */
}
04

Grid Layout

Grid Container

display: grid creates a two-dimensional layout. grid-template-columns defines column tracks; the fr unit distributes free space proportionally. repeat(auto-fit, minmax(200px, 1fr)) is the magic recipe for responsive grids — columns auto-create and resize without media queries. gap adds space between tracks cleanly.

css3
.grid {
  display: grid;
  /* fixed columns */
  grid-template-columns: 200px 1fr 200px;
  /* responsive auto columns */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  grid-template-rows: auto 1fr auto;
  gap: 20px;            /* row + column gap */
  row-gap: 20px;        /* gap between rows */
  column-gap: 16px;     /* gap between columns */
}

Grid Template Areas

grid-template-areas lets you draw the layout visually with named regions — extremely readable. Each quoted string is a row; names place items, and a dot marks an empty cell. An item with grid-area: name fills that region. This is the clearest way to build page-level layouts like app shells.

css3
.layout {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
  min-height: 100vh;
}

.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

/* empty cell with a dot */
grid-template-areas: "header header" "sidebar .";

Grid Item Placement

Grid items can be placed explicitly by line numbers. grid-column: 1 / 3 spans from line 1 to line 3 (covering 2 columns). The span keyword sizes by track count. Lines can also be named in grid-template-columns for readability. Without explicit placement, items auto-flow into available cells.

css3
.item {
  /* place by line numbers: row-start / col-start / row-end / col-end */
  grid-column: 1 / 3;      /* span columns 1 to 3 */
  grid-row: 1 / 2;
  
  /* span keyword */
  grid-column: span 2;     /* span 2 columns */
  grid-row: 2 / span 3;
  
  /* shorthand: grid-area: row-start / col-start / row-end / col-end */
  grid-area: 1 / 1 / 3 / 4;
  
  /* name a line and reference it */
  grid-column-start: content-start;
}

Grid Alignment

Grid alignment works on two levels. justify-content/align-content position the entire grid within the container when there's leftover space. justify-items/align-items align items inside their cells (stretch by default). justify-self/align-self override alignment per item. 'justify' = inline/main axis, 'align' = block/cross axis.

css3
.grid {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  
  /* align tracks within container */
  justify-content: center;  /* center the grid horizontally */
  align-content: center;    /* center vertically (needs fixed height) */
  
  /* align items within their cells */
  justify-items: stretch;   /* fill cell width (default) */
  align-items: center;      /* center within cell height */
}

.item {
  justify-self: start;      /* override justify-items for one item */
  align-self: end;
}

auto-fit vs auto-fill

Both create as many tracks as fit, but behave differently with few items. auto-fit collapses unused tracks and stretches existing items to fill the row. auto-fill keeps empty tracks, so items stay their min size. Use auto-fit when you want items to grow and fill space; use auto-fill when you want a fixed, consistent item size.

css3
/* auto-fit: empty tracks collapse, items stretch to fill */
.autofit {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

/* auto-fill: empty tracks remain, items keep their size */
.autofill {
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}

/* with few items:
   auto-fit -> items stretch to fill the row
   auto-fill -> items stay small, leaving empty columns */
05

Positioning

Position Values

static is the default. relative offsets an element from its normal flow position (space is preserved). absolute removes it from flow and positions it relative to the nearest positioned ancestor (or viewport). fixed is relative to the browser viewport. sticky toggles between relative and fixed based on scroll position — ideal for sticky headers.

css3
/* static - default, flows normally */
.static { position: static; }

/* relative - offset from its normal position */
.relative { position: relative; top: 10px; left: 20px; }

/* absolute - positioned relative to nearest positioned ancestor */
.absolute { position: absolute; top: 0; right: 0; }

/* fixed - positioned relative to viewport (stays on scroll) */
.fixed { position: fixed; bottom: 0; left: 0; }

/* sticky - relative until scroll threshold, then fixed */
.sticky { position: sticky; top: 0; }

Absolute Centering

Several ways to absolutely center. The translate(-50%, -50%) trick works on elements of any size. The inset: 0 + margin: auto technique requires explicit dimensions. The modern approach is display: grid with place-items: center (shorthand for align-items + justify-items) — no positioning math needed and the simplest method.

css3
/* classic: position absolute + transform */
.center {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}

/* inset: 0 + margin: auto (needs width/height) */
.center-auto {
  position: absolute;
  inset: 0;          /* top/right/bottom/left: 0 */
  margin: auto;
  width: 300px;
  height: 200px;
}

/* modern: no positioning needed */
.center-grid { display: grid; place-items: center; }

Sticky Positioning

position: sticky keeps an element in normal flow until it reaches a threshold (e.g., top: 0), then it sticks like fixed. Perfect for sticky headers or table-of-contents sidebars. Gotcha: any ancestor with overflow: hidden/auto/scroll (in the scroll direction) becomes the containing block and breaks expected behavior. Always set a background and z-index on sticky elements.

css3
.header {
  position: sticky;
  top: 0;            /* sticks when header hits top of viewport */
  z-index: 100;
  background: white;
}

/* sticky sidebar that scrolls with content until threshold */
.sidebar {
  position: sticky;
  top: 60px;         /* offset below the fixed header */
  max-height: calc(100vh - 60px);
  overflow-y: auto;
}

/* NOTE: a parent with overflow: hidden breaks sticky */

z-index & Stacking Contexts

z-index only works on positioned (non-static) elements. A stacking context is created by position + z-index, but also by opacity < 1, transform, filter, will-change, and isolation: isolate. Inside a stacking context, child z-index values are isolated — a child can never render above a sibling of an ancestor. Understanding this prevents z-index escalation wars.

css3
.modal { position: fixed; z-index: 1000; }
.overlay { position: fixed; z-index: 999; }
.card { position: relative; z-index: 1; }

/* creating a stacking context (isolates z-index) */
.context {
  position: relative;
  z-index: 0;  /* children's z-index only matters within this */
}

/* other properties that create stacking contexts:
   opacity < 1, transform, filter, will-change, isolation: isolate */
.isolated { isolation: isolate; }

inset Shorthand

inset is the logical shorthand for top, right, bottom, and left — like margin/padding it takes 1, 2, or 4 values. inset: 0 stretches an absolutely-positioned element to fill its containing block. It's cleaner than declaring four separate properties and pairs well with margin: auto for centering.

css3
/* inset sets top right bottom left (like margin) */
.popup {
  position: absolute;
  inset: 0;          /* all sides = 0 (stretches to fill parent) */
}

/* two values: vertical horizontal */
.pill {
  position: absolute;
  inset: 10px 20px;  /* top/bottom 10px, left/right 20px */
}

/* four values: top right bottom left */
.box {
  position: absolute;
  inset: 10px 20px 30px 40px;
}

/* longhand equivalents */
.inset-longhand {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
}
06

Text Styling

Font Properties

font-family should always end with a generic family (serif, sans-serif, monospace) as a last-resort fallback. font-weight ranges 100-900; common: 400 normal, 700 bold. line-height is best set unitless (1.5) so it scales with font-size. The font shorthand requires size and family; order is style weight size/line-height family.

css3
body {
  font-family: "Helvetica Neue", Arial, sans-serif; /* fallback chain */
  font-size: 16px;
  font-weight: 400;     /* 100-900; 400=normal, 700=bold */
  font-style: italic;   /* normal | italic | oblique */
  line-height: 1.5;     /* unitless = multiplier of font-size */
  letter-spacing: 0.5px;
  word-spacing: 2px;
  font-variant: small-caps;
}

/* shorthand: style weight size/line-height family (order matters) */
p { font: italic 700 16px/1.5 "Georgia", serif; }

Text Formatting

text-align: justify can create rivers of whitespace; pair with hyphens: auto for cleaner blocks. text-decoration now accepts line, style, and color in one shorthand. text-transform: capitalize title-cases each word. white-space: nowrap prevents wrapping. text-shadow takes offset-x offset-y blur color.

css3
.text {
  text-align: justify;          /* left | right | center | justify */
  text-decoration: underline wavy red; /* line style color */
  text-decoration: line-through;
  text-transform: uppercase;    /* lowercase | capitalize */
  text-indent: 2em;             /* first-line indent */
  white-space: nowrap;          /* prevent wrapping */
  word-break: break-word;       /* break long words */
  hyphens: auto;                /* auto-hyphenate (needs lang attr) */
  text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}

Text Overflow & Ellipsis

Single-line truncation needs three properties: white-space: nowrap, overflow: hidden, text-overflow: ellipsis. For multi-line truncation, use -webkit-line-clamp with display: -webkit-box (still needs the -webkit- prefix but works in all modern browsers). A max-width or width is required for truncation to trigger.

css3
.truncate {
  /* three properties required for ellipsis */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;    /* clip | ellipsis */
  max-width: 200px;
}

/* multi-line clamp (line-clamp) */
.clamp {
  display: -webkit-box;
  -webkit-line-clamp: 3;       /* limit to 3 lines */
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* custom fade effect */
.fade {
  -webkit-mask-image: linear-gradient(to bottom, black 70%, transparent);
}

text-shadow & Effects

text-shadow takes offset-x offset-y blur-radius color. Multiple shadows layer with the first on top — useful for neon glows or text outlines (a 4-direction shadow mimics an outline). Unlike -webkit-text-stroke, text-shadow is universally supported. Hard shadows (no blur) create a retro, sticker-like aesthetic.

css3
/* x-offset y-offset blur color */
.shadow { text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }

/* multiple shadows (first = top) */
.neon { text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #0ff; }

/* 3D letterpress effect */
.letterpress { text-shadow: 0 1px 0 rgba(255,255,255,0.5); }

/* outline text (no blur, light color) */
.outline { text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; }

/* hard shadow retro effect */
.retro { text-shadow: 4px 4px 0 #ff6b6b; }

Writing Modes & Direction

writing-mode controls text direction: horizontal-tb (default), vertical-rl (e.g., traditional Chinese/Japanese), or vertical-lr. direction: rtl handles right-to-left languages like Arabic. Logical properties (margin-inline-start, padding-block, inset-inline-end) adapt automatically to the writing mode and direction — essential for truly international, direction-agnostic layouts.

css3
/* horizontal (default) */
.horizontal { writing-mode: horizontal-tb; }

/* vertical, top to bottom */
.vertical-rl { writing-mode: vertical-rl; }
.vertical-lr { writing-mode: vertical-lr; }

/* text direction */
.rtl { direction: rtl; unicode-bidi: bidi-override; }

/* logical properties adapt to writing mode */
.box {
  margin-inline-start: 10px;   /* logical left in LTR */
  padding-block: 20px;         /* logical top/bottom */
  inset-inline-end: 0;         /* logical right in LTR */
}
07

Backgrounds & Gradients

Background Shorthand

The background shorthand accepts all background properties. When using position and size together, separate them with a slash (center/cover). Multiple backgrounds are layered comma-separated, first declared = topmost. A solid color should always be last as a fallback. Use cover to fill the box and contain to fit the whole image.

css3
.box {
  /* longhand */
  background-color: #fff;
  background-image: url("bg.png");
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  background-attachment: fixed;
  
  /* shorthand (color image position/size repeat attachment) */
  background: #fff url("bg.png") center/cover no-repeat fixed;
}

/* multiple backgrounds (first = top layer) */
.multi {
  background: url("top.png") top left/contain no-repeat,
              url("bottom.png") bottom right/cover no-repeat #eee;
}

Linear Gradients

linear-gradient(direction, color-stops) creates a smooth transition. Direction can be a keyword (to right) or angle (45deg). Stops can include positions as percentages — placing two stops at the same position creates a hard edge (great for stripes). Gradients are images, so they go on background-image, and can be used wherever images are accepted.

css3
/* direction or angle, then color stops */
.gradient { background: linear-gradient(to right, red, blue); }
.gradient-45 { background: linear-gradient(45deg, red, blue); }

/* multiple color stops */
.rainbow { background: linear-gradient(to right, red, orange, yellow, green, blue); }

/* color stops with positions */
.hard-stop { background: linear-gradient(to right, red 0%, red 50%, blue 50%, blue 100%); }

/* transparent stops for fade effects */
.fade { background: linear-gradient(to bottom, rgba(0,0,0,0.5), transparent); }

Radial & Conic Gradients

radial-gradient radiates outward from a center point — useful for spotlights and buttons. You can position the center (at top left) and control the shape/size (circle, closest-side). conic-gradient rotates colors around a point — perfect for pie charts and color wheels. Hard stops in conic gradients create clean pie slices.

css3
/* radial: radiates from a center point */
.radial { background: radial-gradient(circle, red, blue); }
.radial-c { background: radial-gradient(circle at top left, #fff, #000); }
.radial-size { background: radial-gradient(closest-side, red, blue); }

/* conic: rotates around a center point */
.pie { background: conic-gradient(red 0% 30%, blue 30% 70%, green 70% 100%); }

/* conic with angle start */
.spin { background: conic-gradient(from 45deg, red, yellow, green, red); }

background-clip & origin

background-clip controls how far the background extends: border-box (default, under the border), padding-box (inside border), or content-box (inside padding). The headline trick: background-clip: text with color: transparent fills text with a gradient or image. Always include the -webkit- prefixed version for Safari. background-origin sets the positioning area.

css3
/* clip: where the background paints */
.border-box { background-clip: border-box; } /* default - under border */
.padding-box { background-clip: padding-box; } /* inside border */
.content-box { background-clip: content-box; } /* inside padding */

/* text clip: gradient-filled text */
.gradient-text {
  background: linear-gradient(to right, red, blue);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* origin: where positioning starts */
.origin { background-origin: content-box; }

Background Attachment & Parallax

background-attachment: fixed creates a parallax effect — the background stays fixed while content scrolls over it. scroll (default) moves with the element; local scrolls with the element's content (useful for scrollable boxes). Fixed attachment is janky and poorly supported on mobile; for production parallax, use transform: translate3d based on scroll position instead.

css3
/* fixed: background stays put during scroll (parallax) */
.parallax {
  background-image: url("hero.jpg");
  background-attachment: fixed;
  background-size: cover;
  height: 100vh;
}

/* scroll: scrolls with element (default) */
.scroll { background-attachment: scroll; }

/* local: scrolls with element's content */
.local {
  background-attachment: local; /* scrolls with inner content */
}

/* NOTE: background-attachment: fixed has poor mobile support
   prefer transform-based parallax for mobile */
08

Borders & Rounded Corners

border-radius

border-radius rounds corners, taking 1-4 values (clockwise from top-left). A slash separates horizontal/vertical radii for elliptical corners. border-radius: 50% on a square creates a perfect circle; on a rectangle, an ellipse. Longhand properties (border-top-left-radius) control individual corners. Rounded corners clip backgrounds, borders, and (with overflow: hidden) child content.

css3
.box { border-radius: 10px; }              /* all corners */
.box-2 { border-radius: 10px 20px; }        /* top-left/bottom-right  top-right/bottom-left */
.box-4 { border-radius: 10px 20px 30px 40px; } /* TL TR BR BL (clockwise) */

/* per-corner longhand */
.card {
  border-top-left-radius: 10px;
  border-top-right-radius: 20px;
  border-bottom-right-radius: 30px;
  border-bottom-left-radius: 40px;
}

/* elliptical corners (horizontal / vertical) */
.ellipse { border-radius: 50px / 25px; }

/* perfect circle (square element) */
.circle { border-radius: 50%; }

border-image

border-image slices an image into 9 regions and applies the corners/edges to a border. border-image-slice divides the source image; repeat controls how edges tile. A common modern trick: use border-image with a linear-gradient and slice 1 to create gradient borders. Note: border-image replaces border-style, so set border-width first.

css3
.frame {
  border: 20px solid transparent;
  border-image-source: url("frame.png");
  border-image-slice: 30;        /* slice image into 9 regions */
  border-image-width: 20px;
  border-image-outset: 0;
  border-image-repeat: stretch;  /* stretch | repeat | round | space */
}

/* shorthand */
.frame-shorthand {
  border: 20px solid transparent;
  border-image: url("frame.png") 30 stretch;
}

/* gradient border (using border-image) */
.grad-border {
  border: 4px solid;
  border-image: linear-gradient(to right, red, blue) 1;
}

Multiple Borders

CSS can't stack multiple border properties, but there are workarounds. box-shadow with zero offset and spread creates crisp outer borders (works with border-radius). outline draws a second border outside, but ignores border-radius. A pseudo-element gives full control including radius. Each technique has tradeoffs — choose based on whether you need rounded corners.

css3
/* technique 1: box-shadow (no rounded issue) */
.multi-shadow {
  box-shadow: 0 0 0 5px red, 0 0 0 10px blue;
}

/* technique 2: outline (outside border, no radius) */
.outline-border {
  border: 5px solid red;
  outline: 5px solid blue;
  outline-offset: 0;
}

/* technique 3: pseudo-element (full control) */
.pseudo-border {
  position: relative;
  border: 5px solid red;
}
.pseudo-border::after {
  content: "";
  position: absolute;
  inset: -10px;
  border: 5px solid blue;
  border-radius: inherit;
}

Gradient Borders

Gradient borders are tricky because border-image doesn't support border-radius. The robust solution: layer two backgrounds — a solid color clipped to padding-box and a gradient clipped to border-box, with a transparent border. This respects border-radius and works everywhere. The simpler border-image method works when you don't need rounded corners.

css3
/* method 1: border-image (works but no border-radius) */
.grad-1 {
  border: 4px solid;
  border-image: linear-gradient(to right, red, blue) 1;
}

/* method 2: background-clip trick (works with radius) */
.grad-2 {
  border: 4px solid transparent;
  border-radius: 12px;
  background:
    linear-gradient(white, white) padding-box,
    linear-gradient(to right, red, blue) border-box;
}

/* method 3: mask + pseudo (advanced, supports radius + animation) */

Outline & Focus Styles

outline draws outside the border and doesn't affect layout, making it ideal for focus indicators. outline-offset adds a gap. Critical accessibility rule: never set outline: none without a visible alternative. :focus-visible shows the outline only for keyboard navigation (not mouse clicks) — the modern, user-friendly approach to focus styling.

css3
/* outline: doesn't affect layout, draws outside border */
.focusable:focus {
  outline: 2px solid blue;
  outline-offset: 2px;  /* gap between element and outline */
}

/* never use outline: none without a replacement */
button:focus { outline: none; box-shadow: 0 0 0 3px rgba(0,150,255,0.5); }

/* :focus-visible: only shows outline for keyboard users */
button:focus-visible { outline: 2px solid blue; outline-offset: 2px; }
button:focus:not(:focus-visible) { outline: none; }

/* outline shorthand */
.box { outline: 2px dashed red; }
09

Transforms

2D Transforms

2D transforms — translate, rotate, scale, skew — move and distort elements without affecting layout (siblings don't reflow). Combining transforms applies right-to-left, so order matters: rotate before translate moves along the rotated axis. Because transforms don't trigger reflow, they're ideal for animations and are GPU-accelerated.

css3
.box { transform: translate(50px, 20px); }   /* move right 50, down 20 */
.box-x { transform: translateX(50px); }
.box-scale { transform: scale(1.5); }          /* grow 1.5x */
.box-scale-2 { transform: scale(1, 2); }       /* x 1, y 2 */
.box-rotate { transform: rotate(45deg); }      /* clockwise */
.box-skew { transform: skew(20deg, 10deg); }   /* skew x, y */

/* combine transforms (order matters!) */
.combo { transform: translate(20px, 0) rotate(45deg) scale(1.2); }

/* transform does NOT affect layout (no reflow) */

transform-origin

transform-origin sets the pivot point for transforms — default is the element's center. It accepts keywords (top left), pixels, or percentages. Changing the origin dramatically alters rotation and scaling behavior: a card flipping around its top edge needs transform-origin: top. For 3D transforms, a third z-axis value can be specified.

css3
.box { transform: rotate(45deg); }

/* default origin: center center (50% 50%) */
.center { transform-origin: center center; }

/* rotate around top-left corner */
.tl { transform-origin: top left; transform: rotate(45deg); }

/* custom point (x y) */
.custom { transform-origin: 20px 50px; transform: rotate(45deg); }

/* 3-value form (x y z) */
.three-d { transform-origin: 0 0 100px; }

/* percentage-based */
.pct { transform-origin: 100% 0; } /* top-right corner */

3D Transforms

3D transforms add rotateX, rotateY, rotateZ, translateZ, and scaleZ. perspective(n) must be applied as the first transform function (or on the parent) to enable depth — smaller values exaggerate the 3D effect. translate3d and scale3d are GPU-accelerated and perform better in animations than their 2D counterparts.

css3
.card { transform: perspective(1000px) rotateY(45deg); }

/* rotateX/Y/Z - rotate around 3D axes */
.flip { transform: rotateX(180deg); }   /* flip horizontally */
.turn { transform: rotateY(180deg); }   /* flip like a page */
.spin { transform: rotateZ(90deg); }    /* same as rotate() */

/* translate3d (GPU-accelerated) */
.move { transform: translate3d(50px, 20px, 100px); }

/* scale3d */
.grow { transform: scale3d(1.5, 1.5, 1.5); }

/* combined 3D */
.combo-3d { transform: perspective(800px) rotateX(15deg) rotateY(-20deg) translateZ(-50px); }

Perspective & 3D Space

perspective defines the viewer's distance — smaller = more dramatic 3D. Put it on the parent so all children share the same vanishing point. transform-style: preserve-3d lets nested elements live in a shared 3D space (essential for building cubes). backface-visibility: hidden hides an element's back — key for flip-card effects.

css3
/* perspective on parent: applies to all children uniformly */
.scene { perspective: 1000px; }
.cube { transform: rotateY(45deg); }

/* perspective() in transform: only affects that element */
.solo { transform: perspective(1000px) rotateY(45deg); }

/* perspective-origin: vanishing point */
.scene { perspective: 1000px; perspective-origin: top left; }

/* transform-style: preserve-3d lets children share 3D space */
.cube {
  transform-style: preserve-3d;
  transform: rotateY(30deg);
}
.cube .face { position: absolute; transform: translateZ(50px); }

/* backface-visibility: hide back of flipped element */
.card { backface-visibility: hidden; }

Animating Transforms (Performance)

Always animate transform and opacity — they're GPU-accelerated and don't trigger layout (reflow) or paint. Animating top/left/width/margin forces the browser to recalculate layout on every frame, causing jank. will-change: transform hints the browser to prepare a compositor layer in advance, but overuse wastes memory. Apply it just before an animation and remove after.

css3
/* GOOD: animate transform & opacity (GPU-accelerated) */
.smooth {
  transition: transform 0.3s ease;
}
.smooth:hover { transform: translateY(-5px); }

/* BAD: animating top/left triggers reflow */
.janky { transition: top 0.3s; }
.janky:hover { top: -5px; }

/* will-change: hint the browser to optimize */
.prepared {
  will-change: transform;  /* prepare for upcoming transform changes */
}

/* force GPU layer (use sparingly) */
.gpu { transform: translateZ(0); }
10

Transitions

Transition Shorthand

transition: property duration timing-function delay. The default is all 0s ease 0s — so just writing transition: 0.3s animates all changed properties. For performance and control, list specific properties. Multiple transitions are comma-separated. The transition goes on the element's default state (not :hover), so the reverse animation also plays.

css3
.btn {
  background: blue;
  /* shorthand: property duration timing-function delay */
  transition: background 0.3s ease 0s;
  
  /* common usage */
  transition: all 0.3s ease;
  
  /* multiple properties (comma-separated) */
  transition: background 0.3s, transform 0.2s ease-out;
  
  /* list the same duration on many properties */
  transition: 0.3s;
}

.btn:hover {
  background: darkblue;
  transform: scale(1.05);
}

Timing Functions

Timing functions control acceleration. ease (default) is smooth. ease-out feels most responsive for UI (fast start, slow end). linear feels unnatural for most motion. cubic-bezier(x1,y1,x2,y2) lets you craft custom curves — values outside 0-1 on the y-axis create overshoot/bounce. steps(n) creates discrete jumps, useful for sprite-sheet animations.

css3
.ease { transition: all 0.3s ease; }              /* default, smooth start/end */
.linear { transition: all 0.3s linear; }          /* constant speed */
.ease-in { transition: all 0.3s ease-in; }        /* slow start */
.ease-out { transition: all 0.3s ease-out; }      /* slow end (feels responsive) */
.ease-in-out { transition: all 0.3s ease-in-out; }/* slow both ends */

/* cubic-bezier: custom curve (P1x P1y P2x P2y) */
.custom { transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); } /* overshoot */

/* steps: jump in increments */
.steps { transition: all 0.3s steps(4, end); }

Multiple & Staggered Transitions

Different properties can have different durations and delays — useful for choreographed effects where one property finishes before another starts. transition-delay creates staggered entrances for lists; combine with nth-child to delay each item progressively. Delays also work for exits, but be careful: if the user hovers off mid-delay, the entrance may suddenly snap.

css3
.card {
  /* different durations per property */
  transition: transform 0.2s ease-out, box-shadow 0.4s ease;
  
  /* delay creates staggered effect */
  transition: background 0.3s, color 0.3s 0.1s; /* color starts after 0.1s */
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  background: #f5f5f5;
}

/* stagger multiple elements via transition-delay */
.item:nth-child(1) { transition-delay: 0s; }
.item:nth-child(2) { transition-delay: 0.1s; }
.item:nth-child(3) { transition-delay: 0.2s; }

Transitionable Properties

Some properties transition smoothly; others cause jank. The GPU-accelerated stars are transform, opacity, and filter. Colors and box-shadow are okay (they trigger paint, not layout). Avoid transitioning width, height, margin, top, left — they force layout recalculation on every frame. To animate a size change, use transform: scale() instead.

css3
/* WELL-SUPPORTED & performant */
.good {
  transition: transform 0.3s, opacity 0.3s, filter 0.3s;
}

/* OK but may trigger paint */
.okay {
  transition: background-color 0.3s, color 0.3s, border-color 0.3s;
  transition: box-shadow 0.3s;
}

/* AVOID: triggers layout (reflow) - janky */
.bad {
  transition: width 0.3s, height 0.3s, margin 0.3s, top 0.3s, left 0.3s;
}

/* if you must animate size, use transform: scale() instead */
.smooth-resize { transition: transform 0.3s; }

Transition Triggers

Transitions trigger on any property change: pseudo-class states (:hover, :focus, :checked), class changes (added/removed by JS), and media query changes. They interpolate between old and new values smoothly. Transitions only run on the way to the new state — both directions (in/out) animate. For more complex, looping, or multi-step motion, use @keyframes animations.

css3
/* hover trigger */
.btn { transition: all 0.3s; }
.btn:hover { transform: scale(1.1); }

/* focus trigger (accessibility-friendly) */
.input { transition: border-color 0.2s; }
.input:focus { border-color: blue; }

/* class change via JavaScript */
.modal { transition: opacity 0.3s; opacity: 0; }
.modal.open { opacity: 1; }

/* media query trigger */
.box { transition: width 0.3s; width: 100%; }
@media (min-width: 768px) { .box { width: 50%; } }

/* CSS state triggers (:checked) */
.toggle:checked + .menu { transition: max-height 0.3s; max-height: 500px; }
11

Animations

@keyframes

@keyframes defines an animation's sequence. Stops can be percentages (0%, 50%, 100%) or from/to (equivalent to 0%/100%). Properties not listed in a stop interpolate between defined stops. Only animatable properties (transform, opacity, colors) work — display, position, etc. don't. Define @keyframes once and reuse across many elements.

css3
@keyframes bounce {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(-30px); }
  100% { transform: translateY(0); }
}

/* from/to shorthand (2-stop) */
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* multiple properties at each stop */
@keyframes pulse {
  0%   { opacity: 1; transform: scale(1); }
  50%  { opacity: 0.5; transform: scale(1.1); }
  100% { opacity: 1; transform: scale(1); }
}

Animation Shorthand

The animation shorthand packs 8 properties in this order: name duration timing-function delay iteration-count direction fill-mode play-state. At minimum you need name and duration. Note: a time value without a unit ambiguity — the first time is duration, the second is delay. For readability in production, prefer longhand or partial shorthand.

css3
.bouncing {
  /* longhand */
  animation-name: bounce;
  animation-duration: 1s;
  animation-timing-function: ease-in-out;
  animation-delay: 0s;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-fill-mode: both;
  animation-play-state: running;
  
  /* shorthand: name duration timing delay count direction fill-mode play-state */
  animation: bounce 1s ease-in-out 0s infinite alternate both running;
  
  /* minimal: name + duration */
  animation: bounce 1s;
}

Iteration & Direction

animation-iteration-count controls repetition — infinite loops forever, fractional values play partial cycles. animation-direction: alternate plays the animation forward then backward on the next iteration, creating a seamless bounce. reverse plays backward, and alternate-reverse starts backward. Alternate is essential for ping-pong motions like pulsing or breathing effects.

css3
/* iteration-count: how many times to play */
.once { animation-iteration-count: 1; }    /* default */
.thrice { animation-iteration-count: 3; }
.forever { animation-iteration-count: infinite; }
.fractional { animation-iteration-count: 0.5; } /* half a play */

/* direction */
.normal { animation-direction: normal; }       /* 0% -> 100% */
.reverse { animation-direction: reverse; }     /* 100% -> 0% */
.alternate { animation-direction: alternate; } /* forward, then backward */
.alternate-rev { animation-direction: alternate-reverse; }

Animation Fill Mode

animation-fill-mode controls styles before and after the animation runs. none (default) reverts to the element's normal styles. forwards holds the final keyframe state after finishing — essential for entrance animations so elements don't snap back. backwards applies the first keyframe during the delay. both covers both cases. Use forwards or both for one-shot entrances.

css3
@keyframes slideIn {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}

/* none (default): reverts to original styles before/after */
.none { animation: slideIn 1s; }

/* forwards: holds the final (to) state after finishing */
.forwards { animation: slideIn 1s forwards; }

/* backwards: applies the (from) state during delay */
.backwards { animation: slideIn 1s 0.5s backwards; }

/* both: applies from during delay AND holds to after */
.both { animation: slideIn 1s 0.5s both; }

Play State & Pausing

animation-play-state: paused freezes an animation at its current frame; running resumes it. This is the cleanest way to pause on hover or via a JS-toggled class — the animation remembers its position rather than restarting. For performance, pause animations on elements that scroll out of view using IntersectionObserver. Combine with reduced-motion media queries for accessibility.

css3
.anim { animation: spin 2s linear infinite; }

/* pause on hover */
.anim:hover { animation-play-state: paused; }

/* pause via class (JS toggle) */
.anim.paused { animation-play-state: paused; }

/* useful for performance: pause off-screen animations */
.offscreen { animation-play-state: paused; }

/* resume */
.resumed { animation-play-state: running; }
12

Media Queries

Basic Syntax

Media queries apply styles conditionally. (min-width: 768px) is mobile-first — it applies on screens 768px and up. Combine with and (both must match), commas (OR), and not (negate). The mobile-first approach (min-width) is preferred: write base styles for small screens, then enhance for larger ones. This keeps small-screen CSS lean.

css3
/* apply styles when viewport is at least 768px wide */
@media (min-width: 768px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}

/* combine conditions with and */
@media (min-width: 768px) and (max-width: 1024px) {
  .box { padding: 20px; }
}

/* list of queries (OR) */
@media (max-width: 600px), (min-width: 1200px) {
  .text { font-size: 14px; }
}

/* negate with not */
@media not print {
  .screen-only { display: block; }
}

Min/Max Width Breakpoints

Breakpoints should be based on content, not specific devices, but common ranges work well: 640px (tablet), 1024px (desktop), 1280px (large). Mobile-first means using min-width and writing mobile styles as the base. The newer range syntax (width >= 768px) is more readable and supported in all modern browsers, replacing verbose min/max combinations.

css3
/* common breakpoints (mobile-first) */
/* base styles (mobile) */
.container { padding: 16px; }

@media (min-width: 640px)  { /* small tablets */ 
  .container { padding: 24px; }
}
@media (min-width: 1024px) { /* desktops */
  .container { padding: 32px; max-width: 1200px; margin: 0 auto; }
}
@media (min-width: 1280px) { /* large screens */
  .container { max-width: 1400px; }
}

/* range syntax (newer, cleaner) */
@media (width >= 768px) and (width <= 1024px) {
  .box { padding: 20px; }
}

Orientation & Aspect Ratio

Beyond width, media queries can target orientation (portrait/landscape), aspect-ratio, hover capability, and pointer type. @media (hover: hover) ensures hover styles only apply on devices with a real pointer (avoiding sticky hover on touch). (pointer: coarse) targets touch devices — perfect for enlarging tap targets. These features enable truly device-appropriate interfaces.

css3
/* portrait vs landscape */
@media (orientation: portrait) {
  .grid { grid-template-columns: 1fr; }
}
@media (orientation: landscape) {
  .grid { grid-template-columns: 1fr 1fr; }
}

/* aspect ratio */
@media (min-aspect-ratio: 16/9) {
  .video { width: 100%; height: auto; }
}

/* hover capability: devices that support hover */
@media (hover: hover) {
  .btn:hover { background: darkblue; }
}

/* pointer: fine (mouse) vs coarse (touch) */
@media (pointer: coarse) {
  .btn { padding: 16px 32px; } /* bigger tap targets */
}

Print Styles

Print media queries style how pages look when printed. Hide navigation, ads, and interactive elements; expand content to full width; force black-on-white for ink efficiency. break-inside: avoid prevents elements from splitting across pages, and break-before: page forces page breaks. The ::after trick on links reveals URLs in print — invaluable for printable articles.

css3
@media print {
  /* hide non-essential UI */
  .nav, .sidebar, .ads, .comments { display: none; }
  
  /* expand main content */
  .main { width: 100%; margin: 0; }
  
  /* force black text on white */
  body { color: black; background: white; }
  
  /* avoid breaking inside elements */
  h1, h2, img, table { break-inside: avoid; }
  
  /* page breaks before sections */
  h1 { break-before: page; }
  
  /* show URLs after links */
  a[href]::after { content: " (" attr(href) ")"; }
}

prefers-reduced-motion & color-scheme

prefers-reduced-motion is a critical accessibility feature — users with vestibular disorders need motion minimized. Always provide a reduced-motion fallback that disables or shortens animations. prefers-color-scheme lets you adapt to the user's OS theme automatically. For dark mode, also consider contrast, images, and shadows that work on dark backgrounds.

css3
/* respect users who prefer less motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* light/dark mode */
@media (prefers-color-scheme: dark) {
  body { background: #1a1a1a; color: #eee; }
  .card { background: #2a2a2a; }
}

@media (prefers-color-scheme: light) {
  body { background: white; color: #333; }
}
13

Responsive Design

Fluid Typography with clamp()

clamp(min, preferred, max) creates values that scale fluidly between bounds — perfect for typography and spacing that adapts without media queries. The preferred value is usually a vw-based expression. This eliminates many breakpoints. Pair with rem for accessibility (users can still scale root font-size). Fluid type improves perceived performance and readability across devices.

css3
/* clamp(min, preferred, max) - fluid between bounds */
h1 {
  /* scales from 1.5rem to 3rem as viewport grows */
  font-size: clamp(1.5rem, 5vw, 3rem);
}

p {
  /* readable body text on any screen */
  font-size: clamp(1rem, 2.5vw, 1.25rem);
}

/* fluid spacing */
.section {
  padding: clamp(1rem, 4vw, 3rem);
}

/* fluid container width */
.container {
  width: clamp(280px, 90vw, 1200px);
  margin: 0 auto;
}

Mobile-First Approach

Mobile-first means writing small-screen styles as the base, then using min-width media queries to enhance for larger screens. This produces leaner CSS (mobile downloads only what it needs) and forces a content-prioritized mindset. Desktop-first (max-width) forces mobile devices to download and override desktop styles — slower and harder to maintain.

css3
/* base = mobile styles (no media query) */
.card {
  width: 100%;
  padding: 12px;
  font-size: 14px;
}

/* progressively enhance for larger screens */
@media (min-width: 640px) {
  .card { width: 50%; padding: 16px; }
}

@media (min-width: 1024px) {
  .card { width: 33.33%; padding: 24px; font-size: 16px; }
}

/* AVOID desktop-first (forces mobile to override) */
/* @media (max-width: 640px) { ... } */

Container Queries

Container queries are a game-changer: they let components respond to their parent's size rather than the viewport. A card can adapt its layout whether it's in a narrow sidebar or a wide main area — true component-level responsiveness. container-type: inline-size queries width (most performant). Name containers to target them specifically, or use unnamed queries for the nearest ancestor.

css3
/* make an element a query container */
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}

/* styles based on the container's size, not the viewport */
@container sidebar (min-width: 400px) {
  .card {
    grid-template-columns: 1fr 1fr;
  }
}

/* inline-size = width only (most common, performant) */
/* size = both dimensions (more expensive) */

/* unnamed container query (uses nearest ancestor) */
.card-wrap { container-type: inline-size; }
@container (min-width: 500px) {
  .card { flex-direction: row; }
}

Viewport Units

vh/vw are percentages of the viewport. The problem: 100vh on mobile changes as the address bar shows/hides, causing jank. The newer svh/lvh/dvh units fix this — dvh updates dynamically, svh uses the smallest stable value. vmin/vmax are the smaller/larger viewport dimension — great for square elements that scale. Always pair vw font sizes with a rem/px floor via calc or clamp.

css3
.hero {
  height: 100vh;        /* full viewport height */
  /* small viewport units: account for mobile browser chrome */
  height: 100svh;       /* smallest possible viewport (stable) */
  height: 100lvh;       /* largest possible viewport */
  height: 100dvh;       /* dynamic - updates as chrome shows/hides */
}

.full-width { width: 100vw; }
.responsive-font { font-size: 5vw; }

/* vmin/vmax: smaller/larger of width or height */
.square { width: 50vmin; height: 50vmin; }
.banner { height: 30vmax; }

/* combining with calc for safety */
.hero-text { font-size: calc(16px + 2vw); }

aspect-ratio

aspect-ratio maintains a width:height ratio automatically — no more padding-top hacks for responsive video. The element's height is derived from its width (or vice versa). Combined with object-fit: cover on images, this prevents layout shift as images load. It's the cleanest way to reserve space for media and avoid cumulative layout shift (CLS) — a Core Web Vital.

css3
/* maintain a 16:9 ratio regardless of width */
.video {
  aspect-ratio: 16 / 9;
  width: 100%;
}

/* square avatar */
.avatar {
  aspect-ratio: 1;
  width: 80px;
  border-radius: 50%;
}

/* golden ratio card */
.card {
  aspect-ratio: 1.618;
}

/* combined with object-fit for responsive images */
.thumb {
  aspect-ratio: 4 / 3;
  width: 100%;
  object-fit: cover;
}
14

Custom Properties (Variables)

Defining & Using Variables

CSS custom properties (variables) are declared with --name and used with var(--name). Define them on :root for global access, or scope them to any element. Unlike preprocessor variables, they're live: changing a variable updates all uses in real time, and they cascade and inherit. var() accepts a fallback as the second argument, used when the variable is undefined.

css3
:root {
  --primary: #3490dc;
  --spacing: 16px;
  --max-width: 1200px;
}

.button {
  background: var(--primary);
  padding: var(--spacing);
  /* with fallback value (used if --primary undefined) */
  color: var(--text-color, #333);
}

/* override in a specific scope */
.dark-theme {
  --primary: #5a9fee;
}

Theming & Dark Mode

Custom properties make theming trivial: define semantic variables (--bg, --text, --primary), then override them in a [data-theme] selector. Toggling the data-theme attribute on <html> instantly swaps the entire theme — no duplicated stylesheets. Combine with prefers-color-scheme for automatic dark mode that respects a user's manual override. This is the modern, performant way to theme.

css3
:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --primary: #3490dc;
}

[data-theme="dark"] {
  --bg: #1a1a1a;
  --text: #ffffff;
  --primary: #5a9fee;
}

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

/* auto dark mode via media query */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #1a1a1a;
    --text: #ffffff;
  }
}

Cascading & Inheritance

Custom properties cascade and inherit like normal CSS. A variable defined on .card is available to .card and all descendants — but not siblings. Each element reads the variable's computed value at its own scope. @property (newer) lets you declare typed variables with a syntax, initial value, and inheritance flag — enabling smooth transitions and animations of custom properties.

css3
:root { --color: blue; }       /* global */
.card { --color: green; }      /* scoped to .card */
.card .inner { color: var(--color); } /* green - inherits from .card */

/* variables inherit like other properties */
.parent { --size: 20px; }
.child { font-size: var(--size); } /* 20px via inheritance */

/* but: each element reads its own computed value */
.a { --x: 10px; }
.b { /* --x is NOT defined here, even if .a is sibling */ }

/* @property: typed variables with initial value */
@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

JavaScript Interaction

CSS variables are fully accessible from JavaScript. Read with getComputedStyle(el).getPropertyValue('--name') (returns a string, often with a leading space — trim it). Set with el.style.setProperty('--name', value). This makes custom properties a powerful JS-to-CSS bridge: update a variable in JS and CSS reacts instantly — perfect for scroll-driven effects, theming, and dynamic layouts.

css3
/* read a variable */
const styles = getComputedStyle(document.documentElement);
const primary = styles.getPropertyValue("--primary"); /* "#3490dc" */

/* set a variable globally */
document.documentElement.style.setProperty("--primary", "#ff0000");

/* set on a specific element */
el.style.setProperty("--spacing", "24px");

/* inline style with variable */
<div style="--rotate: 45deg"></div>

/* useful pattern: expose JS values to CSS */
.root { --scroll: 0; }
.box { transform: rotate(calc(var(--scroll) * 1deg)); }

Fallbacks & Chaining

var() supports a fallback as its second argument, used only when the variable is undefined (not when it's invalid — invalid values make the whole property fall back to its default). Fallbacks can chain: var(--a, var(--b, #default)). This is useful for progressive enhancement and third-party component integration. Use @supports (--x: 0) to detect support, though all modern browsers support custom properties.

css3
.box {
  /* single fallback */
  color: var(--undefined, #333);
  
  /* chained: try --primary, fall back to --blue, then #00f */
  color: var(--primary, var(--blue, #0000ff));
  
  /* fallback in calc() */
  width: calc(100% - var(--sidebar, 250px));
  
  /* invalid values fall back to inherited/default */
  /* var() itself doesn't fail — the property does */
}

/* feature detection for custom property support */
@supports (--custom: property) {
  :root { --modern: true; }
}
15

Filters

Filter Functions

filter applies visual effects to an element. brightness/contrast/saturate take multipliers (1 = normal) or percentages. hue-rotate shifts all hues by an angle. Multiple filters chain left-to-right. Common use: grayscale images on hover, vintage photo effects, or dimming backgrounds. Filters affect the element, its background, borders, and children.

css3
.blur { filter: blur(4px); }              /* gaussian blur in px */
.bright { filter: brightness(1.5); }     /* 1 = normal, >1 brighter */
.contrast { filter: contrast(200%); }    /* 100% = normal */
.gray { filter: grayscale(100%); }       /* 0-100% */
.sepia { filter: sepia(80%); }
.hue { filter: hue-rotate(90deg); }      /* rotate hue */
.invert { filter: invert(100%); }        /* invert colors */
.saturate { filter: saturate(2); }       /* 1 = normal */
.opacity { filter: opacity(50%); }

/* combine multiple filters (left-to-right) */
.vintage { filter: sepia(60%) contrast(110%) brightness(90%) saturate(1.2); }

drop-shadow

drop-shadow is the filter equivalent of box-shadow — but it follows the element's actual alpha shape, not its box. This means shadows hug PNG/SVG cutouts and irregular shapes. Use it for icon shadows, glowing logos, or any non-rectangular element. Multiple drop-shadows can be layered. Note: it can be more expensive than box-shadow, so use judiciously.

css3
/* drop-shadow follows the alpha shape (unlike box-shadow) */
.cutout {
  filter: drop-shadow(4px 4px 8px rgba(0,0,0,0.4));
}

/* shadows on PNG with transparency - hugs the shape */
.logo {
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));
}

/* glow effect on text or SVG */
.icon {
  filter: drop-shadow(0 0 8px rgba(0, 150, 255, 0.8));
}

/* multiple drop-shadows layer */
.deep {
  filter: drop-shadow(0 1px 1px rgba(0,0,0,0.5))
          drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}

backdrop-filter

backdrop-filter applies a filter to everything behind an element — the signature of glassmorphism. A semi-transparent background plus backdrop-filter: blur() creates the frosted-glass look. Always include the -webkit- prefix for Safari. The element needs some transparency in its background for the effect to show. Combine with saturate() for vibrant results, and add a subtle border to simulate edge reflection.

css3
/* frosted glass effect */
.glass {
  background: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px); /* Safari */
  border: 1px solid rgba(255, 255, 255, 0.3);
}

/* saturate + blur for vivid glass */
.vivid-glass {
  backdrop-filter: blur(16px) saturate(180%);
}

/* dark glass for dark mode */
.dark-glass {
  background: rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(12px);
}

Combining Filters

Multiple filters apply left-to-right, so order affects the result (grayscale before hue-rotate has nothing to rotate, for instance). Filters are animatable and GPU-accelerated — great for hover effects. Pair filter with mix-blend-mode to blend an element with its backdrop. For dynamic control, store the filter chain in a custom property and update via JS.

css3
/* order matters: each filter feeds the next */
.artsy {
  filter: grayscale(100%) contrast(150%) brightness(110%);
}

/* animate filters smoothly */
img { transition: filter 0.3s; }
img:hover { filter: grayscale(0%) saturate(1.4); }

/* use CSS variables for dynamic filters */
.dynamic { filter: var(--photo-filter, none); }

/* layer with other effects */
.complex {
  filter: blur(1px) brightness(1.1);
  mix-blend-mode: multiply;
  opacity: 0.9;
}

Filter Performance

Filters are GPU-accelerated but not free. Large blurs on full-screen elements cause heavy repaints and jank, especially on mobile. Keep blur radii small, limit the filtered area, and apply will-change: filter before triggering animations. backdrop-filter is particularly expensive — use it on small UI elements, not full sections. contain: paint isolates the repaint area for better performance.

css3
/* AVOID: large blur on full-screen elements */
.janky { filter: blur(20px); } /* causes repaints */

/* BETTER: pre-blurred image, or limit blur area */
.smooth {
  filter: blur(2px); /* small radius is cheaper */
}

/* backdrop-filter is expensive - use sparingly */
/* contain: paint can limit repaint area */
.contained {
  contain: paint;
  backdrop-filter: blur(10px);
}

/* will-change prepares the GPU layer */
.prepared {
  will-change: filter;
  filter: blur(4px);
}
16

Pseudo-elements

::before & ::after

::before and ::after insert generated content before/after an element's actual content. The content property is required (even if empty string for decorative elements). Content can be text, escaped Unicode, or attribute values via attr(). These pseudo-elements default to inline — set display: block for box-like decoration. They're perfect for icons, decorative lines, quotes, and clearing floats.

css3
.quote::before { content: "\201C"; }  /* opening quote */
.quote::after  { content: "\201D"; }  /* closing quote */

/* icon via Unicode */
.warning::before { content: "\26A0"; margin-right: 6px; }

/* decorative element with content: "" */
.card::after {
  content: "";
  display: block;
  width: 50px; height: 2px;
  background: currentColor;
  margin-top: 12px;
}

/* content can use attributes */
a[href^="http"]::after {
  content: " (" attr(href) ")";
  font-size: 0.8em;
}

::first-letter & ::first-line

::first-letter styles the first character — perfect for drop caps in articles. ::first-line styles the first formatted line, which dynamically adjusts as the viewport changes width. Both only work on block-level elements. Note that ::first-letter only applies when the first character isn't preceded by other content (like images). They're typography essentials for editorial layouts.

css3
article p::first-letter {
  font-size: 3em;
  font-weight: bold;
  float: left;
  line-height: 0.8;
  margin-right: 8px;
  /* drop cap effect */
}

article p::first-line {
  text-transform: uppercase;
  letter-spacing: 1px;
  font-weight: bold;
}

/* only works on block-level elements */
/* affects the first formatted line (reflows with width) */

::selection

::selection styles the portion of text a user highlights. Only a handful of properties apply: color, background-color, text-shadow, and text-decoration. Use it to brand the selection color to match your site. Apply globally with ::selection or scope to specific elements. Older Firefox needed ::-moz-selection, but modern Firefox supports the standard ::selection.

css3
/* style highlighted/selected text */
::selection {
  background: #b3d4fc;
  color: black;
  text-shadow: none;
}

/* scoped to specific elements */
p::selection { background: yellow; }
.highlight::selection { background: gold; color: #333; }

/* note: only a few properties work:
   color, background-color, text-shadow, 
   text-decoration (and its longhands) */

/* firefox historically used ::-moz-selection */

::placeholder & ::marker

::placeholder styles the placeholder text in form inputs — set opacity: 1 because Firefox defaults to a translucent placeholder. ::marker styles list-item bullets or numbers, and supports a limited set of properties (color, content, font, direction). You can replace bullets with custom content (Unicode, counters). These pseudo-elements bring form and list styling fully under designer control.

css3
/* style input placeholder text */
input::placeholder {
  color: #999;
  font-style: italic;
  opacity: 1; /* Firefox applies opacity < 1 by default */
}

/* style list item markers (bullets/numbers) */
ul li::marker {
  color: red;
  font-size: 1.2em;
  content: "\25CF"; /* custom bullet */
}

ol li::marker {
  content: counter(list-item) ". ";
  color: blue;
  font-weight: bold;
}

/* ::marker supports: color, content, font properties,
   direction, unicode-bidi, text-transform, white-space */

Generated Content & Counters

Generated content via ::before/::after can include counter() values, attribute values (attr()), or string literals — enabling CSS-only numbering, icons, and tooltips without extra DOM. counters() produces nested numbering like '1.2.3'. Tooltips built with attr(data-tooltip) and a hover transition are a lightweight alternative to JS libraries. Remember: generated content isn't in the DOM and isn't read by some screen readers — don't put critical content there.

css3
/* counter in generated content */
h2::before {
  content: "Section " counter(section) ": ";
  color: gray;
}

/* attr() reads HTML attributes */
a::after {
  content: attr(data-icon);
}

/* counters() for nested numbering */
ol { counter-reset: item; }
li { display: block; }
li::before {
  content: counters(item, ".") " ";
  counter-increment: item;
}

/* CSS-only tooltip */
.tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  background: black; color: white;
  padding: 4px 8px;
  opacity: 0; transition: opacity 0.2s;
}
.tooltip:hover::after { opacity: 1; }
17

Counters

counter-reset & counter-increment

CSS counters let you number elements without JavaScript. counter-reset initializes a counter (defaults to 0, but you can start elsewhere). counter-increment increases it (default by 1, but any integer works — even negative). Display the value with counter() inside content. Counters are scoped to the element they're reset on and its descendants, enabling nested numbering.

css3
body { counter-reset: section; }   /* initialize to 0 */

h2 {
  counter-increment: section;  /* +1 each h2 */
}

h2::before {
  content: "Section " counter(section) ": ";
}

/* custom increment value */
ol { counter-reset: item; }
li { counter-increment: item 1; }  /* +1 (default) */
li.big { counter-increment: item 2; } /* +2 */

/* reset to a custom starting value */
.start-5 { counter-reset: item 4; } /* next increment = 5 */

counter() Function

counter(name) outputs the current value of a counter, and counter(name, style) applies a list style: decimal (default), lower/upper-alpha, lower/upper-roman, disc, square, and more. You can run multiple counters in parallel and combine them in content — perfect for figures numbered per chapter (Figure 3-2). Counter values are computed at render time, so they update automatically when the DOM changes.

css3
body { counter-reset: section; }
h2 { counter-increment: section; }
h2::before {
  content: "Chapter " counter(section) ". ";
  color: navy;
}

/* counter style (decimal, lower-alpha, upper-roman, etc.) */
h2::before {
  content: counter(section, upper-roman) ". ";
}

/* multiple counters */
body { counter-reset: section figure; }
h2 { counter-increment: section; }
figcaption { counter-increment: figure; }
figcaption::before {
  content: "Figure " counter(section) "-" counter(figure) ": ";
}

Nested Counters with counters()

counters(name, separator) is the nested-counter function: it concatenates the counter's value at every ancestor scope, separated by the given string. This produces outlines like '1.2.3' automatically — the same counter name is reset on each nested <ol>. Without counters() (plural), nested lists would all share the same numbering. The separator can be any string, like '.' or '-'.

css3
ol { counter-reset: item; list-style: none; }
li::before {
  counter-increment: item;
  /* counters() concatenates ancestor counters with separator */
  content: counters(item, ".") " ";
}

/* produces: 1 / 1.1 / 1.2 / 2 / 2.1 / 2.1.1 ... */
<ol>
  <li>Item
    <ol>
      <li>Sub-item</li>
      <li>Sub-item</li>
    </ol>
  </li>
  <li>Item</li>
</ol>

/* custom separator */
li::before { content: counters(item, "-") ": "; }

Counter Styling

Counters support many built-in styles: decimal, upper-roman, lower-alpha, lower-greek, cjk-decimal, and more. @counter-style lets you define custom styles with a symbol set, system (cyclic, additive, extends), and options like pad (leading zeros), prefix/suffix, and range. This is great for themed lists, emoji bullets, or localized numbering without JavaScript.

css3
/* built-in styles */
.decimal { content: counter(c, decimal); }
.roman { content: counter(c, upper-roman); }       /* I, II, III */
.alpha { content: counter(c, lower-alpha); }        /* a, b, c */
.greek { content: counter(c, lower-greek); }        /* alpha, beta, gamma */
.cjk { content: counter(c, cjk-decimal); }          /* CJK numerals */

/* @counter-style: define a custom style */
@counter-style thumbs {
  system: cyclic;
  symbols: "\1F44D"; /* thumbs up emoji */
  suffix: " ";
}
.like-list { list-style-type: thumbs; }

/* leading zeros via pad */
@counter-style padded {
  system: extends decimal;
  pad: 3 "0"; /* 001, 002, ... */
}

Practical Counter Patterns

Counters shine for numbered step indicators, table-of-contents, and outlines. Style the ::before with a circular badge, color, and number to create numbered step lists without manual numbering. Edit the DOM and numbers update automatically. Caveat: counter values can't easily be read by JavaScript — they're display-only. For logic based on counts, use JS or CSS :nth-child instead.

css3
/* numbered steps */
.steps { counter-reset: step; }
.steps li { counter-increment: step; }
.steps li::before {
  content: counter(step);
  display: inline-block;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: var(--primary);
  color: white;
  text-align: center;
  line-height: 28px;
  margin-right: 10px;
}

/* "you've completed N sections" via counters (limited - 
   counters can't be read by JS easily, use them for display) */
18

Multi-column Layout

column-count & column-width

Multi-column layout flows content into newspaper-like columns. column-count fixes the number; column-width lets the browser create as many 250px columns as fit (responsive without media queries). The shorthand columns takes count and/or width. Multi-column is ideal for long-form reading text, but be cautious: short content can cause uneven column heights.

css3
.article {
  /* fixed number of columns */
  column-count: 3;
  
  /* OR width-based (browser decides count) */
  column-width: 250px;
  
  /* shorthand (count width) */
  columns: 3 250px;
  columns: 3;       /* 3 columns */
  columns: 250px;   /* 250px columns, auto count */
}

/* responsive: width lets columns collapse on small screens */
.fluid { column-width: 200px; } /* 1 column on mobile, more on desktop */

column-gap & column-rule

column-gap sets the space between columns (also used by flexbox and grid now). column-rule draws a vertical divider line in the gap — it's the column equivalent of border, with the same shorthand. The rule doesn't take up extra width; it sits centered in the gap. Use a subtle color to delineate columns without being intrusive.

css3
.article {
  column-count: 3;
  column-gap: 30px;          /* space between columns */
  
  /* divider line (like border) */
  column-rule: 1px solid #ddd; /* shorthand */
  column-rule-width: 1px;
  column-rule-style: solid;
  column-rule-color: #ddd;
}

/* rule sits in the gap, doesn't add width */

column-span

column-span: all makes an element break across all columns — perfect for a magazine-style headline that spans the full width with columns flowing above and below. column-span: none (default) keeps content flowing through columns. Be aware this can cause significant reflow and has had inconsistent support; test thoroughly. It's most reliable for headings between column blocks.

css3
.article {
  column-count: 3;
}

/* heading spans all columns (like a magazine) */
.article h2 {
  column-span: all;
  /* resets above the heading, columns resume below */
}

/* default: content flows through columns */
.normal { column-span: none; }

/* note: column-span: all can cause reflow and is 
   not supported in older Firefox */

break-inside & column breaks

Multi-column content can awkwardly split elements across columns. break-inside: avoid keeps an element (like a card or figure) intact within one column. break-before/break-after: column forces column breaks. The older page-break-* properties work as aliases. A common hack was display: inline-block with width: 100%, but break-inside: avoid is the modern, correct approach.

css3
/* prevent an element from splitting across columns */
.card {
  break-inside: avoid;        /* modern */
  -webkit-column-break-inside: avoid; /* old webkit */
  page-break-inside: avoid;   /* old */
  display: inline-block;      /* hack for older browsers */
  width: 100%;
}

/* control where columns break */
h2 { break-after: column; }   /* force new column after */
h3 { break-before: column; }  /* force new column before */
p  { break-inside: avoid; }   /* keep paragraph together */

column-fill & Balancing

column-fill controls how content distributes across columns. balance (default) equalizes heights so columns end roughly aligned — visually neat. auto fills the first column completely before moving to the next, requiring a fixed height; this matches print behavior. Balanced columns are nicer for general use; auto is better when content should flow strictly top-to-bottom.

css3
.article {
  column-count: 3;
  
  /* balance: equalize column heights (default in most browsers) */
  column-fill: balance;
  
  /* auto: fill first column fully, then next (needs height) */
  column-fill: auto;
  height: 500px; /* required for auto */
}

/* balanced columns look neat but can split awkwardly;
   auto is more like print, fills top-to-bottom */
19

User Interface

Outline & Accessibility

Visible focus indicators are an accessibility requirement. :focus-visible shows the outline only for keyboard users (not mouse), the best practice for focus styling. Never set outline: none without a replacement (like a box-shadow ring). outline-offset adds breathing room between the element and the outline. Modern browsers support rounded outlines via border-radius affecting the outline shape.

css3
/* visible focus ring for keyboard users */
button:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

/* never outline: none without a replacement */
/* custom focus ring via box-shadow */
button:focus {
  outline: none;
  box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.5);
}

/* outline shorthand */
.outlined { outline: 2px dashed red; outline-offset: 4px; }

/* thick rounded outline (modern browsers) */
button:focus-visible {
  outline: 3px solid blue;
  outline-offset: 2px;
  border-radius: 4px;
}

resize

resize controls whether the user can drag-resize an element. textarea defaults to resizable; setting resize: none disables it. vertical/horizontal/both constrain the direction. For resize to work, the element must have overflow other than visible (set overflow: auto or hidden). Useful for textareas, code editors, or any user-adjustable panel.

css3
textarea {
  /* allow vertical resize only (default for textarea) */
  resize: vertical;
  
  /* allow both axes */
  resize: both;
  
  /* horizontal only */
  resize: horizontal;
  
  /* disable resizing */
  .locked { resize: none; }
  
  /* default: inline/block elements can't be resized;
     textarea default is both in most browsers */
}

/* note: resize only works on overflow != visible */

caret-color & user-select

caret-color sets the color of the blinking cursor in editable elements — a small but nice touch for branded forms. user-select controls whether text can be selected: none disables it (for UI controls), all selects the entire element on click (good for code blocks or emails), and contain keeps selection within an element. Avoid user-select: none on body — it harms accessibility.

css3
/* color of the text cursor in inputs */
input, textarea {
  caret-color: #0066cc;  /* default: current color */
}

/* custom thick caret (limited support) */
input { caret-color: red; }

/* control text selection by the user */
.no-select { user-select: none; }        /* disable selection */
.select-all { user-select: all; }        /* select whole element on click */
.select-text { user-select: text; }      /* normal text selection */
.select-contain { user-select: contain; } /* selection stays within */

/* email addresses, code blocks often user-select: all */

appearance & Form Styling

appearance: none strips native OS styling from form controls, letting you style them fully with CSS — essential for consistent cross-browser form design. After removing the native select arrow, add a custom background-image. Custom checkboxes use appearance: none plus :checked for state. Always prefix with -webkit-/-moz- for full support. Test thoroughly across browsers — some controls (date, color) remain hard to fully restyle.

css3
/* remove native widget styling */
input, select, button {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

/* custom select arrow (after removing native) */
select {
  background-image: url("arrow.svg");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 30px;
}

/* style checkbox/radio with appearance: none */
input[type="checkbox"] {
  appearance: none;
  width: 20px; height: 20px;
  border: 2px solid #999;
  border-radius: 4px;
}
input[type="checkbox"]:checked {
  background: var(--primary);
  border-color: var(--primary);
}

scroll-behavior & Scrollbar

scroll-behavior: smooth on <html> makes anchor-link clicks animate to the target instead of jumping — pair with prefers-reduced-motion to disable for sensitive users. Custom scrollbars: the standard scrollbar-width/scrollbar-color properties work in Firefox and modern Chrome; the ::-webkit-scrollbar pseudo-elements give finer control in WebKit/Blink. Subtle custom scrollbars polish the UX of scrollable areas.

css3
/* smooth scrolling for anchor links */
html { scroll-behavior: smooth; }

/* respect reduced motion */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* custom scrollbar (WebKit) */
.scroll-area::-webkit-scrollbar { width: 8px; }
.scroll-area::-webkit-scrollbar-track { background: #f1f1f1; }
.scroll-area::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
.scroll-area::-webkit-scrollbar-thumb:hover { background: #555; }

/* standard scrollbar-color (Firefox + spec) */
.scroll-area {
  scrollbar-width: thin;
  scrollbar-color: #888 #f1f1f1;
}
20

Advanced & Modern Features

Scroll Snap

Scroll Snap creates carousel-like or full-page scrolling without JavaScript. scroll-snap-type: x/y + mandatory/proximity controls the axis and strictness. Children with scroll-snap-align snap to start/center/end. mandatory forces snapping (good for full-page sections); proximity only snaps when close (gentler, for image galleries). Combine with scroll-margin for offsetting snap points below sticky headers.

css3
.gallery {
  scroll-snap-type: x mandatory;  /* snap horizontally, must snap */
  overflow-x: auto;
  display: flex;
}

.gallery > img {
  scroll-snap-align: center;   /* start | center | end */
  scroll-snap-stop: always;    /* can't skip snaps */
  flex: 0 0 80%;
}

/* vertical full-page snap */
.pages {
  scroll-snap-type: y mandatory;
  height: 100vh;
  overflow-y: scroll;
}
.page { scroll-snap-align: start; height: 100vh; }

/* proximity: only snaps when close (gentler) */
.soft { scroll-snap-type: y proximity; }

object-fit & object-position

object-fit controls how an <img> or <video> fills its box — the media equivalent of background-size. cover fills the box and crops overflow; contain fits the whole media with possible empty space; fill stretches (distorts). object-position sets the focal point — useful when cover crops important parts of an image. This eliminates the need for background-image hacks for responsive media.

css3
/* cover: fill box, crop overflow (like background-size: cover) */
.avatar img {
  width: 100px; height: 100px;
  object-fit: cover;        /* cover | contain | fill | none | scale-down */
  object-position: center;  /* position the focal point */
  border-radius: 50%;
}

/* contain: whole image visible, may letterbox */
.preview img {
  width: 300px; height: 200px;
  object-fit: contain;
  background: #eee;
}

/* object-position shifts the visible area */
.banner img {
  width: 100%; height: 300px;
  object-fit: cover;
  object-position: top;  /* show top of image */
}

will-change

will-change hints to the browser that a property will change soon, letting it pre-allocate GPU layers and optimize. Apply it just before an animation and remove after — permanent use on many elements wastes memory and can hurt performance. List only properties that will actually animate (transform, opacity, filter). Overuse triggers the opposite of the intended effect, so use sparingly and intentionally.

css3
/* hint that an element will animate transform */
.card { will-change: transform; }

/* remove when not needed (frees GPU memory) */
.card.idle { will-change: auto; }

/* multiple properties */
.complex { will-change: transform, opacity; }

/* AVOID: permanent will-change on many elements */
/* .all { will-change: transform, opacity, top, left; } */ /* bad */

/* apply just before animation, remove after */
.prepare { will-change: transform; }
/* JS: el.classList.add('prepare'); 
   setTimeout(() => el.classList.remove('prepare'), 1000); */

contain

contain tells the browser that an element is independent of the rest of the page, enabling major performance optimizations — changes inside a contained element don't trigger recalculation outside it. layout, paint, style, and size are the isolation types. strict (all four, including size) is the strongest. Apply contain to repeated widgets, list items, or ads to limit reflow/repaint cost. size requires explicit dimensions.

css3
/* isolate an element's layout/paint/style from the rest */
.widget {
  contain: layout paint style;  /* or: strict, content */
  
  /* size: the element's size is independent of its children */
  contain: size;  /* needs explicit dimensions */
}

/* layout: changes inside don't affect outside layout */
/* paint: children don't render outside the box (clipped) */
/* style: counters and quotes are scoped */
/* size: element ignores children for sizing */

/* full isolation */
.isolated { contain: strict; } /* = size layout paint style */
.content-isolated { contain: content; } /* = layout paint style */

/* useful for performance on complex widgets, lists, ads */

aspect-ratio & gap Universal

aspect-ratio maintains a width:height ratio on any element, preventing layout shift as media loads. gap (and its longhands row-gap/column-gap) is the modern spacing tool, working in flexbox, grid, and multi-column — eliminating the .item + margin + :last-child hacks. Both features are universally supported in modern browsers and together solve two of the most common CSS pain points: maintaining media ratios and spacing items cleanly.

css3
/* aspect-ratio works on any element */
.video-wrap {
  aspect-ratio: 16 / 9;
  background: black;
}

/* gap works in flex, grid, and multi-column */
.flex { display: flex; gap: 16px; }
.grid { display: grid; gap: 16px; }
.cols { column-count: 3; column-gap: 16px; }

/* row-gap / column-gap longhands */
.layout {
  display: grid;
  row-gap: 24px;
  column-gap: 16px;
}

/* gap replaced margin-based spacing hacks */
/* (no more .item:last-child { margin-right: 0 }) */

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.