Web design

Learn CSS from scratch

CSS decides how the thing HTML built should look: colour, spacing, size and where each element sits. Every rule has three parts, a selector that says which element, a property that says what, and a value.

  • Lesson 3 of 12
  • Beginner
  • Free, no signup

How a style reaches an element

element
  1. 1

    A style attribute on the tag

    The closest layer to the element, and the hardest thing to change later.

  2. 2

    Your rule with a class or an id

    The more specific the selector, the more weight it carries. An id is the heaviest, which is why you should use it less.

  3. 3

    Your rule with a tag name

    Your lightest rule, and still ahead of the browser default.

  4. 4

    The browser default

    Large headings, blue links, bulleted lists. This is where everything was defined before you arrived.

  5. 5

    A value inherited from the parent

    The weakest layer. A colour on body never reaches links, because the browser has a direct rule for links.

important throws this order out, which is why it is a last resort. Inheritance is the outermost layer: any direct rule, even the browser own, beats it.

Last checked: Facts and tool names in this lesson are re-checked against their sources on this date.

What does CSS control, and how does it reach the page?

It does one job: it decides how the thing HTML built should look. CSS is short for cascading style sheets, and its smallest unit is a rule:

p {
  color: #333;
  line-height: 1.9;
}

There are three ways to attach it, and in practice we use one. You can write a style attribute on the tag itself, you can put rules inside a style tag in the head, or you can keep everything in a separate file and call it with one line:

<link rel="stylesheet" href="style.css">

Pick the third and keep the other two for exceptions. The reason is not taste: a separate file downloads once and the browser takes it from its own cache on the next page, and the day the button colour has to change you change one place instead of fifty.

One thing confuses every beginner. Before you write a single line of CSS, the page already has styles: the heading is large and bold, links are blue and underlined, lists have bullets. The browser puts those there with its own built in style sheet, and what you write is a rewrite of them.

Selectors: telling the browser which element

Write a tag name and you get every one of those tags. Write a dot and a class name and you get only the elements carrying that class; write a hash and an id and you get one unique element.

a         { color: #0b5; }         /* every link */
.btn      { border-radius: 12px; } /* anything with class="btn" */
#header   { position: sticky; }    /* the element with id="header" */
.card p   { margin-block: 0; }     /* every p inside .card */
a:hover   { text-decoration: underline; }

In practice, write classes almost always. An id is unique on the page and earns its keep for links and for JavaScript, but for styling it brings weight, and later it forces you to bring more weight to override it. That is the road that ends at important.

There is one selector that gets taught less and matters to keyboard users. hover only means something to a mouse; somebody moving through the page with Tab has to see which element they are standing on right now:

.btn:focus-visible {
  outline: 2px solid #0b5;
  outline-offset: 2px;
}

If you removed the browser default ring with outline: none and put nothing in its place, you blinded the page for keyboard users. This is one of the few places where a single line of CSS damages accessibility directly.

Why is my style not being applied?

The short answer: your rule was applied, another one simply beat it. For every property of every element the browser asks three things in order. Which rules target this element at all, which of them is more specific, and if they are equally specific, which came later in the file.

The figure at the top of this page is those layers, from the closest to the element outward. style on the tag itself is the closest, which is why it is the hardest thing to change later, and the browser default is the furthest. The place most misunderstandings happen is the outermost layer: inheritance.

You set a colour on body and expect links to take it too. They do not. The browser has a direct rule for the a tag, and any direct rule, even the browser own, beats a value inherited from a parent. Inheritance is the weakest layer, not the strongest.

Now the fix, which takes ten seconds. Instead of guessing, right click the element, choose inspect and open the Computed tab. The final value of every property is written there, and expanding one shows you the winning rule with its file name and line number, and the losing rules struck through. Guessing has no substitute for this.

Then there is important, the word that breaks that order. Write it once and every later rule has to write it too in order to win, and from then on your file runs on weight rather than on logic. Our rule is simple: if it does not work without important, the selector is wrong. The one real exception is styling code you did not write and cannot reach.

Every element is a box

Everything on the page is a rectangle, even a single letter. That rectangle has four layers: the content itself, padding as the inner space up to the frame, border as the line around it, and margin as the outer space to the other elements. All spacing work in CSS is those four.

There is also a default decision that nearly everybody changes and few explain. By default the number you write in width is the size of the content only, so an element at a hundred percent width that also has padding pushes out of its place. One line fixes it for good:

*, *::before, *::after { box-sizing: border-box; }

With border-box the width number includes padding and border, which is what you expected in the first place. Put that line at the top of every CSS file and stop thinking about it.

One more behaviour surprises everyone once. The vertical space between two stacked elements does not add up; the larger of the two wins. This is called margin collapsing, and it happens only to vertical margins, never to padding and never inside flex or grid. It is one of the reasons modern layouts reach for gap instead of margin.

Layout: when flexbox, when grid?

Flexbox has one axis and grid has two, and that difference settles most decisions. With flex you line a few things up in a row or a column and let each one size itself from its own content. With grid you draw the structure first, you define the columns and rows yourself, and the content then sits inside them.

.toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}

