Web development cheat sheetCSS3 logo

CSS cheat sheet

CSS on one page: selectors and specificity, box model, units, flexbox, grid, colours, animations and container queries, with browser support as of 2026.

Last updated

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.

Adding CSS to a page

TaskCodeNotes
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 ruleh1 { 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.

TaskCodeNotes
Every element of a typep { 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 ruleh1, h2, h3 { font-family: Georgia, serif; }A selector list. One invalid selector in it drops the whole rule
Descendant, at any depthnav a { text-decoration: none; }A space. Every a somewhere inside a nav
Direct child onlyul > li { list-style: square; }Skips the li elements of a nested list inside
The sibling straight afterh2 + p { margin-top: 0; }Only the p that immediately follows an h2
Every later siblingh2 ~ p { color: #444; }
Every element* { margin: 0; }The universal selector. Cheap, despite its reputation
Has an attributeinput[required] { border-color: crimson; }
Attribute equals a valueinput[type="email"] { width: 100%; }
Attribute starts witha[href^="https://"] { color: green; }
Attribute ends witha[href$=".pdf"] { font-weight: bold; }Add i before the ] to ignore case: [href$=".pdf" i]
Attribute containsa[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 siblingh2: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 exceptli: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.

TaskCodeNotes
Mouse overa:hover { color: crimson; }Touch screens have no hover. Put hover-only effects inside @media (hover: hover)
Keyboard focusbutton: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 focusinput:focus { border-color: navy; }Includes clicks and taps
Something inside has focus.search:focus-within { box-shadow: 0 0 0 3px gold; }
Being pressedbutton:active { translate: 0 1px; }
Visited linka:visited { color: purple; }Only colour properties apply, so sites cannot sniff your history
First and last childli:first-child { font-weight: bold; } li:last-child { border: 0; }
Zebra stripestr:nth-child(even) { background: #f5f5f5; }Also odd, 3n (every third), 3n+1 and -n+3 (the first three)
The nth of its element typep:nth-of-type(2) { font-style: italic; }Counts only p siblings. nth-child counts every sibling
The nth that matches a selectorli:nth-child(odd of .visible) { background: #f5f5f5; }Keeps stripes correct when some rows are hidden
Only childli:only-child { list-style: none; }
Empty elementdiv: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 radioinput:checked + label { font-weight: bold; }
Switched-off fieldinput:disabled { opacity: 0.5; }
Invalid field, after the user touched itinput:user-invalid { border-color: crimson; }:invalid matches before anyone types. :user-invalid waits until they have interacted
Field showing its placeholderinput:placeholder-shown { font-style: italic; }
Required and optional fieldsinput: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 textinput::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 numbersli::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 letterp::first-line { font-variant: small-caps; } p::first-letter { font-size: 2em; }
The file upload buttoninput::file-selector-button { border-radius: 4px; }
Behind a modal dialogdialog::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.

SelectorExampleSpecificity (id, class, element)
Universal*0,0,0. So are the combinators > + ~ and the space
Elementp0,0,1
Two elementsul li0,0,2
Class.card0,1,0
Attribute[type="email"]0,1,0, the same as a class
Element and pseudo-classa:hover0,1,1
Element and pseudo-elementp::first-line0,0,2. A pseudo-element counts as an element
Two classes.btn.primary0,2,0
Id#nav1,0,0. Beats any number of classes
Id, class and pseudo-class#nav .link:hover1,2,0
:is() takes its strongest argument:is(#main, .sidebar) p1,0,1, even when it matches through .sidebar
:where() counts for nothing:where(#main, .sidebar) p0,0,1
:not() and :has() count their argumentli: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 importantp { 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.

TaskCodeNotes
Later rule wins a tiep { 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 valuebutton { font: inherit; color: inherit; }Form controls do not inherit fonts by default. This fixes it
Browser's default for the elementh1 { font-size: revert; }Rolls back to the user-agent stylesheet
Inherit if inheritable, else initiala { color: unset; }unset. Text properties inherit, box properties do not
The property's initial valueli { 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.

TaskCodeNotes
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 lengtharticle { max-width: 65ch; }Roughly 65 characters per line
Padding on all four sidespadding: 1rem;
Vertical, then horizontalpadding: 1rem 2rem;Two values: top and bottom, then left and right
All four, one by onemargin: 10px 20px 30px 40px;Clockwise from the top: top, right, bottom, left
Top and bottom in writing ordermargin-block: 2rem;Logical properties. block is top and bottom in English, inline is left and right
Left and right in writing orderpadding-inline: 1rem;Flips for right-to-left languages with no extra CSS
Borderborder: 2px solid #222;Width, style, colour. The style is required: without it there is no border
One side onlyborder-bottom: 1px solid #ddd;
Rounded cornersborder-radius: 8px;50% on a square makes a circle. 999px makes a pill of any size
Outline that takes no spaceoutline: 2px solid navy; outline-offset: 2px;Drawn outside the border and never moves the layout
Soft shadowbox-shadow: 0 2px 8px rgb(0 0 0 / 0.15);x, y, blur, colour. Takes no space either
Hard offset shadowbox-shadow: 4px 4px 0 #000;No blur. The look this site uses
Inner shadowbox-shadow: inset 0 2px 4px rgb(0 0 0 / 0.2);
Fixed shapeaspect-ratio: 16 / 9;Set a width and the height follows. Good for video embeds and cards
Scroll when content is too bigoverflow: auto;Scrollbars only when needed. scroll always shows them
Scroll sideways onlyoverflow-x: auto;For wide tables and code blocks on phones
Cut off overflowoverflow: hidden;Also makes the element a scroll container, which breaks position: sticky inside. clip does not
Clip without a scroll containeroverflow: clip;
Hide, keeping the spacevisibility: hidden;Screen readers skip it too
Hide, removing the spacedisplay: none;Gone from layout and from screen readers
Block, inline or bothdisplay: 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 fitimg { max-width: 100%; height: auto; }Keeps the ratio set by the width and height attributes
Crop an image to fill its boximg { width: 100%; height: 200px; object-fit: cover; }contain fits the whole image instead. object-position moves the crop
Textarea that grows as you typetextarea { 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.

UnitExampleRelative to
pxborder-width: 1px;A CSS pixel, which is not a device pixel. Does not scale with the user's font size
remfont-size: 1.25rem;The root font size, 16px by default. The safe default for type and spacing
empadding: 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 vhwidth: 100vw;1% of the viewport width or height. 100vw includes the scrollbar, so it can overflow sideways
dvh, svh and lvhmin-height: 100dvh;Viewport height with the mobile address bar shown (svh), hidden (lvh) or as it is right now (dvh)
vmin and vmaxwidth: 80vmin;The smaller or larger of vw and vh
chmax-width: 65ch;The width of the 0 character in the current font
lhmin-height: 3lh;The element's line height. 3lh is three lines of text
frgrid-template-columns: 1fr 2fr;A share of the free space in a grid. Grid only
cqifont-size: 5cqi;1% of the nearest container's inline size. Needs a container-type on an ancestor
No unit on line-heightline-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 turnrotate: 0.25turn;0.25turn is 90deg
s and mstransition-duration: 200ms;0.2s is the same thing. A bare 0 is not valid here
Mix unitswidth: calc(100% - 2rem);Spaces round + and - are required
Smaller of twowidth: min(100%, 40rem);Full width on phones, capped at 40rem
Larger of twopadding: max(1rem, 5vw);
Fluid between a minimum and maximumfont-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.

TaskCodeNotes
Turn on flexbox.row { display: flex; }The children become flex items, side by side
Stack verticallyflex-direction: column;row-reverse and column-reverse change the look but not the reading or tab order
Space between itemsgap: 1rem;Only between items, never on the outer edges
Wrap onto new linesflex-wrap: wrap;Without it, items squeeze onto one line and can overflow
Align along the main axisjustify-content: space-between;Also flex-start, center, flex-end, space-around, space-evenly
Align across the axisalign-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 linesalign-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.

TaskCodeNotes
Three equal columns.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
Sidebar and main areagrid-template-columns: 250px 1fr;
Responsive columns, no media querygrid-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-fitgrid-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 outgrid-template-columns: repeat(3, minmax(0, 1fr));1fr alone will not shrink below its longest word or widest image
Row gap and column gapgap: 2rem 1rem;Rows first, then columns
Row heights for extra rowsgrid-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 areaheader { 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 celljustify-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 itemsgrid-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

TaskCodeNotes
Normal flowposition: static;The default. top, left and z-index do nothing
Nudge from where it would beposition: 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 headerh2 { scroll-margin-top: 5rem; }Stops #links landing under the header
Wrap text round an imageimg.left { float: left; margin: 0 1rem 1rem 0; }What float is for. Use flexbox or grid for layout

Typography

TaskCodeNotes
Font stackfont-family: "Inter", system-ui, sans-serif;Tried in order. End with a generic family. Quote names with spaces
The device's own fontfont-family: system-ui, sans-serif;Loads nothing, and looks native everywhere
Code fontfont-family: "Cascadia Code", Menlo, Consolas, monospace;Whichever the device has, then its default monospace font
Sizefont-size: 1.125rem;
Fluid heading sizeh1 { font-size: clamp(2rem, 1.5rem + 2.5vw, 3.5rem); }Grows with the screen between the two limits
Weightfont-weight: 700;400 is normal, 700 bold. Any value from 1 to 1000 with a variable font
Italicfont-style: italic;
Line spacingline-height: 1.5;Unitless. 1.5 or more for body text
Letter spacingletter-spacing: 0.05em;In em, so it scales with the font. Use it on capitals
Alignmenttext-align: center;start and end follow the writing direction. justify spaces out whole lines
Capitalstext-transform: uppercase;Changes the look only. The HTML keeps the case you typed
Remove link underlinesa { text-decoration: none; }Links inside paragraphs need a cue besides colour, so keep underlines there
Nicer underlinesa { 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 headingsh1, h2 { text-wrap: balance; }Evens out the line lengths. Newly available, Baseline since May 2024. Ignored where unsupported
No lonely last wordp { text-wrap: pretty; }Not Baseline yet: not in Firefox. Browsers without it ignore it, so it is safe to add
Break long words and URLsoverflow-wrap: anywhere;Stops a long URL making a phone page scroll sideways
Automatic hyphensp { hyphens: auto; }Needs a lang attribute on the page to know the language
Keep spaces and line breakswhite-space: pre-wrap;Like pre, but long lines still wrap
Numbers that line up in columnstd { font-variant-numeric: tabular-nums; }Every digit the same width
Remove list bulletsul { 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 fonth1 { font-family: "Inter", sans-serif; font-weight: 850; }

Colours and backgrounds

TaskCodeNotes
Text colourcolor: #1a1a1a;Inherited by every child
Background colourbackground-color: #fffbea;
Named colourcolor: rebeccapurple;148 names, from aliceblue to yellowgreen
Hexcolor: #ff6600;#f60 is the three-digit short form. Add two digits for transparency: #ff660080
RGB with transparencybackground: rgb(255 102 0 / 0.5);Spaces, not commas, then a slash and the opacity from 0 to 1. rgba() is an alias
HSLcolor: hsl(24deg 100% 50%);Hue, saturation, lightness. Easy to make lighter and darker versions by hand
OKLCHcolor: oklch(70% 0.19 45);Lightness, chroma, hue. Equal lightness looks equally light across hues, which HSL does not
Match the text colourborder: 2px solid currentcolor;The element's color value. Icons in SVG use it with fill: currentcolor
See-throughbackground: transparent;
Mix two coloursbackground: color-mix(in oklch, var(--brand), white 20%);80% brand, 20% white. Good for hover states from one token
Adjust a colourcolor: 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 backgroundcolor: 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 imagebackground: url("cat.jpg") center / cover no-repeat;Position / size. cover fills the box and crops; contain shows it all
Linear gradientbackground: linear-gradient(to right, #f5c518, #ff6600);Also an angle, such as 135deg
Radial gradientbackground: radial-gradient(circle at top left, #fff, #ddd);
Hard stripesbackground: linear-gradient(90deg, navy 50%, gold 50%);Two stops at the same point make a sharp edge
Image over a colourbackground: 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 elementopacity: 0.6;Fades the children too. Use an alpha colour to fade only the background
Image effectsfilter: grayscale(100%) blur(2px);Also brightness, contrast, drop-shadow, saturate
Frosted glassbackdrop-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.

TaskCodeNotes
Movetranslate: 0 -4px;x then y. The individual properties can be animated separately
Scalescale: 1.05;
Rotaterotate: 45deg;
Several at once, in ordertransform: translateX(10px) rotate(10deg) scale(1.1);Applied right to left, so order changes the result
Pivot pointtransform-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 timingtransition: opacity 200ms, translate 300ms ease-out;
Wait before startingtransition: opacity 200ms ease-in 100ms;The second time is the delay
Easing curvestransition-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 frameanimation-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.

TaskCodeNotes
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.

TaskCodeNotes
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.

TaskCodeNotes
Define:root { --brand: #f5c518; --space: 1rem; --radius: 8px; }Two dashes, then any name. Case-sensitive: --Brand is a different property
Usecolor: var(--brand);
Fallback if it is not setborder-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 onemargin-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 JavaScriptel.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 rightWhat actually happensDo this instead
width: 100% plus paddingThe box is wider than its parentbox-sizing: border-box on everything
height: 100% on a childIgnored when the parent has no set heightmin-height: 100dvh, or a grid or flex parent
height: 100vh on phonesTaller than the screen while the address bar shows100dvh, or 100svh for the smallest size
margin-top on a first childLeaks out of the parent through margin collapsegap in a flex or grid parent, or padding on the parent
z-index: 9999 that still hidesThe element is static, or trapped in a parent's stacking contextposition: relative, and raise the parent instead
position: sticky that never sticksNo top, or an ancestor has overflow: hiddenAdd top: 0, and use overflow: clip on the ancestor
Long text in a flex itemOverflows instead of truncatingmin-width: 0 on the flex item
1fr grid columnsA long word or wide image stretches the columnminmax(0, 1fr)
var(--size)pxInvalid, so the declaration is thrown awaycalc(var(--size) * 1px), or store --size: 16px
outline: none on focusKeyboard users cannot see where they areStyle :focus-visible instead of removing it
transition: allAnimates properties you did not mean to, and costs performanceName the properties
Transitioning height: autoJumps instead of animatingAnimate grid-template-rows from 0fr to 1fr, or max-height
display: initialBecomes inline, not the element's usual blockdisplay: revert
li { font-size: 0.8em; } with nested listsShrinks at every level, because em compoundsrem
color: #999 on whiteFails the 4.5:1 contrast minimum#767676 or darker
Styling with #id selectorsNothing short of another id or !important overrides themClasses
A comma after the last selector in a listThe whole rule is droppedNo trailing comma
@import below other rulesIgnoredPut 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.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.