CSS decides how a web page looks: colour, type, spacing and layout. The
reference below is grouped by what you are trying to do, and the filter box
searches all of it at once. Type grid for every grid row, or baseline for
everything new enough that you should check your audience first.
Checked against CSS as of September 2026. Everything is Baseline Widely
available, working in Chrome, Edge, Firefox and Safari on desktop and mobile for
at least two and a half years, unless its notes say Newly available with the
month it reached every major browser, or Not Baseline yet. Every snippet passed
a CSS validator, and the specificity numbers were calculated, not guessed. Class
names like .card and colours like --brand are placeholders for your own. For
the elements these selectors target, see the
HTML cheat sheet.
Searches the task, the command and the third column. Press / from anywhere on the page.
269 commands
Adding CSS to a page
| Task | Code | Notes |
|---|---|---|
| Link a stylesheet | <link rel="stylesheet" href="styles.css"> | In the head. The browser caches the file, so every page after the first loads faster |
| CSS inside the page | <style> p { line-height: 1.6; } </style> | In the head. Fine for a single page or critical above-the-fold styles |
| Style one element | <p style="color: navy">Inline</p> | Beats every selector. Only an !important rule in a stylesheet overrides it |
| Anatomy of a rule | h1 { color: navy; font-size: 2rem; } | h1 is the selector. Each property: value pair is a declaration, ended by a semicolon |
| Comment | /* Explain why, not what */ | The only comment syntax. A // line comment is not CSS and breaks the rule after it |
| Pull in another stylesheet | @import url("reset.css"); | Must come before every other rule except @charset and @layer. It delays rendering, so link tags load faster |
| Pull a file into a cascade layer | @import url("reset.css") layer(reset); | Everything in reset.css lands in the reset layer. See the cascade group |
Selectors
A selector picks the elements a rule applies to. Classes do most of the work: they are reusable, and low enough in specificity to override later.
| Task | Code | Notes |
|---|---|---|
| Every element of a type | p { line-height: 1.6; } | |
| Elements with a class | .card { padding: 1rem; } | Matches class="card", and also class="card featured" |
| Element with both classes | .btn.primary { background: navy; } | No space between them. With a space it means a .primary inside a .btn |
| The element with an id | #site-header { position: sticky; top: 0; } | One id beats any number of classes, so keep ids for links and scripts |
| Several selectors, one rule | h1, h2, h3 { font-family: Georgia, serif; } | A selector list. One invalid selector in it drops the whole rule |
| Descendant, at any depth | nav a { text-decoration: none; } | A space. Every a somewhere inside a nav |
| Direct child only | ul > li { list-style: square; } | Skips the li elements of a nested list inside |
| The sibling straight after | h2 + p { margin-top: 0; } | Only the p that immediately follows an h2 |
| Every later sibling | h2 ~ p { color: #444; } | |
| Every element | * { margin: 0; } | The universal selector. Cheap, despite its reputation |
| Has an attribute | input[required] { border-color: crimson; } | |
| Attribute equals a value | input[type="email"] { width: 100%; } | |
| Attribute starts with | a[href^="https://"] { color: green; } | |
| Attribute ends with | a[href$=".pdf"] { font-weight: bold; } | Add i before the ] to ignore case: [href$=".pdf" i] |
| Attribute contains | a[href*="example.com"] { color: purple; } | |
| Parent that contains something | .card:has(img) { padding-top: 0; } | Styles the card, not the image. The parent selector CSS went without for decades |
| Previous sibling | h2:has(+ p) { margin-bottom: 0.25rem; } | An h2 that is directly followed by a p |
| Any of several, in one place | :is(article, aside) h2 { font-size: 1.25rem; } | Shorter than listing every pair. Takes the specificity of its most specific argument |
| Same, with zero specificity | :where(ul, ol) { padding-left: 1.25rem; } | Anything overrides it, so it suits resets and library defaults |
| Everything except | li:not(:last-child) { border-bottom: 1px solid #ddd; } | Takes a list too: :not(.a, .b) |
| Nest rules inside a rule | .card { padding: 1rem; & h2 { margin: 0; } &:hover { background: #f5f5f5; } } | Native nesting, no preprocessor. & stands for the parent selector |
| Nest a media query | .card { padding: 1rem; @media (width >= 48rem) { padding: 2rem; } } | Same as writing the media query outside with .card inside it |
Pseudo-classes and pseudo-elements
A pseudo-class (one colon) matches an element in a state: hovered, checked, first in its parent. A pseudo-element (two colons) styles a part of an element, or adds one.
| Task | Code | Notes |
|---|---|---|
| Mouse over | a:hover { color: crimson; } | Touch screens have no hover. Put hover-only effects inside @media (hover: hover) |
| Keyboard focus | button:focus-visible { outline: 3px solid navy; outline-offset: 2px; } | Shows for keyboard users, not on mouse clicks. Never remove an outline without a replacement |
| Any focus | input:focus { border-color: navy; } | Includes clicks and taps |
| Something inside has focus | .search:focus-within { box-shadow: 0 0 0 3px gold; } | |
| Being pressed | button:active { translate: 0 1px; } | |
| Visited link | a:visited { color: purple; } | Only colour properties apply, so sites cannot sniff your history |
| First and last child | li:first-child { font-weight: bold; } li:last-child { border: 0; } | |
| Zebra stripes | tr:nth-child(even) { background: #f5f5f5; } | Also odd, 3n (every third), 3n+1 and -n+3 (the first three) |
| The nth of its element type | p:nth-of-type(2) { font-style: italic; } | Counts only p siblings. nth-child counts every sibling |
| The nth that matches a selector | li:nth-child(odd of .visible) { background: #f5f5f5; } | Keeps stripes correct when some rows are hidden |
| Only child | li:only-child { list-style: none; } | |
| Empty element | div:empty { display: none; } | No child elements and no text |
| The root element | :root { --brand: #f5c518; } | The html element, with class-level specificity. The usual home for custom properties |
| Target of the URL fragment | :target { outline: 2px dashed gold; } | The element whose id matches the #part of the URL |
| Text in a language | :lang(fr) q { quotes: "«" "»"; } | |
| Ticked checkbox or radio | input:checked + label { font-weight: bold; } | |
| Switched-off field | input:disabled { opacity: 0.5; } | |
| Invalid field, after the user touched it | input:user-invalid { border-color: crimson; } | :invalid matches before anyone types. :user-invalid waits until they have interacted |
| Field showing its placeholder | input:placeholder-shown { font-style: italic; } | |
| Required and optional fields | input:required { border-left: 3px solid navy; } | :optional for the rest |
| Add content before or after | .required::after { content: " *"; color: crimson; } | Nothing renders without content. Does not work on img or input |
| Decorative shape | .badge::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: green; } | |
| Placeholder text | input::placeholder { color: #767676; } | Keep it at 4.5:1 contrast or darker |
| Highlighted text | ::selection { background: gold; color: black; } | Not Baseline yet: Safari on iPhone and iPad ignores it, and shows the system highlight |
| List bullets and numbers | li::marker { color: crimson; } | Colour, font and content only, never margin or padding. Not Baseline yet: Safari applies only some properties, colour and font size among them |
| First line and first letter | p::first-line { font-variant: small-caps; } p::first-letter { font-size: 2em; } | |
| The file upload button | input::file-selector-button { border-radius: 4px; } | |
| Behind a modal dialog | dialog::backdrop { background: rgb(0 0 0 / 0.5); } | Also styles the backdrop of a fullscreen element |
Specificity
When two rules set the same property on the same element, the one with higher specificity wins, whatever order they are in. Count ids, then classes, attributes and pseudo-classes, then elements and pseudo-elements, and compare left to right: 1,0,0 beats 0,12,0.
| Selector | Example | Specificity (id, class, element) |
|---|---|---|
| Universal | * | 0,0,0. So are the combinators > + ~ and the space |
| Element | p | 0,0,1 |
| Two elements | ul li | 0,0,2 |
| Class | .card | 0,1,0 |
| Attribute | [type="email"] | 0,1,0, the same as a class |
| Element and pseudo-class | a:hover | 0,1,1 |
| Element and pseudo-element | p::first-line | 0,0,2. A pseudo-element counts as an element |
| Two classes | .btn.primary | 0,2,0 |
| Id | #nav | 1,0,0. Beats any number of classes |
| Id, class and pseudo-class | #nav .link:hover | 1,2,0 |
| :is() takes its strongest argument | :is(#main, .sidebar) p | 1,0,1, even when it matches through .sidebar |
| :where() counts for nothing | :where(#main, .sidebar) p | 0,0,1 |
| :not() and :has() count their argument | li:not(.done) | 0,1,1. The pseudo-class itself adds nothing |
| Nested rule | .card { & h2 { color: navy; } } | 0,1,1, like .card h2. The & counts as :is() around the parent |
| Inline style attribute | <p style="color: red"> | Beats every selector |
| Marked important | p { color: red !important; } | Beats every normal declaration, inline styles included. Then specificity decides between important ones |
The cascade and inheritance
Specificity is one step of the cascade. Before it come origin, importance and layer. After it, the rule that comes last in the CSS wins.
| Task | Code | Notes |
|---|---|---|
| Later rule wins a tie | p { color: red; } p { color: blue; } | Blue. Same specificity, so source order decides |
| Force a value | .hidden { display: none !important; } | For utility classes that must always win. Everywhere else it starts an arms race |
| Declare layer order | @layer reset, base, components, utilities; | Later layers win whatever the specificity. Styles in no layer beat every layer |
| Put rules in a layer | @layer components { .btn { padding: 0.5rem 1rem; } } | |
| Take the parent's value | button { font: inherit; color: inherit; } | Form controls do not inherit fonts by default. This fixes it |
| Browser's default for the element | h1 { font-size: revert; } | Rolls back to the user-agent stylesheet |
| Inherit if inheritable, else initial | a { color: unset; } | unset. Text properties inherit, box properties do not |
| The property's initial value | li { display: initial; } | inline for display, not the block you might expect. revert is usually what you want |
| Reset every property at once | .reset-button { all: unset; } | Also strips the focus outline, so add a :focus-visible style back |
| Styles scoped to a component | @scope (.card) to (.card-body) { img { border-radius: 8px; } } | Only img inside .card and not inside .card-body. Newly available, Baseline since March 2026 |
Inherited by default: color, font properties, line-height, letter-spacing, text-align, visibility, cursor and custom properties. Not inherited: margin, padding, border, background, width, height, display and position.
Box model
Every element is a box: content, then padding, then border, then margin on the outside. Background fills the padding and border area; margin is always transparent.
| Task | Code | Notes |
|---|---|---|
| Make width include padding and border | *, *::before, *::after { box-sizing: border-box; } | The first line of almost every stylesheet. Without it, width: 100% plus padding overflows |
| Full width up to a maximum | .page { width: 100%; max-width: 70rem; } | |
| Centre a block horizontally | .page { max-width: 70rem; margin-inline: auto; } | Needs a width or max-width. auto splits the leftover space |
| Readable line length | article { max-width: 65ch; } | Roughly 65 characters per line |
| Padding on all four sides | padding: 1rem; | |
| Vertical, then horizontal | padding: 1rem 2rem; | Two values: top and bottom, then left and right |
| All four, one by one | margin: 10px 20px 30px 40px; | Clockwise from the top: top, right, bottom, left |
| Top and bottom in writing order | margin-block: 2rem; | Logical properties. block is top and bottom in English, inline is left and right |
| Left and right in writing order | padding-inline: 1rem; | Flips for right-to-left languages with no extra CSS |
| Border | border: 2px solid #222; | Width, style, colour. The style is required: without it there is no border |
| One side only | border-bottom: 1px solid #ddd; | |
| Rounded corners | border-radius: 8px; | 50% on a square makes a circle. 999px makes a pill of any size |
| Outline that takes no space | outline: 2px solid navy; outline-offset: 2px; | Drawn outside the border and never moves the layout |
| Soft shadow | box-shadow: 0 2px 8px rgb(0 0 0 / 0.15); | x, y, blur, colour. Takes no space either |
| Hard offset shadow | box-shadow: 4px 4px 0 #000; | No blur. The look this site uses |
| Inner shadow | box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.2); | |
| Fixed shape | aspect-ratio: 16 / 9; | Set a width and the height follows. Good for video embeds and cards |
| Scroll when content is too big | overflow: auto; | Scrollbars only when needed. scroll always shows them |
| Scroll sideways only | overflow-x: auto; | For wide tables and code blocks on phones |
| Cut off overflow | overflow: hidden; | Also makes the element a scroll container, which breaks position: sticky inside. clip does not |
| Clip without a scroll container | overflow: clip; | |
| Hide, keeping the space | visibility: hidden; | Screen readers skip it too |
| Hide, removing the space | display: none; | Gone from layout and from screen readers |
| Block, inline or both | display: inline-block; | block takes a full line, inline flows in text and ignores width, inline-block flows but takes width |
| Contain floated children | .media { display: flow-root; } | Replaces the old clearfix hack |
| Images that shrink to fit | img { max-width: 100%; height: auto; } | Keeps the ratio set by the width and height attributes |
| Crop an image to fill its box | img { width: 100%; height: 200px; object-fit: cover; } | contain fits the whole image instead. object-position moves the crop |
| Textarea that grows as you type | textarea { field-sizing: content; min-height: 3lh; } | Newly available, Baseline since June 2026. Older browsers keep a fixed size |
Units
Use rem for sizes and spacing so the page scales when someone raises their browser's font size, px for hairlines and shadows, and percentages or fr for layout.
| Unit | Example | Relative to |
|---|---|---|
| px | border-width: 1px; | A CSS pixel, which is not a device pixel. Does not scale with the user's font size |
| rem | font-size: 1.25rem; | The root font size, 16px by default. The safe default for type and spacing |
| em | padding: 0.5em 1em; | The element's own font size. For font-size itself, the parent's. Compounds when nested |
| % | width: 50%; | Usually the parent's size. Vertical padding and margin use the parent's width, not its height |
| vw and vh | width: 100vw; | 1% of the viewport width or height. 100vw includes the scrollbar, so it can overflow sideways |
| dvh, svh and lvh | min-height: 100dvh; | Viewport height with the mobile address bar shown (svh), hidden (lvh) or as it is right now (dvh) |
| vmin and vmax | width: 80vmin; | The smaller or larger of vw and vh |
| ch | max-width: 65ch; | The width of the 0 character in the current font |
| lh | min-height: 3lh; | The element's line height. 3lh is three lines of text |
| fr | grid-template-columns: 1fr 2fr; | A share of the free space in a grid. Grid only |
| cqi | font-size: 5cqi; | 1% of the nearest container's inline size. Needs a container-type on an ancestor |
| No unit on line-height | line-height: 1.5; | 1.5 times the font size, recalculated for each child. 1.5em would be fixed at the parent's size |
| deg and turn | rotate: 0.25turn; | 0.25turn is 90deg |
| s and ms | transition-duration: 200ms; | 0.2s is the same thing. A bare 0 is not valid here |
| Mix units | width: calc(100% - 2rem); | Spaces round + and - are required |
| Smaller of two | width: min(100%, 40rem); | Full width on phones, capped at 40rem |
| Larger of two | padding: max(1rem, 5vw); | |
| Fluid between a minimum and maximum | font-size: clamp(1.25rem, 1rem + 1.5vw, 2rem); | Minimum, preferred, maximum. Keep a rem in the middle value so zoom still works |
Flexbox
Flexbox lays out items in one direction, a row or a column, and shares out the space between them. The main axis runs along that direction; the cross axis runs across it.
| Task | Code | Notes |
|---|---|---|
| Turn on flexbox | .row { display: flex; } | The children become flex items, side by side |
| Stack vertically | flex-direction: column; | row-reverse and column-reverse change the look but not the reading or tab order |
| Space between items | gap: 1rem; | Only between items, never on the outer edges |
| Wrap onto new lines | flex-wrap: wrap; | Without it, items squeeze onto one line and can overflow |
| Align along the main axis | justify-content: space-between; | Also flex-start, center, flex-end, space-around, space-evenly |
| Align across the axis | align-items: center; | The default, stretch, makes every item as tall as the tallest. baseline lines up the text |
| Centre one thing in both directions | .center { display: flex; justify-content: center; align-items: center; } | |
| Item that fills the space left | .search { flex: 1; } | Short for flex: 1 1 0%. Two items with flex: 1 get equal widths |
| Item that takes twice the share | .main { flex: 2; } | |
| Fixed-width item that never shrinks | .sidebar { flex: 0 0 250px; } | grow, shrink, basis |
| Item that keeps its content size | .logo { flex: none; } | Short for 0 0 auto |
| Stop an icon being squashed | .icon { flex-shrink: 0; } | |
| Push one item to the far end | .logout { margin-left: auto; } | An auto margin in a flex row soaks up all the free space |
| Align one item differently | .badge { align-self: flex-start; } | |
| Let text truncate inside an item | .title { min-width: 0; } | Flex items refuse to shrink below their content by default. This allows it |
| Space between wrapped lines | align-content: flex-start; | Only does anything with flex-wrap: wrap |
| Change the visual order | .featured { order: -1; } | Keyboard and screen reader order stays the HTML order, so use it sparingly |
Grid
Grid lays out items in rows and columns at once. Define the tracks on the container, then place items on the lines between them, or let them flow in order.
| Task | Code | Notes |
|---|---|---|
| Three equal columns | .grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; } | |
| Sidebar and main area | grid-template-columns: 250px 1fr; | |
| Responsive columns, no media query | grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); | As many 16rem columns as fit. The min() stops overflow on screens narrower than 16rem |
| auto-fill instead of auto-fit | grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); | Keeps empty tracks, so a few items do not stretch across the whole row |
| Columns that cannot blow out | grid-template-columns: repeat(3, minmax(0, 1fr)); | 1fr alone will not shrink below its longest word or widest image |
| Row gap and column gap | gap: 2rem 1rem; | Rows first, then columns |
| Row heights for extra rows | grid-auto-rows: minmax(8rem, auto); | At least 8rem, taller if the content needs it |
| Span two columns | .wide { grid-column: span 2; } | |
| Full width, first line to last | .banner { grid-column: 1 / -1; } | Negative numbers count from the end. Covers explicit columns only |
| Place by line numbers | .hero { grid-column: 2 / 4; grid-row: 1 / 3; } | Start line / end line. Lines are numbered from 1 |
| Name the areas | .layout { display: grid; grid-template-areas: "header header" "nav main" "footer footer"; } | One string per row. Each area must be a rectangle, and . leaves a cell empty |
| Put an item in an area | header { grid-area: header; } | |
| Centre one child | .center { display: grid; place-items: center; min-height: 100dvh; } | The shortest centring rule in CSS |
| Align every item in its cell | justify-items: start; align-items: end; | justify is the inline direction, align the block direction |
| Align one item in its cell | .badge { justify-self: end; align-self: start; } | |
| Fill gaps left by big items | grid-auto-flow: dense; | Moves later items into earlier holes, so the visual order no longer matches the HTML |
| Line up nested content | .card { display: grid; grid-row: span 3; grid-template-rows: subgrid; } | The card uses its parent's row tracks, so titles and buttons line up across cards |
Positioning
| Task | Code | Notes |
|---|---|---|
| Normal flow | position: static; | The default. top, left and z-index do nothing |
| Nudge from where it would be | position: relative; top: 4px; | The original space stays reserved |
| Anchor for absolute children | .card { position: relative; } | The usual reason for relative with no offsets |
| Pin inside the nearest positioned parent | .close { position: absolute; top: 0.5rem; right: 0.5rem; } | Taken out of the flow. Positioned against the closest ancestor that is not static |
| Fill the parent | .overlay { position: absolute; inset: 0; } | inset is top, right, bottom and left in one |
| Pin to the viewport | .chat { position: fixed; right: 1rem; bottom: 1rem; } | An ancestor with a transform or filter traps it inside that ancestor instead |
| Stick while scrolling | .toolbar { position: sticky; top: 0; } | Needs top, bottom, left or right. An ancestor with overflow hidden or auto stops it sticking |
| Centre an absolute element | .modal { position: absolute; top: 50%; left: 50%; translate: -50% -50%; } | The percentages in translate are of the element's own size |
| Stack order | .dropdown { position: relative; z-index: 10; } | Works on positioned elements and flex or grid items. Higher is in front |
| New stacking context | .card { isolation: isolate; } | Keeps children's z-index values from competing with the rest of the page |
| Keep anchored targets below a sticky header | h2 { scroll-margin-top: 5rem; } | Stops #links landing under the header |
| Wrap text round an image | img.left { float: left; margin: 0 1rem 1rem 0; } | What float is for. Use flexbox or grid for layout |
Typography
| Task | Code | Notes |
|---|---|---|
| Font stack | font-family: "Inter", system-ui, sans-serif; | Tried in order. End with a generic family. Quote names with spaces |
| The device's own font | font-family: system-ui, sans-serif; | Loads nothing, and looks native everywhere |
| Code font | font-family: "Cascadia Code", Menlo, Consolas, monospace; | Whichever the device has, then its default monospace font |
| Size | font-size: 1.125rem; | |
| Fluid heading size | h1 { font-size: clamp(2rem, 1.5rem + 2.5vw, 3.5rem); } | Grows with the screen between the two limits |
| Weight | font-weight: 700; | 400 is normal, 700 bold. Any value from 1 to 1000 with a variable font |
| Italic | font-style: italic; | |
| Line spacing | line-height: 1.5; | Unitless. 1.5 or more for body text |
| Letter spacing | letter-spacing: 0.05em; | In em, so it scales with the font. Use it on capitals |
| Alignment | text-align: center; | start and end follow the writing direction. justify spaces out whole lines |
| Capitals | text-transform: uppercase; | Changes the look only. The HTML keeps the case you typed |
| Remove link underlines | a { text-decoration: none; } | Links inside paragraphs need a cue besides colour, so keep underlines there |
| Nicer underlines | a { text-decoration-thickness: 2px; text-underline-offset: 0.2em; } | |
| One line with an ellipsis | .title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | All three are needed |
| Cut off after three lines | .excerpt { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; } | Prefixed, but works in every major browser. The unprefixed line-clamp is not Baseline yet |
| Balanced headings | h1, h2 { text-wrap: balance; } | Evens out the line lengths. Newly available, Baseline since May 2024. Ignored where unsupported |
| No lonely last word | p { text-wrap: pretty; } | Not Baseline yet: not in Firefox. Browsers without it ignore it, so it is safe to add |
| Break long words and URLs | overflow-wrap: anywhere; | Stops a long URL making a phone page scroll sideways |
| Automatic hyphens | p { hyphens: auto; } | Needs a lang attribute on the page to know the language |
| Keep spaces and line breaks | white-space: pre-wrap; | Like pre, but long lines still wrap |
| Numbers that line up in columns | td { font-variant-numeric: tabular-nums; } | Every digit the same width |
| Remove list bullets | ul { list-style: none; padding-left: 0; } | The padding is the indent the bullets sat in |
| Load a web font | @font-face { font-family: "Inter"; src: url("/fonts/inter.woff2") format("woff2"); font-weight: 100 900; font-display: swap; } | A weight range for a variable font. swap shows fallback text while it loads |
| Use every weight of a variable font | h1 { font-family: "Inter", sans-serif; font-weight: 850; } |
Colours and backgrounds
| Task | Code | Notes |
|---|---|---|
| Text colour | color: #1a1a1a; | Inherited by every child |
| Background colour | background-color: #fffbea; | |
| Named colour | color: rebeccapurple; | 148 names, from aliceblue to yellowgreen |
| Hex | color: #ff6600; | #f60 is the three-digit short form. Add two digits for transparency: #ff660080 |
| RGB with transparency | background: rgb(255 102 0 / 0.5); | Spaces, not commas, then a slash and the opacity from 0 to 1. rgba() is an alias |
| HSL | color: hsl(24deg 100% 50%); | Hue, saturation, lightness. Easy to make lighter and darker versions by hand |
| OKLCH | color: oklch(70% 0.19 45); | Lightness, chroma, hue. Equal lightness looks equally light across hues, which HSL does not |
| Match the text colour | border: 2px solid currentcolor; | The element's color value. Icons in SVG use it with fill: currentcolor |
| See-through | background: transparent; | |
| Mix two colours | background: color-mix(in oklch, var(--brand), white 20%); | 80% brand, 20% white. Good for hover states from one token |
| Adjust a colour | color: oklch(from var(--brand) calc(l - 0.1) c h); | Relative colour: the brand colour with its lightness lowered by 0.1 on a 0 to 1 scale. Newly available, Baseline since September 2024 |
| Light and dark value in one | :root { color-scheme: light dark; color: light-dark(#1a1a1a, #f0f0f0); } | Follows the system theme. Needs color-scheme. Newly available, Baseline since May 2024 |
| Black or white text for any background | color: contrast-color(var(--bg)); | Returns black or white, whichever contrasts more. Newly available, Baseline since April 2026 |
| Let form controls and scrollbars follow dark mode | :root { color-scheme: light dark; } | |
| Background image | background: url("cat.jpg") center / cover no-repeat; | Position / size. cover fills the box and crops; contain shows it all |
| Linear gradient | background: linear-gradient(to right, #f5c518, #ff6600); | Also an angle, such as 135deg |
| Radial gradient | background: radial-gradient(circle at top left, #fff, #ddd); | |
| Hard stripes | background: linear-gradient(90deg, navy 50%, gold 50%); | Two stops at the same point make a sharp edge |
| Image over a colour | background: url("pattern.svg") repeat, #fffbea; | Several layers, comma-separated. The first is on top, and only the last can hold a colour |
| Fade the whole element | opacity: 0.6; | Fades the children too. Use an alpha colour to fade only the background |
| Image effects | filter: grayscale(100%) blur(2px); | Also brightness, contrast, drop-shadow, saturate |
| Frosted glass | backdrop-filter: blur(8px); | Blurs what is behind a see-through element. Newly available, Baseline since September 2024 |
Transforms, transitions and animations
Transforms move, scale and rotate without affecting the layout. Transitions animate a change between two states; keyframe animations run on their own. Animate transform and opacity where you can: the browser does not have to redo the layout for either.
| Task | Code | Notes |
|---|---|---|
| Move | translate: 0 -4px; | x then y. The individual properties can be animated separately |
| Scale | scale: 1.05; | |
| Rotate | rotate: 45deg; | |
| Several at once, in order | transform: translateX(10px) rotate(10deg) scale(1.1); | Applied right to left, so order changes the result |
| Pivot point | transform-origin: top left; | The default is the centre |
| Animate a change | .btn { transition: background-color 200ms ease; } | Put it on the element, not the :hover rule, so it animates both ways |
| Two properties, different timing | transition: opacity 200ms, translate 300ms ease-out; | |
| Wait before starting | transition: opacity 200ms ease-in 100ms; | The second time is the delay |
| Easing curves | transition-timing-function: cubic-bezier(0.2, 0, 0, 1); | Also ease, linear, ease-in, ease-out, ease-in-out and steps(4) |
| Define an animation | @keyframes pulse { from { opacity: 1; } to { opacity: 0.4; } } | Percentages work too: 0%, 50%, 100% |
| Run it | .dot { animation: pulse 1s ease-in-out infinite alternate; } | Name, duration, easing, repeat count, direction |
| Spinner | @keyframes spin { to { rotate: 1turn; } } .spinner { animation: spin 1s linear infinite; } | |
| Keep the last frame | animation-fill-mode: forwards; | Otherwise it snaps back to the start when it ends |
| Pause on hover | .ticker:hover { animation-play-state: paused; } | |
| Respect reduced motion | @media (prefers-reduced-motion: reduce) { .dot { animation: none; } } | For people who get motion sick. Fades are usually fine; movement and zoom are not |
| Animate in when it first appears | .toast { opacity: 1; transition: opacity 300ms; @starting-style { opacity: 0; } } | @starting-style gives a start value for the first render. Newly available, Baseline since August 2024 |
| Hint that something will animate | .drawer { will-change: transform; } | Only on elements about to animate. On everything, it wastes memory |
Media queries
Mobile first: write the phone styles with no query, then add min-width queries as the screen grows.
| Task | Code | Notes |
|---|---|---|
| From tablet width up | @media (width >= 48rem) { .grid { grid-template-columns: 1fr 1fr; } } | Range syntax. 48rem is 768px at the default font size |
| Same, the older way | @media (min-width: 48rem) { .grid { grid-template-columns: 1fr 1fr; } } | |
| Between two widths | @media (40rem <= width < 64rem) { .sidebar { display: none; } } | |
| Landscape | @media (orientation: landscape) { .hero { min-height: 60vh; } } | |
| Dark mode | @media (prefers-color-scheme: dark) { :root { --bg: #111; --text: #eee; } } | |
| Less motion | @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } } | Opt in to motion rather than opting out |
| More contrast | @media (prefers-contrast: more) { .btn { border: 2px solid; } } | |
| High contrast mode on Windows | @media (forced-colors: active) { .btn { border: 2px solid ButtonText; } } | The system replaces your colours and removes shadows. System colours like ButtonText and Canvas follow its theme |
| Real hover and a precise pointer | @media (hover: hover) and (pointer: fine) { .card:hover { translate: 0 -2px; } } | A mouse or trackpad. Keeps hover effects off touch screens |
| Printing | @media print { nav, footer { display: none; } } | |
| Either condition | @media (width < 40rem), print { .sidebar { display: none; } } | A comma means or |
| Stylesheet only for some media | <link rel="stylesheet" href="print.css" media="print"> | Still downloaded, but does not block rendering the screen |
| Only if the browser supports it | @supports (backdrop-filter: blur(1px)) { .nav { background: rgb(255 255 255 / 0.7); } } | |
| Only if it does not | @supports not (aspect-ratio: 1) { .square { height: 200px; } } | |
| Test a selector | @supports selector(:has(a)) { .card:has(img) { padding-top: 0; } } |
Common breakpoints are 40rem, 48rem, 64rem and 80rem (640, 768, 1024 and 1280px), but the best ones are wherever your content starts to look wrong. A rem in a media query is always the browser's default font size, usually 16px. Setting font-size on html does not change your breakpoints.
Container queries
A media query asks how wide the screen is. A container query asks how wide the component's parent is, so the same card can be a stack in a sidebar and a row in the main column.
| Task | Code | Notes |
|---|---|---|
| Make a container | .card-wrapper { container-type: inline-size; } | On the parent. An element cannot query its own size |
| Named container | .sidebar { container: sidebar / inline-size; } | Name, then type |
| Query the nearest container | @container (width >= 30rem) { .card { display: grid; grid-template-columns: 8rem 1fr; } } | |
| Query a named container | @container sidebar (width < 20rem) { .card { font-size: 0.875rem; } } | |
| Size relative to the container | .card h2 { font-size: clamp(1rem, 4cqi + 0.5rem, 1.75rem); } | cqi is 1% of the container's inline size |
| Query a custom property | @container style(--variant: compact) { .card { padding: 0.5rem; } } | Style queries, custom properties only. Newly available, Baseline since May 2026 |
Custom properties
Custom properties, often called CSS variables, are live values the browser resolves per element. They inherit, they can change inside a media query or a class, and JavaScript can read and set them.
| Task | Code | Notes |
|---|---|---|
| Define | :root { --brand: #f5c518; --space: 1rem; --radius: 8px; } | Two dashes, then any name. Case-sensitive: --Brand is a different property |
| Use | color: var(--brand); | |
| Fallback if it is not set | border-radius: var(--radius, 4px); | Everything after the first comma is the fallback |
| Override inside a component | .card.compact { --space: 0.5rem; } | Every child of that card picks up the new value |
| Override for dark mode | @media (prefers-color-scheme: dark) { :root { --bg: #111; --text: #eee; } } | |
| Do maths with one | margin-bottom: calc(var(--space) * 2); | |
| Add a unit to a number | --columns: 3; grid-template-columns: repeat(var(--columns), 1fr); | Store numbers bare. calc(var(--n) * 1px) turns one into a length; var(--n)px does not work |
| Set one from HTML | <div class="bar" style="--progress: 40%"></div> | Then width: var(--progress) in the stylesheet |
| Set one from JavaScript | el.style.setProperty('--progress', '60%'); | Read it with getComputedStyle(el).getPropertyValue('--progress') |
| Typed property that can animate | @property --angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; } | With a type, a transition or animation can interpolate it. Newly available, Baseline since July 2024 |
A modern starter stylesheet
The few rules almost every site begins with: sizing that includes padding, media that shrinks to fit, form controls that inherit the page font, a light and dark theme from a handful of custom properties, visible keyboard focus, and motion only for people who have not asked for less.
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
color-scheme: light dark;
--bg: #fffdf7;
--text: #1a1a1a;
--brand: #b35900;
--space: 1rem;
--radius: 8px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #141414;
--text: #f0f0f0;
--brand: #ffb366;
}
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: system-ui, sans-serif;
line-height: 1.6;
}
img,
svg,
video {
display: block;
max-width: 100%;
height: auto;
}
button,
input,
select,
textarea {
font: inherit;
}
h1,
h2,
h3 {
line-height: 1.2;
text-wrap: balance;
}
a {
color: var(--brand);
text-underline-offset: 0.2em;
}
:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}Every colour lives in one place, so dark mode is four lines that change the
tokens and nothing else. color-scheme tells the browser to draw scrollbars and
form controls in the matching theme. text-wrap: balance is newly available, and
a browser without it simply wraps the heading as normal, which is why it is safe
to include. Both brand colours pass 4.5:1 contrast against their background.
A responsive card grid, start to finish
A grid that goes from one column on a phone to as many as fit on a desktop with no media queries, and cards that switch their own layout with a container query once they have room.
<ul class="cards">
<li class="card-wrapper">
<article class="card">
<img src="cat.jpg" alt="Ginger cat on a windowsill" width="800" height="600">
<div class="card-body">
<h2><a href="/blog/windowsill-naps">Windowsill naps</a></h2>
<p>Why cats pick the sunniest spot in the house.</p>
</div>
</article>
</li>
</ul>.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1.5rem;
padding: 0;
list-style: none;
}
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
gap: 1rem;
border: 2px solid #000;
border-radius: 8px;
box-shadow: 4px 4px 0 #000;
overflow: clip;
}
.card img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.card-body {
padding: 0 1rem 1rem;
}
@container (width >= 30rem) {
.card {
grid-template-columns: 12rem 1fr;
align-items: center;
}
.card img {
height: 100%;
}
.card-body {
padding: 1rem;
}
}
.card:has(a:focus-visible) {
outline: 3px solid navy;
outline-offset: 4px;
}repeat(auto-fit, minmax(min(100%, 18rem), 1fr)) reads as: fit as many columns
as possible, each at least 18rem and sharing out the rest. The min() means a
screen narrower than 18rem gets one full-width column instead of a sideways
scroll. The container query sits on the wrapper, not the card, because an element
cannot query its own size. Give the page a wide single column and each card turns
into an image-beside-text row by itself. The last rule lights up the whole card
when a link inside it has keyboard focus.
A page layout with grid areas
Header, navigation, main content and footer. Stacked on a phone, sidebar on a wider screen, and the footer pushed to the bottom of short pages.
.layout {
display: grid;
grid-template-areas:
"header"
"nav"
"main"
"footer";
grid-template-rows: auto auto 1fr auto;
min-height: 100dvh;
}
@media (width >= 48rem) {
.layout {
grid-template-areas:
"header header"
"nav main"
"footer footer";
grid-template-columns: 15rem minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
}
}
.layout > header {
grid-area: header;
}
.layout > nav {
grid-area: nav;
}
.layout > main {
grid-area: main;
}
.layout > footer {
grid-area: footer;
}The 1fr row takes whatever height is left over, which is what holds the footer
at the bottom when there is not much content. The areas follow the HTML order,
header, nav, main, footer, at both sizes. Grid can put areas in any visual order,
but keyboard and screen reader users still move through the HTML order, so keep
the two the same. The minmax(0, 1fr) column stops a wide code block or table in
main from pushing the page wider than the screen.
A button with a hover effect and a spinner
A button that lifts on hover, presses on click, shows a loading spinner, and keeps still for people who have asked for reduced motion.
.btn {
display: inline-flex;
gap: 0.5rem;
align-items: center;
padding: 0.75rem 1.25rem;
border: 2px solid #000;
border-radius: 8px;
background: #f5c518;
color: #000;
font: inherit;
font-weight: 700;
box-shadow: 4px 4px 0 #000;
cursor: pointer;
transition:
translate 150ms ease-out,
box-shadow 150ms ease-out;
}
@media (hover: hover) {
.btn:hover {
translate: -2px -2px;
box-shadow: 6px 6px 0 #000;
}
}
.btn:active {
translate: 2px 2px;
box-shadow: 2px 2px 0 #000;
}
.btn:focus-visible {
outline: 3px solid navy;
outline-offset: 3px;
}
.btn .spinner {
width: 1em;
height: 1em;
border: 2px solid currentcolor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 800ms linear infinite;
}
@keyframes spin {
to {
rotate: 1turn;
}
}
@media (prefers-reduced-motion: reduce) {
.btn {
transition: none;
}
.btn .spinner {
animation-duration: 2.4s;
}
}Only translate and box-shadow change, and the transition sits on .btn
itself so it plays both on the way in and on the way out. The hover rule is inside
(hover: hover) so a tap on a phone does not leave the button stuck in its lifted
state. With reduced motion the spinner slows down rather than stopping, because a
frozen spinner looks like a broken page. To toggle the loading state from a
script, add and remove a class with classList, covered in the
JavaScript cheat sheet.
Gotchas
The mistakes almost everyone makes in their first month of CSS.
| Looks right | What actually happens | Do this instead |
|---|---|---|
width: 100% plus padding | The box is wider than its parent | box-sizing: border-box on everything |
height: 100% on a child | Ignored when the parent has no set height | min-height: 100dvh, or a grid or flex parent |
height: 100vh on phones | Taller than the screen while the address bar shows | 100dvh, or 100svh for the smallest size |
margin-top on a first child | Leaks out of the parent through margin collapse | gap in a flex or grid parent, or padding on the parent |
z-index: 9999 that still hides | The element is static, or trapped in a parent's stacking context | position: relative, and raise the parent instead |
position: sticky that never sticks | No top, or an ancestor has overflow: hidden | Add top: 0, and use overflow: clip on the ancestor |
| Long text in a flex item | Overflows instead of truncating | min-width: 0 on the flex item |
1fr grid columns | A long word or wide image stretches the column | minmax(0, 1fr) |
var(--size)px | Invalid, so the declaration is thrown away | calc(var(--size) * 1px), or store --size: 16px |
outline: none on focus | Keyboard users cannot see where they are | Style :focus-visible instead of removing it |
transition: all | Animates properties you did not mean to, and costs performance | Name the properties |
Transitioning height: auto | Jumps instead of animating | Animate grid-template-rows from 0fr to 1fr, or max-height |
display: initial | Becomes inline, not the element's usual block | display: revert |
li { font-size: 0.8em; } with nested lists | Shrinks at every level, because em compounds | rem |
color: #999 on white | Fails the 4.5:1 contrast minimum | #767676 or darker |
Styling with #id selectors | Nothing short of another id or !important overrides them | Classes |
| A comma after the last selector in a list | The whole rule is dropped | No trailing comma |
@import below other rules | Ignored | Put it first, or use a link tag |
Common questions
Which version of CSS does this cheat sheet cover?
CSS as it stands in September 2026. There is no CSS4: since CSS3 the language has been split into modules that the W3C updates separately, so the useful question is what browsers support. Everything here is Baseline Widely available, meaning it has worked in Chrome, Edge, Firefox and Safari on desktop and mobile for at least two and a half years, unless its note says Newly available with the month it reached every major browser, or Not Baseline yet. Every snippet was checked with a CSS validator and linter.
What is specificity in CSS?
Specificity decides which rule wins when two rules set the same property on the same element. It is counted in three columns: ids, then classes, attributes and pseudo-classes, then elements and pseudo-elements. Compare from the left, and the first column that differs decides, so one id (1,0,0) beats any number of classes. :where() counts as zero, and :is(), :not() and :has() count as their most specific argument. When specificity is equal, the rule that comes later wins.
Should I use flexbox or grid?
Flexbox for one direction, grid for two. Flexbox suits a row of buttons, a navbar or a toolbar, where items size from their content and wrap as needed. Grid suits a page layout or a gallery, where the columns are defined first and items line up both across and down. They nest well: a grid for the page, and flexbox inside each card.
What is the difference between em and rem?
rem is relative to the root font size, which is 16px unless the reader or your stylesheet changes it, so 1.5rem is the same everywhere on the page. em is relative to the current element's font size, so it compounds: a 1.2em list inside a 1.2em list is 1.44 times the size. Use rem for font sizes and most spacing, and em for things that should scale with their own text, such as the padding on a button.
What is the difference between display: none and visibility: hidden?
display: none removes the element from the layout, so the space closes up. visibility: hidden hides it but keeps its space. Both hide it from screen readers. To hide something visually but keep it for screen readers, use a visually-hidden class that clips it to one pixel instead, as in the HTML cheat sheet.
Why is my z-index not working?
Usually for one of two reasons. The element is position: static, and z-index only applies to positioned elements and to flex and grid items. Or it is inside a stacking context, created by a parent with a transform, opacity below 1, a filter, isolation: isolate or its own z-index, and nothing inside that context can rise above something outside it, however high the number. Give the parent the z-index instead.
Why is position: sticky not working?
Check three things. It needs a threshold such as top: 0. It sticks only within its parent, so a parent no taller than the element gives it nowhere to go. And an ancestor with overflow set to hidden, auto or scroll becomes the scroll container it sticks to, which is usually not the page. overflow: clip hides overflow without that side effect.
Are CSS variables the same as Sass variables?
No. Sass variables are replaced with their values when the stylesheet is compiled and do not exist in the browser. CSS custom properties exist at run time: they inherit down the page, can be changed by a class or a media query, and can be read and set from JavaScript. Native CSS now also has nesting, so many projects no longer need a preprocessor at all.
Media queries or container queries?
Media queries for the page layout, which really does depend on the screen: how many columns, whether the navigation collapses. Container queries for components, which should depend on the space they are given. A card in a narrow sidebar and the same card in a wide main column then lay themselves out correctly with no extra classes.
How do I centre a div?
On the parent, display: grid and place-items: center centres a child in both directions. With flexbox, display: flex with justify-content: center and align-items: center does the same. To centre a block horizontally in normal flow, give it a max-width and margin-inline: auto. The parent needs a height for vertical centring to be visible, such as min-height: 100dvh for a full screen.