That second rule is worth knowing by heart. It says each column should be at least 220 pixels and should share the leftover space, and the browser works out how many columns fit at any width. A responsive grid without a single media query.

The two are not rivals, and choosing one does not mean giving up the other. In practice the skeleton of a page is laid out with grid and a flex row sits inside each cell. Rule of thumb: if you can say "these few things next to each other", flex; if you say "this table of cells", grid.

Flexbox and grid: two tools for two kinds of problem

One lines content up along a single axis, the other defines a structure on two.

Flexbox, one axis

  • One row or one column, each item sized by its own content
  • For a toolbar, a menu, a row of buttons, an icon sitting beside text
  • When you do not know how many items there will be and only want them side by side
  • With wrap it moves to the next line, but the columns do not stay aligned

Grid, two axes

  • You define the columns and rows, and the content sits inside them
  • For the page skeleton, a card grid, anywhere vertical alignment matters
  • With repeat and minmax it picks the column count at any width, without a media query
  • Overkill for a simple row, where flex is shorter and easier to read

On a real page both work together: the skeleton in grid, a flex row inside each cell. Choosing one is not giving up the other.

Why a Persian site needs margin-inline-start, not margin-left

Because margin-left is not the same place in Persian as it is in English. Physical properties think in terms of the left and right of the screen; logical properties think in terms of the start and end of the line. margin-inline-start means the space from wherever the line begins; the browser reads that as the right in Persian and Arabic and as the left in English, and you do nothing.

The translation is not hard:

PhysicalLogicalWhat it does in right to left
margin-leftmargin-inline-startspace from the start of the line, the right side in Persian
padding-rightpadding-inline-endinner space from the end of the line, the left side in Persian
left: 0inset-inline-start: 0sticking to the edge the line starts from
border-leftborder-inline-startthe line beside the element, always on the start side
text-align: lefttext-align: starttext aligned to the start of the line, correct in both
width and heightinline-size and block-sizesize along the line and across it

Testing it takes ten seconds and nobody teaches it: in the browser developer tools, on the html tag, change dir from rtl to ltr and look at the same page. Anything that did not move, or that stuck to the wrong edge, is a physical property left behind. On our own site that check is part of reviewing every new template.

Where the rule is not needed: a site with one left to right language that will never take another. There margin-left never bites, and insisting on logical properties is a good habit rather than a requirement. But if your site is Persian, or may one day also be English, this difference decides whether that day costs you an hour or a week.

The same card, with physical and with logical properties

Four language layout review sheetCSS

With logical properties

  • One CSS file for all four languages, with no separate right to left build
  • Adding a new language needs no CSS work
  • An icon beside text always stays on the correct side
  • Testing it is changing one dir attribute in the developer tools

With physical properties

  • A second language needs either a second file or a set of mirrored rules
  • Every time somebody adds a new margin-left, it breaks again in that spot
  • The failures are quiet: nothing breaks, it just sits wrong
  • On a single language site none of this ever happens

The difference only shows up once a site has more than one direction. On a single language left to right site, physical properties never break anything.

CSS variables: define once, use everywhere

A CSS variable is a name holding a value. Its technical name is a custom property, it starts with two dashes and it is read with var:

:root {
  --brand: #0b5;
  --gap: 16px;
}

.btn {
  background: var(--brand);
  padding: var(--gap);
}

What gets said less is that this variable inherits like any other value. Redefine the same name on one element and everything inside that element sees the new value while the rest of the page stays untouched:

