Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 1 – How CSS works

Adding CSS to HTML and browser default styles

Add CSS with a link element, a style element or a style attribute, see what browsers style before your CSS loads, and write a small base stylesheet.

  • Beginner
  • 25 minutes
  • Examples run with Node.js 24.21.0
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Add CSS with a link element, a style element and the style attribute, and choose the right one for a job
  • Explain what the browser's default (user-agent) styles do before any CSS is written
  • Predict which of two linked stylesheets wins when their selectors tie
  • Write a small modern base stylesheet instead of copying a large reset

Before you start

What you’ll learn · Before you start

What you will learn

  • Add CSS with a link element, a style element and the style attribute, and choose the right one for a job
  • Explain what the browser's default (user-agent) styles do before any CSS is written
  • Predict which of two linked stylesheets wins when their selectors tie
  • Write a small modern base stylesheet instead of copying a large reset

Before you start

On this page

There are three ways to add CSS to an HTML page. A <link rel="stylesheet"> element in the <head> loads a separate .css file, and it is the right choice for almost all CSS: one file styles every page that links it, and the browser keeps a copy for the next page. A <style> element holds CSS inside the page itself, which suits styles that only that page needs. A style attribute puts declarations on a single element. It beats every rule in your stylesheets, but it cannot use selectors, media queries or states such as :hover. And before any of your CSS arrives, the browser has already styled the page with its own defaults: a margin around the body, large bold headings, blue underlined links.

This lesson shows the three ways and when each fits, measures the defaults in Chromium, Firefox and WebKit, and ends with a short base stylesheet that keeps the useful defaults and changes the rest.

The three ways in one page HTML · three-ways.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Order status</title>

  <!-- 1. A link element: an external stylesheet, shared by every page that links it -->
  <link rel="stylesheet" href="/css/site.css">

  <!-- 2. A style element: CSS for this page only -->
  <style>
    .status {
      font-weight: 600;
    }
  </style>
</head>
<body>
  <!-- 3. A style attribute: declarations for this one element -->
  <p class="status" style="color: #0b6e4f">Your parcel is on its way.</p>
</body>
</html>

Which way to use

link, style or style attribute?
Criterion <link><style>style=
Lives in A .css fileThe pageOne element
Styles Every page linking itOne pageOne element
Cached YesNoNo
Can use :hover YesYesNo
Priority UsualUsualHigher
Under style-src 'self' AllowedBlockedBlocked
When to choose Almost all CSSOne page, critical CSSValues set by scripts

The rest of this lesson takes the three in turn, then looks at what happens when two stylesheets disagree.

Linking a stylesheet

<link rel="stylesheet" href="/css/site.css"> asks the browser to fetch site.css and apply it to the page. Put it in the <head>. The HTML Standard makes a stylesheet linked there render-blocking: the browser does not show the page until the file has arrived and been parsed, so nobody sees the bare, unstyled HTML first. A stylesheet linked later in the <body> does not hold back the first paint, which is why late stylesheets can make a page jump as styles arrive. One exception helps: a link whose media attribute does not match the screen, such as media="print", does not block rendering either.

Blocking is the right default, but it means the size and number of stylesheets in the <head> set how soon anything appears, and @import makes that worse.

Why @import inside CSS is slow

An @import at the top of a stylesheet pulls in another stylesheet. The catch is that the browser only learns about the imported file after the file that imports it has arrived, so each level of importing adds a full request to the wait. Here main.css imports theme.css, which imports type.css:

Three stylesheets that import each other

main.css

@import url("theme.css");

.page {
  max-inline-size: 40rem;
  margin-inline: auto;
}

theme.css

@import url("type.css");

:root {
  color: #1d3557;
  background: #f8fafc;
}

type.css

