font-family Is a List, Not a Choice
font-family does not name one font. It gives the browser an ordered list of preferences, and the browser walks that list until it finds one it can actually use — either installed on the device or downloaded by your CSS. If the first name is unavailable, it quietly tries the second, then the third. This is why the property is almost always written with several names separated by commas.
The last name in the list should always be a generic family: serif, sans-serif, monospace, cursive or fantasy. These are not fonts — they are categories, and the browser substitutes whatever it considers its default of that kind. Ending with a generic guarantees that even a device with none of your preferred fonts still renders something appropriate rather than falling back to a random default.
Two practical rules about writing the names. A font name containing spaces needs quotes: 'Courier New', 'Times New Roman'. And system-ui is a special value worth knowing — it means "whatever the operating system uses for its own interface", so your page picks up the native look on every platform and downloads nothing at all. For an interface, that is often the best-performing choice available.
One caveat that matters for Indian-language content: a font stack that looks perfect for English says nothing about how Devanagari, Bengali, Tamil or Odia text will render. If your page mixes scripts, test it. The browser will fall back per character to whatever font on the device can draw that script, and the result can look mismatched unless you plan for it.
/* Read as: try these in order, settle for the category at the end */
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
}
h1, h2 {
font-family: Georgia, 'Times New Roman', serif;
}
code, pre {
font-family: 'Courier New', ui-monospace, monospace;
}
/* Names with spaces must be quoted */
.wrong { font-family: Times New Roman, serif; } /* invalid */
.right { font-family: 'Times New Roman', serif; } /* correct */ - The browser uses the first name in the list it can resolve
- Always end the list with a generic family such as
sans-serif - Quote any font name containing spaces
system-uiuses the device's own interface font and costs nothing to loadfont-familyis inherited, so setting it onbodycovers the whole page
px, em and rem — and Why the Choice Is About Accessibility
px is an absolute size. font-size: 16px is sixteen pixels, always. That predictability is why beginners like it, and for borders and small fixed details it is genuinely the right unit.
For text it has a real cost. Every browser has a setting for default text size, and people who find small text hard to read change it — that is exactly what the setting is for. When you size text in px, you override that preference and their choice does nothing on your site. It is not a rare edge case; it is one of the most-used accessibility settings there is.
rem means "relative to the root element's font size". The root is <html>, and its font size is 16px by default — but it is whatever the reader has chosen, and it moves when they change the setting. So font-size: 1rem normally renders at 16px, while 1.5rem is 24px, and if a reader raises their default to 20px, your whole type scale grows with it. Same design, same proportions, readable text. Do the mental arithmetic by dividing by 16: 24px is 1.5rem, 14px is 0.875rem, 12px is 0.75rem.
em is relative to the font size of the parent element, and that difference is the trap. Nest three lists each set to font-size: 0.9em and the sizes multiply: 0.9, then 0.81, then 0.729, and by the fourth level the text is unreadably small. rem never compounds, because it always measures from the root regardless of nesting. Use rem for font sizes, and keep em for values that should scale with the element's own text — padding inside a button, or letter-spacing on a label.
/* Sizes that respect the reader's browser setting */
html { font-size: 100%; } /* honour the default; do not set 62.5% */
body { font-size: 1rem; } /* 16px by default */
h1 { font-size: 2.5rem; } /* 40px by default */
h2 { font-size: 1.75rem; } /* 28px */
.small { font-size: 0.875rem; } /* 14px */
/* em compounds — this is the classic bug */
.menu { font-size: 0.9em; }
.menu .menu { font-size: 0.9em; } /* 0.81em of the original */
/* em used well: padding that grows with the button's own text */
.btn {
font-size: 1rem;
padding: 0.75em 1.5em; /* stays proportional at any size */
}
.btn--large { font-size: 1.25rem; } /* padding scales automatically */ - You may still see the trick of setting
html { font-size: 62.5%; }so that 1rem equals 10px and the arithmetic is easier. Avoid it. It shrinks the reader's chosen size to 62.5% of what they asked for, which is precisely the problemremwas meant to solve. Divide by 16 instead.
Weight, Style, and What the Browser Fakes
font-weight accepts the keywords normal and bold, or a number from 100 to 900 in steps of 100. The numbers are the more useful form because they give you the middle ground: 400 is the same as normal, 700 is the same as bold, and 500 or 600 gives a heading a little authority without shouting.
There is a catch. A font family only contains the weights its designer actually drew. If you ask for font-weight: 700 and the loaded font has no bold cut, the browser does not give up — it synthesises one by thickening the letterforms itself. The result usually looks slightly smeared and uneven next to a real bold, and it is a common reason a page "looks a bit off" without an obvious cause. The same happens with font-style: italic: an artificial italic is just the upright letters slanted, which is visibly different from a properly drawn italic.
The fix is to load the weights you use and use the weights you load. If your design needs regular and bold, request 400 and 700 from your font source and nothing else. Every additional weight is another file the visitor downloads before they can read anything, so restraint here is both a design decision and a performance one.
/* Numbers give you more room than the keywords */
.body-text { font-weight: 400; } /* normal */
.subtle { font-weight: 300; }
.heading { font-weight: 600; } /* half a step short of bold */
.strong { font-weight: 700; } /* bold */
/* Italic and its relatives */
.quote { font-style: italic; }
.plain { font-style: normal; } /* cancels an inherited italic */
/* The font shorthand: size and family are both required */
.caption { font: italic 400 0.875rem/1.5 Georgia, serif; }
/* style weight size/line-height family */ 400=normal,700=bold- Ask only for weights the font actually includes, or the browser fakes them
- Every extra weight is an extra file to download
font-style: normalis how you un-italicise something like<em>inside a quote- The
fontshorthand resets any font property you leave out — use longhands when adjusting one thing
Web Fonts and Type That Adapts to the Screen
A web font is a font file your CSS asks the browser to download, so your page can use a typeface that is not installed on the visitor's device. You register it with @font-face, giving it a name and a source, and from then on you refer to that name in font-family like any other font. Use the woff2 format — it is the most compressed and every current browser supports it.
The property that matters most here is font-display: swap. Without it, some browsers hide your text entirely while the font downloads, which on a slow connection means a visitor stares at a blank page holding text that is already there. With swap, the text is drawn immediately in the fallback font and re-drawn when the custom font arrives. The brief change of appearance is a far better experience than invisible content, and on patchy mobile data it is the difference between a usable page and an empty one.
Finally, sizing text for very different screens. Rather than writing a heading at one size and then overriding it in media queries, clamp() lets you state a minimum, a preferred value that scales with the viewport, and a maximum, all in one declaration. The heading grows smoothly between a phone and a desktop and stops at both ends. Keep a rem term inside the middle value, as in the example — a preferred size made only of vw units ignores the reader's text-size preference entirely.
/* Register a font you host yourself */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* show fallback text immediately */
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
body { font-family: 'Inter', system-ui, Arial, sans-serif; }
/* One declaration instead of three media queries */
h1 {
font-size: clamp(1.75rem, 1.25rem + 2.5vw, 3rem);
/* minimum preferred (scales) maximum */
} - Every web font is a file the visitor must download before your page looks finished. Two weights of one family is a reasonable budget; five families is not. If the design does not depend on a particular typeface, a
system-uistack loads instantly and looks native on every device.