.card--warn { --brand: #d97706; }

Now every button inside that card is orange without a single line of the .btn rule changing. A dark theme is built on exactly this idea: one block that redefines a handful of names, not a second CSS file.

And the limit: this is not a variable in the programming sense. Arithmetic with it happens only inside calc, and you cannot use it in the condition of a media query. If you are after loops, conditions and functions, you are heading towards tools like Sass, whose whole job is that and whose output is this same plain CSS.

The fast path, with AI

Language models are good at "build me this layout", because that job is a mechanical translation of a description into flex and grid. Where they fall short is exactly where a beginner gets stuck: working out why a style did not apply. For that they would have to see your page, and they do not. So the fast path has two halves: hand over the layout, keep the cascade. For the first half a fast cheap model is enough; our current pick is in <a class="text-link" href="/en/ai/">the AI section</a>.

  1. Isolate the block that has the problem and take its real HTML. Describing the page instead of pasting the markup is the biggest single cause of an irrelevant answer.
  2. Run the recipe below. If you already have colour and spacing variables, list them right there so the model does not invent new names.
  3. Drop the output in and test both directions: change the dir value on the html tag in the developer tools and look at the same page.
  4. If a style did not apply, do not go back to the chat. Open the Computed tab: it says which rule won, from which file and which line.

Copy-ready recipe

Role: someone who writes modern CSS, with no framework.

What I want:
Section: {section name}
Current markup: {paste the HTML here}
Desired behaviour: {for example three cards side by side, stacked on mobile}
Direction: right to left (Persian), but the same code must be correct in left to right too

Output rules:
- Give CSS only. Do not change the markup; if a class has to be added, say where and why first.
- All spacing and direction through logical properties: margin-inline, padding-inline, inset-inline-start, text-align: start. Write no physical left or right.
- Take colours and spacing from these variables: {list your existing variables}. Do not invent a new variable without stating why.
- One axis layout with flex, two axis with grid. Write one line on which you chose and why.
- No important. If it does not work without it, the selector is wrong; say that instead.
- No fixed pixel widths except for something genuinely fixed, such as an icon.
- Anything with a hover state must have a focus-visible state too.

After the code, say in three lines what moves in left to right, and where you were unsure.

Before you trust the output: Two things you have to check yourself. First direction: no automated tool tells you the page sits wrong in the mirror, only your own eyes do. Second the cascade: ask a model why your style did not apply and you get something shaped like an answer but which is a guess, because it never saw your page. That question is answered somewhere else, in the Computed tab of your own browser.

AI in this kind of work

In CSS, AI has one clear place and stops being useful quickly outside it. Three things it genuinely speeds up: turning a described layout into flex and grid, explaining an unfamiliar block of CSS line by line, and converting a file full of left and right into its logical version. Two we do not hand over: deciding what the page should look like, and finding out why a rule did not apply.

Tools that actually help

  • Claude Good at explaining a CSS file line by line and at rewriting physical properties into logical ones. Iran is not on Anthropic supported-countries list; we read that on Anthropic own page.
  • Gemini Enough for building a layout from a description and for basic questions, and it handles Persian well. Google own page says the Gemini web app runs in over 230 countries and territories, and Iran is not on that list.
  • Claude Code Useful once you have to change a whole file rather than a snippet, because it works in your own editor and terminal and can see the file. It installs free but needs an Anthropic subscription, and Iran is not on the supported-countries list.

Where it backfires

Two specific risks here, and both are quiet. First direction: the model does not know your page is right to left unless you say so, so if you do not ask in the prompt, nothing stops margin-left, and the output is correct in English and mirrored in Persian. That is why the recipe above asks for logical properties explicitly; MDN documents the full set. Second the cascade: the model cannot see your page, so any answer about why a style did not apply is a guess, and the guesses that happen to work raise selector weight or add important. Both work once and make the next change harder. Our position: layout with the model, cascade with the browser. For how each tool can be paid for from Iran, see the buying guide.

Sources: MDN: CSS logical properties and values MDN: Handling conflicts, the cascade and specificity Anthropic: supported countries Google: where Gemini Apps are available

Where this advice stops

CSS builds how one page looks, not a site. Content and structure are HTML, behaviour is JavaScript, and getting the page to open on the internet is hosting and a domain. And a point that gets made less: if you are putting your site up on WordPress with a ready theme, the whole CSS you need may be twenty lines, and going deep on grid is not your fast path. This lesson is written for someone who wants to control the look themselves, or to understand what their theme is doing. The logical properties rule is just as conditional: on a site that will have one left to right language forever, following it is a good habit that rescues nothing.

From our own work

This very site is our own example and can be checked. The main style file of rgb.ir is about a hundred and thirty kilobytes and serves all four languages at once: Persian and Arabic right to left, English and Turkish left to right, with no separate rtl file anywhere in the theme. On the day this lesson was checked, that file used logical properties in seventy places and had zero occurrences of margin-left, padding-right or even a bare left or right; no text-align: left and no float either. And one necessary piece of honesty: four margin-left lines remain in the diagram stylesheet, deliberately, because all four are the same centring trick whose value is symmetric, and there left carries no direction at all. The rule is that logical properties are needed where a layout has a start and an end, not everywhere the word left appears.

Real follow-up questions

Should I learn HTML first, or can I start straight with CSS?

HTML first, but not until you have finished it. A CSS selector points at elements and classes, so until you know what the main elements are, half the rules you write will match nothing. One afternoon of HTML is enough for CSS to make sense.

Should I go straight to Bootstrap or Tailwind?

Not in the first week. A framework hides the very cascade you have not learned yet behind a layer of classes, and the day something does not sit where it should, you have no tool for understanding it. Once selectors, the cascade and grid make sense, picking a framework is a reasonable project decision rather than a shortcut.

Why does my site fall apart on a phone?

Usually one of two things: the viewport tag is missing from the head, or somewhere you wrote a fixed pixel width larger than a phone screen. The first is one line, covered in the HTML lesson of this path; the second is normally a width that should have been a max-width. Responsive design is the next lesson in this path.