body {
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

The program below reads those three files, follows the @import lines the way a browser discovers them, and compares the result with a <link> for each file, assuming every request takes 300 ms:

When each stylesheet arrives JavaScript · waterfall.mjs
// When each stylesheet arrives: one <link> to a file that @imports the next, against a <link> for every file.
// A model with one fixed cost per request: a stylesheet arrives ROUND_TRIP_MS after the browser asks for it. The
// browser can ask for an imported file only once the file that imports it has arrived, but it can ask for every
// <link> in the <head> at once. The measured runs in the lesson used the same 300 ms per request.
import { readFileSync } from "node:fs";

const ROUND_TRIP_MS = 300;

const importsOf = (file) =>
  [...readFileSync(file, "utf8").matchAll(/@import\s+url\(\s*"([^"]+)"\s*\)/g)].map((m) => m[1]);

// Follow the @import lines from the linked file, the way the browser discovers them.
function importChain(entry) {
  const rows = [];
  const visit = (file, askedAt) => {
    const arrives = askedAt + ROUND_TRIP_MS;
    rows.push({ file, askedAt, arrives });
    for (const next of importsOf(file)) visit(next, arrives);
  };
  visit(entry, 0);
  return rows;
}

const bar = (r) => " ".repeat(r.askedAt / 100) + "#".repeat((r.arrives - r.askedAt) / 100); // one # per 100 ms

function show(title, rows) {
  console.log(title);
  for (const r of rows) {
    console.log(`  ${r.file.padEnd(10)} ${String(r.askedAt).padStart(3)} to ${String(r.arrives).padStart(3)} ms  ${bar(r)}`);
  }
  const done = Math.max(...rows.map((r) => r.arrives));
  console.log(`  rendering can start at ${done} ms, once every stylesheet has arrived\n`);
}

const chain = importChain("main.css");
show("One <link> to main.css, which imports theme.css, which imports type.css", chain);
show("A <link> for each of the three files", chain.map((r) => ({ file: r.file, askedAt: 0, arrives: ROUND_TRIP_MS })));

Output

One <link> to main.css, which imports theme.css, which imports type.css
  main.css     0 to 300 ms  ###
  theme.css  300 to 600 ms     ###
  type.css   600 to 900 ms        ###
  rendering can start at 900 ms, once every stylesheet has arrived

A <link> for each of the three files
  main.css     0 to 300 ms  ###
  theme.css    0 to 300 ms  ###
  type.css     0 to 300 ms  ###
  rendering can start at 300 ms, once every stylesheet has arrived

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node waterfall.mjs

Real browsers behave the same way. Served from a test server on an Apple M5 Pro that answered every stylesheet after 300 ms, the page with the import chain and the page with three links were each loaded three times, without a cache:

The table gives the time until all three stylesheets had loaded:

Engine Import chain Three links
Chromium 153 915 to 917 ms 311 to 313 ms
Firefox 155 949 to 1,069 ms 360 to 515 ms
WebKit 26.6 925 to 947 ms 311 to 322 ms

Firefox and WebKit also report when they first painted the page: 3 to 12 ms after the last stylesheet arrived, in every run that recorded it. Three round trips instead of one is the whole cost, and on a phone network, where a round trip can take far longer than on a fast connection, it grows with every level of importing. The fix needs no change to the CSS rules: link each file from the HTML, or combine them into one file when the site is built. @import is still useful in development and with cascade layers, which the cascade module covers.

The style element

A <style> element holds CSS in the page. A <style> element in the <head> blocks rendering just as a linked stylesheet does, but there is no extra request: the CSS arrives with the HTML. That makes it a fit for styles only this page needs, and for a small block of the most important styles, inlined so that the top of the page can appear while the full stylesheet is still loading in a way that does not block rendering. The cost is that the CSS is sent again with every page instead of being cached once.

The style attribute

A style attribute holds declarations for one element: style="color: #0b6e4f". The CSS Style Attributes specification defines its value as the inside of a declaration block, so there is no selector, and with it no :hover, no ::before and no @media. In the cascade, declarations attached to an element beat declarations from any style rule of the same importance, however specific the rule’s selector is. From a stylesheet, only an !important declaration or a running animation wins over it. That is why style attributes are hard to override and best kept for values a script sets at run time, such as the position of an element someone is dragging.

Style attributes are also the first thing a strict Content Security Policy removes. A policy is a response header that lists where a page may load things from. With style-src 'self', a page may use stylesheets from its own site, and nothing else:

One page, three kinds of CSS, a strict policy HTML · strict-policy.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A strict style policy</title>
  <!-- The server sends the header  Content-Security-Policy: style-src 'self'  -->
  <link rel="stylesheet" href="/site.css"> <!-- site.css holds .card { color: rgb(10, 20, 30); } -->
  <style>
    .card { background-color: rgb(255, 255, 0); }
  </style>
</head>
<body>
  <p class="card" style="border: 3px solid rgb(255, 0, 0)">A card</p>
</body>
</html>

All three engines applied the linked stylesheet, so the text took its colour, and blocked both the <style> block and the style attribute: no yellow background and no red border. Each logged two policy violations in the console, and the violation events named style-src-elem for the block and style-src-attr for the attribute. A policy can allow inline styles again with 'unsafe-inline', or allow particular ones with a nonce or a hash, but the safer habit is not to need them. Scripts are affected differently: under the same policy, all three engines applied a colour that a script set with element.style.setProperty(), and blocked one that a script wrote with setAttribute('style', …). This site’s own pages send style-src 'self', which is why none of its HTML uses style attributes.

When two stylesheets disagree

When two declarations for the same property tie on everything else the cascade looks at, the one that comes later wins. Separately linked stylesheets count as if they were joined end to end in the order of their <link> elements, and an imported stylesheet counts as if its rules were pasted where the @import stands. So if a library’s stylesheet and yours both set .button { background: … }, link yours after the library’s and yours wins.

That is order between equals. A more specific selector earlier in the page still beats a less specific one later, and the cascade module explains how specificity is counted.

Three sources of CSS, weakest first: browser defaults, then your stylesheets, then style attributes on single elements.Browser defaultsthe user-agent stylesheet:margins, heading sizes, link coloursYour stylesheets<link>, <style> and @import:selectors decide what matches,then specificity and order decide tiesstyle attributesdeclarations on one element,no selectors, no media queriesoverridden byoverridden by

Where an element's styles come from

Text description of the diagram

The diagram stacks the three places an element's normal declarations come from, from the weakest at the top to the strongest at the bottom.

  1. Browser defaults: the user-agent stylesheet, which sets things like margins, heading sizes and link colours before any of your CSS loads.
  2. Your stylesheets, from link elements, style elements and @import: selectors decide which elements a rule matches, and when two rules set the same property, specificity and then the order of the rules decide which one wins. They override the browser defaults.
  3. style attributes: declarations attached to one element, with no selectors and no media queries. They override declarations from your stylesheets.

Declarations marked !important change this order; the cascade module of this track covers them.

What the browser styles before your CSS

Every browser ships a user-agent stylesheet, its own default CSS, and the cascade gives it the lowest priority, so any rule of yours overrides it. The HTML Standard’s Rendering section describes the defaults browsers are expected to use, in the form of CSS rules: block display for paragraphs and headings, 2em for a top-level heading, a 1em margin around paragraphs, 40px of indent for lists, an 8px margin around the page. The Standard calls these “expected” rather than required, and it leaves some things, such as the font of form controls, entirely to each browser.

For a page with no CSS of its own, at a width of 1280 px, Chromium 153, Firefox 155 and WebKit 26.6 computed the same values for most elements: an 8px margin around the body, 16px text, a 32px h1 (also inside section and article), a 24px h2, a 16px top margin on paragraphs, 40px of indent on lists, and links in rgb(0, 0, 238), underlined. These follow the Standard: 32px is 2em of 16px. Three things differed:

  • The body font: Times in Chromium, serif in Firefox and -webkit-standard in WebKit.
  • The top margin of an h1: 21.44px in Chromium and Firefox, 21.4375px in WebKit.
  • The font of a button: 13.33px Arial in Chromium, 13.33px in the system font in Firefox and 11px system-ui in WebKit.

The heading margin is 0.67em of 32px; WebKit rounds lengths to 1/64 of a pixel, hence 21.4375px. The default font for body text has a different name in each engine, and form controls differ most: on the same page, a button’s text was 13.33px in Chromium and Firefox and 11px in WebKit, each in a different font. Older browsers also made an h1 inside a section or article smaller; the current Standard has no such rule, and none of the three engines did so.

A small base stylesheet

The defaults are mostly useful: they keep an unstyled page readable, and lists, headings and paragraphs have sensible spacing. A few get in the way of building layouts, and the form-control differences above are a real inconsistency. Instead of a large reset that removes every default and then adds most of them back, a short base stylesheet changes only those:

A base stylesheet that keeps the useful defaults CSS · base.css
/* A small base layer: keep the browser's useful defaults, change only the ones that get in the way. */

/* Widths include padding and border, so a 50% box stays half as wide. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* No 8px gap around the page; easier-to-read lines. */
body {
  margin: 0;
  line-height: 1.5;
}

/* Media never overflows its container and sits on its own line. */
img,
svg,
video {
  display: block;
  max-inline-size: 100%;
  block-size: auto;
}

/* Form controls use the page's font instead of a smaller system font. */
button,
input,
select,
textarea {
  font: inherit;
}

With base.css linked, the same page in all three engines had a 0px body margin and 24px lines of text (1.5 × 16px), images became blocks capped at the width of their container, and every button, input, select and textarea used the page’s 16px font instead of the 11px to 13.33px each engine chose on its own. Headings, paragraphs and lists kept their default sizes and spacing, because nothing in the file touches them.

Link the base stylesheet first, before your other stylesheets, so that everything after it can build on it.

Try it: move inline styles into a stylesheet

This promo card is styled entirely with style attributes:

A card styled with attributes HTML · inline-before.html
<article style="max-width: 32rem; padding: 1rem 1.25rem; border: 1px solid #c8d3e0; border-radius: 8px">
  <h2 style="margin: 0 0 0.5rem; font-size: 1.25rem; color: #1d3557">Monsoon sale</h2>
  <p style="margin: 0; line-height: 1.5">Umbrellas and raincoats, 20% off this week.</p>
  <a href="#shop" style="display: inline-block; margin-top: 0.75rem; padding: 0.5rem 1rem; color: #ffffff; background: #0b6e4f; text-decoration: none">Shop now</a>
</article>

Rewrite it with a class on each element and one stylesheet, so that it looks exactly the same and no style attribute is left. Then add a hover and keyboard-focus colour for the link, something the attributes could not express. You can check your version side by side in the HTML, CSS & JS Playground, or open both in DevTools and compare the computed values of each element. One answer:

The same card, with a stylesheet

HTML · inline-after.html

<article class="promo">
  <h2 class="promo-title">Monsoon sale</h2>
  <p class="promo-text">Umbrellas and raincoats, 20% off this week.</p>
  <a class="promo-link" href="#shop">Shop now</a>
</article>

CSS · inline-after.css

.promo {
  max-width: 32rem;
  padding: 1rem 1.25rem;
  border: 1px solid #c8d3e0;
  border-radius: 8px;
}

.promo-title {
  margin: 0 0 0.5rem;
  font-size: 1.25rem;
  color: #1d3557;
}

.promo-text {
  margin: 0;
  line-height: 1.5;
}

.promo-link {
  display: inline-block;
  margin-top: 0.75rem;
  padding: 0.5rem 1rem;
  color: #ffffff;
  background: #0b6e4f;
  text-decoration: none;
}

/* What a style attribute cannot do: states and conditions. */
.promo-link:hover,
.promo-link:focus-visible {
  background: #095c42;
}
HTML, CSS & JS Playground (Online Editor) Paste the HTML and the CSS into its panes to see the card, and inspect the preview with DevTools.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Your site has 40 pages that share one look. Where should most of its CSS go?

    Choose one answer.

    Show the answer to question 1

    Answer: In one or a few .css files, linked from every page with <link rel="stylesheet">

    A linked stylesheet is written once, used by every page and cached by the browser after the first one, so the next pages need no new request for it. A <style> element repeats the CSS in every page, style attributes repeat it on every element and cannot use selectors or media queries, and an @import adds a request that can start only after the importing CSS has been read.

  2. Question 2 of 6 What colour is the paragraph's text?

    Read the code, then choose one answer.

    <style>
      #intro.lead { color: navy; }
    </style>
    <p id="intro" class="lead" style="color: darkgreen">Welcome</p>
    Show the answer to question 2

    Answer: darkgreen

    Declarations in a style attribute are attached to the element, and the cascade ranks them above every declaration that reaches the element through a selector, whatever its specificity, as long as both have the same importance. Only an !important declaration in a stylesheet, or a running animation, would win here.

  3. Question 3 of 6 A page links app.css, which imports layout.css, which imports colours.css. Every request takes 200 ms and starts the moment the browser knows about the file. After how many milliseconds have all three stylesheets arrived?

    Type a number.

    Show the answer to question 3

    Answer: 600 ms

    The browser learns about each imported file only when the file that imports it has arrived: app.css arrives at 200 ms, layout.css at 400 ms and colours.css at 600 ms. Three links in the HTML would all have arrived at about 200 ms.

  4. Question 5 of 6 A page is served with Content-Security-Policy: style-src 'self'. Which of these styles still apply?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • A stylesheet on the same site, linked with <link rel="stylesheet">

    'self' allows stylesheets from the page's own origin. Inline CSS, both <style> blocks and style attributes, is blocked unless the policy allows it with 'unsafe-inline', a nonce or a hash, and stylesheets from other origins need to be listed in the policy.

  5. Question 6 of 6 Why does a small base stylesheet set font: inherit on buttons, inputs, selects and text areas?

    Choose one answer.

    Show the answer to question 6

    Answer: Browsers give form controls their own font and size, which differ between engines, instead of the page's font

    On a page with no CSS, a button's text was 13.33px Arial in Chromium, 13.33px in the system font in Firefox and 11px system-ui in WebKit, while the page's own text was 16px. font: inherit makes all four controls use the font of the text around them in every engine. It does not change their colours.

Interview questions

What is the difference between linking a stylesheet with <link> and loading it with @import?

Both load an external stylesheet, but the browser finds them at different times. A <link rel="stylesheet"> sits in the HTML, so the browser sees it while parsing the <head> and can request every linked stylesheet at once. An @import sits inside a stylesheet, so the imported file can be requested only after the file containing it has arrived and been read; a chain of imports therefore costs one full round trip per level, while the page waits. In a test with 300 ms per request, three chained stylesheets finished loading after 0.9 to 1.1 seconds and three links after 0.3 to 0.5. For the cascade, the two are equivalent: an imported stylesheet counts as if its rules were written where the @import stands. Use <link>, or combine files when the site is built.

Key takeaways

  • Link stylesheets with <link rel="stylesheet"> in the <head>: it is cached, works on every page and blocks rendering only until it arrives.
  • A <style> element is for one page’s styles or a small critical block; it arrives with the HTML and is not cached separately.
  • A style attribute holds declarations only, beats every rule of the same importance, and is blocked by a strict Content Security Policy.
  • Each level of @import adds a full request before the page can render; link the files instead.
  • Between equally specific rules, the later one wins, in the order of the <link> elements, with imports counted in place.
  • Browsers agree on most defaults (8px body margin, 32px h1, 40px list indent) but not on form-control fonts, so a small base stylesheet with font: inherit evens them out.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.