CSS — Engineering
Notes for Real Layouts
A working reference for CSS as it's used to ship interfaces — the cascade and specificity that decide who wins, modern layout with Grid and Flexbox, custom properties, and the container/logical features that replaced a decade of hacks.
.card {} /* class */
#app {} /* id (avoid for styling) */
a[target="_blank"] {} /* attribute */
.a > .b {} /* direct child */
.a .b {} /* any descendant */
.a + .b {} /* next sibling */
.a ~ .b {} /* any later sibling */
:is(h1, h2, h3) a {} /* match-any group */
:where(.a, .b) {} /* like :is but 0 specificity */Specificity is counted as three numbers: a = id selectors, b = class/attribute/pseudo-class, c = element/pseudo-element. Inline styles outrank all of them; !important overrides everything (and is a smell).
#nav .item a /* (1,1,1) */ .nav .item a /* (0,2,1) — loses to above */ a.btn.lg /* (0,2,1) */ :where(#x) a /* (0,0,1) — :where zeroes it */
:where() for resets. When everything is (0,1,0)-ish, source order decides — which is predictable.@layer reset, base, components, utilities; @layer components { .btn { color: blue; } } /* a later layer always beats an earlier one, regardless of selector specificity */
Typography-ish properties inherit (color, font-*, line-height, visibility). Box properties do not (margin, padding, border, display). Force it either way with the universal keywords:
.x { color: inherit; } /* take parent's */
.x { margin: unset; } /* inherited→inherit, else initial */
.x { all: revert; } /* back to the UA stylesheet */*, *::before, *::after { box-sizing: border-box; }
/* width now INCLUDES padding + border —
the intuitive behaviour */gap in flex/grid layouts instead of relying on margins.| Unit | Relative to | Use for |
|---|---|---|
rem | root font size | type scale, spacing (respects user zoom) |
em | element font size | padding that scales with text |
% | parent | fluid widths |
vw/vh/dvh | viewport | full-screen sections (dvh handles mobile bars) |
ch | "0" width | readable line length (~60ch) |
fr | grid free space | grid track sizing |
rem for font size and spacing so the layout respects the user's browser font preference. Fixed px everywhere is an accessibility failure.color: rgb(255 0 0 / .5); /* modern slash-alpha */ color: hsl(210 100% 50%); color: oklch(0.7 0.15 250); /* perceptually uniform */ color: color-mix(in srgb, blue 30%, transparent); /* relative color syntax */ --hover: oklch(from var(--brand) calc(l - .1) c h);
h1 {
font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
line-height: 1.1; /* tighter for headings */
text-wrap: balance; /* even multi-line headings */
}
p { max-width: 65ch; line-height: 1.6; text-wrap: pretty; }:root { --space: 1rem; --brand: #38bdf8; }
.card { padding: var(--space, 1rem); }
.card.tight { --space: .5rem; } /* scoped override */
[data-theme="dark"] { --brand: #7dd3fc; }--brand on a parent themes everything below it. This is how runtime theming works without a build step.width: min(100%, 60ch); /* fluid but capped */ width: max(320px, 40%); /* floor */ padding: clamp(1rem, 5vw, 3rem); /* min, preferred, max */ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
minmax(220px, 1fr) + auto-fit is a complete responsive card grid in one line — no media queries needed.Modern layout is Flexbox and Grid; float is for wrapping text around an image and nothing else. display: flow-root creates a clean block formatting context (the modern clearfix). display: contents removes a box from layout while keeping its children — useful but breaks accessibility if misused.
.row {
display: flex;
gap: 1rem;
justify-content: space-between; /* main axis */
align-items: center; /* cross axis */
flex-wrap: wrap;
}
.grow { flex: 1; } /* 1 1 0 — fill space */
.fixed { flex: 0 0 auto; } /* don't grow/shrink */.layout {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"head head"
"side main"
"foot foot";
min-height: 100dvh;
}
header { grid-area: head; }
main { grid-area: main; }.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
}.bar { position: sticky; top: 0; z-index: 10; }
/* sticky needs a scroll container + a threshold (top) */z-index only works within a stacking context. transform, opacity < 1, filter and will-change all create one — which is why a high z-index sometimes still renders behind a sibling. Fix the context, don't escalate the number..hero {
background:
radial-gradient(circle at top, #0003, transparent),
linear-gradient(135deg, #1e293b, #0f172a);
border-radius: 1rem;
outline: 1px solid #fff2; /* doesn't affect layout */
}.x { translate: 0 10px; scale: 1.05; rotate: 3deg; }
/* composable + independently animatable (modern) */
.y { transform: translate3d(0,10px,0); } /* old form */transform and opacity for 60fps — they're composited off the main thread. Animating width/top/margin triggers layout on every frame..btn {
transition: background-color .2s ease, transform .2s ease;
}
.btn:hover { transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; }
}transition: all — it animates properties you didn't intend (including expensive ones) and is a measurable perf cost. List the properties explicitly.@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .4; } } .dot { animation: pulse 2s ease-in-out infinite; }
@media (768px <= width < 1024px) { ... } /* modern range */ @media (prefers-color-scheme: dark) { ... } @media (hover: hover) { .x:hover { ... } } /* skip on touch */ @supports (display: grid) { ... }
min-width queries add complexity for larger ones. It degrades gracefully and ships less CSS to the constrained device..card { container-type: inline-size; }
@container (min-width: 400px) {
.card .layout { display: grid; grid-template-columns: 1fr 2fr; }
}.field:has(input:invalid) { border-color: red; }
/* style a label based on its input's state */
article:has(> img) { padding-block: 0; }:has() is the long-requested "parent selector" — it removes an enormous category of JavaScript that existed only to toggle a class on an ancestor.- One low, flat specificity tier — single classes, no id styling,
@layerfor order - Design tokens as custom properties (color, space, radius, type scale) — one source of truth
- Co-locate styles with components (CSS Modules / scoped) so deletion is safe
- Logical properties (
margin-inline,padding-block) for RTL/i18n from day one - A reset/normalize in the first
@layerso UA differences never surprise you
@scope, or a strict naming convention) is what makes a large stylesheet refactorable instead of append-only./* flex */ .c { display: flex; place-items: center; min-height: 100dvh; } /* grid — shortest */ .c { display: grid; place-content: center; } /* absolute */ .c { position: absolute; inset: 0; margin: auto; width: fit-content; }
| Goal | Declaration |
|---|---|
| Responsive grid | grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)) |
| Readable column | max-width: 65ch |
| Fluid type | font-size: clamp(1rem, 2.5vw, 1.5rem) |
| Aspect ratio | aspect-ratio: 16 / 9 |
| Truncate text | text-overflow: ellipsis; overflow: hidden; white-space: nowrap |
| Full-bleed bg | min-height: 100dvh (handles mobile bars) |