Lesson 2 of 25

CSS Syntax

The Shape of Every Rule

CSS has a very small grammar. Once you can see the shape of a rule, you can read any stylesheet in the world, even one using properties you have never met. A rule is a selector, then a pair of curly braces, then any number of declarations inside them. Each declaration is a property, a colon, a value, and a semicolon.

A selector can be a list. Write several selectors separated by commas and the same declaration block applies to all of them, which saves you from repeating identical rules three times. This is the single most common way stylesheets stay short.

Everything else in a stylesheet is one of three things: a comment, an at-rule (a line beginning with @, covered further down this lesson), or whitespace, which the browser ignores entirely. There is no if, no loop and no function definition. CSS is a list of instructions about appearance, and its power comes from selectors and the cascade rather than from control flow.

Example
/*  selector
      |
      v            */
    .price {
      color: #d1039e;      /* property: value; */
      font-size: 20px;
      font-weight: 700;
    }
/*  ^                ^
    declaration block */

/* A selector list — one block, three targets */
h1, h2, h3 {
  font-family: Georgia, serif;
  line-height: 1.25;
}
  • Selector — which elements this rule affects
  • Declaration block — the { ... } that follows the selector
  • Declaration — one property: value; pair
  • Selector list — several selectors separated by commas, sharing one block
  • At-rule — a statement starting with @, such as @media or @keyframes

Values Have Types, and the Type Matters

Every property accepts only certain kinds of value, and giving it the wrong kind means the declaration is thrown away. color wants a colour. width wants a length or a percentage. display wants one keyword from a fixed list. There is no automatic conversion and no error message — just a rule that appears not to work.

The rule that catches beginners most often is that lengths need units. width: 300 is invalid and gets dropped; it has to be width: 300px. The one exception is zero: margin: 0 is valid, because zero of anything is the same size. A few properties are deliberately unitless because their value is a ratio or a rank rather than a measurement — line-height: 1.6 means "1.6 times this element's font size", opacity: 0.5 is a fraction, and z-index: 10 is a position in a stack, not ten of something.

Some values are written as functions: rgb(209, 3, 158) builds a colour from three numbers, url('bg.jpg') points at a file, var(--brand) reads a custom property, and calc() does arithmetic across different units. calc() has one syntax rule that trips up everybody: the + and - operators must have a space on both sides. calc(100% - 40px) works; calc(100%-40px) is invalid and silently ignored.

Example
/* Lengths need units — except zero */
.box { width: 300px; margin: 0; }
.box { width: 300; }          /* invalid, dropped */

/* Deliberately unitless values */
p      { line-height: 1.6; }   /* a multiplier */
.faded { opacity: 0.5; }       /* a fraction from 0 to 1 */
.modal { z-index: 10; }        /* a rank, not a size */

/* Functions as values */
.brand  { color: rgb(209, 3, 158); }
.hero   { background-image: url('hero.jpg'); }
.button { background: var(--brand, #d1039e); }

/* calc() needs spaces around + and - */
.main { width: calc(100% - 40px); }   /* works */
.main { width: calc(100%-40px); }     /* invalid */
Notes
  • line-height: 1.6 and line-height: 160% are not the same thing. The unitless version is recalculated from each element's own font size, so a child with larger text gets proportionally more line spacing. The percentage is computed once on the parent and the resulting fixed value is inherited, which can leave big text looking cramped. Prefer the unitless form.

Shorthand Properties and the Trap Inside Them

A shorthand property sets several related properties in one declaration. margin: 10px 20px is shorter than four separate lines, and border: 1px solid #ccc is far more readable than setting width, style and colour individually. When a shorthand takes multiple lengths, the order runs clockwise from the top: top, right, bottom, left. Give it two values and it means top-and-bottom, then left-and-right. Give it three and it means top, then left-and-right, then bottom.

Here is the trap. A shorthand does not only set the values you wrote — it resets every property it controls, and anything you left out goes back to its initial value. Write background: #f4f4f7 after a rule that set background-image, and the image disappears, because the shorthand quietly set background-image: none along with the colour you asked for. The same thing happens with font, border and transition.

That leads to a simple working habit. Use the shorthand when you are defining an element's look from scratch, and use the longhand when you are adjusting one aspect of something already styled. If a hover rule only needs to change the border colour, write border-color, not border — otherwise you will accidentally reset the width and style too and wonder why the box jumps by a pixel.

Example
/* Clockwise from the top */
.a { margin: 10px; }                    /* all four sides 10px */
.b { margin: 10px 20px; }               /* top+bottom 10, left+right 20 */
.c { margin: 10px 20px 30px; }          /* top 10, sides 20, bottom 30 */
.d { margin: 10px 20px 30px 40px; }     /* top, right, bottom, left */

/* The reset trap */
.hero {
  background-image: url('hero.jpg');
  background-size: cover;
}
.hero.plain {
  background: #f4f4f7;   /* also sets image to none and size to auto */
}

/* Change one part safely with the longhand */
.button        { border: 2px solid #d1039e; }
.button:hover  { border-color: #7a0060; }   /* width and style survive */
Notes
  • The font shorthand is stricter than the rest: it is only valid if you include both a font size and a font family, in that order at the end. font: bold 16px/1.5 Arial, sans-serif; is valid; font: bold Arial; is not, and the whole declaration is discarded.

At-Rules: Instructions About the Stylesheet Itself

An at-rule begins with @ and does something a plain rule cannot. Some are a single statement ending in a semicolon; others open a block containing further rules. You only need four of them to build a complete modern site, and each gets proper treatment in a later lesson.

@media is the one you will use constantly. It wraps a group of rules in a condition — usually about the width of the screen — so those rules apply only when the condition is true. That single at-rule is what makes a layout responsive. @keyframes defines the stages of an animation and gives them a name. @font-face tells the browser where to download a custom font file from, so you are not limited to fonts already installed on the visitor's device.

@import pulls in another stylesheet from inside a stylesheet. It works, but prefer a second <link> tag in your HTML: the browser cannot discover an @imported file until it has downloaded and started parsing the first stylesheet, so the two files load one after the other instead of together. On a slow connection that is a visible delay for no benefit.

Example
/* Apply these rules only on screens 768px and wider */
@media (min-width: 768px) {
  .layout { display: grid; grid-template-columns: 1fr 2fr; }
}

/* Name a sequence of states */
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Load a font file yourself */
@font-face {
  font-family: 'MyFont';
  src: url('fonts/myfont.woff2') format('woff2');
  font-display: swap;
}

/* Works, but a second <link> tag loads faster */
@import url('typography.css');
  • @media — apply rules only when a condition about the device or screen is true
  • @keyframes — define the named stages of an animation
  • @font-face — register a custom font file the browser should download
  • @import — include another stylesheet; usually better done with <link>
  • @supports — apply rules only if the browser understands a given declaration

How the Parser Recovers From Your Mistakes

Knowing exactly how much a mistake destroys saves a lot of guessing. A bad declaration is thrown away on its own — the rest of the rule survives. So one misspelled property in a block of ten still leaves nine working, which is why a broken rule usually looks half-applied rather than completely dead.

A bad selector is far more expensive, because it invalidates the entire rule including its whole selector list. If you group five selectors with commas and one of them is misspelled or unsupported, the browser discards the block for all five. This is the reason a page can lose a whole section of styling because of one stray character, and it is the strongest argument for keeping selector lists short.

When two rules of equal weight set the same property on the same element, the one written later in the stylesheet wins. That is the plain-language version of "the cascade", and it explains a mistake almost everybody makes once: you write a fix at the top of the file, it does nothing, and the original rule further down is quietly overriding it. Weight is not only about order — the next lessons on selectors and specificity fill in the rest — but source order is the tiebreaker, and it is worth trusting.

Example
/* One bad declaration — the others still apply */
.card {
  colour: red;        /* dropped, no error */
  padding: 16px;      /* applied */
  border-radius: 8px; /* applied */
}

/* One bad selector — the WHOLE rule is discarded */
.card, .price, ::totally-not-real, .badge, .tag {
  font-family: Georgia, serif;   /* none of the five get this */
}

/* Equal weight: the later rule wins */
p { color: blue; }
p { color: green; }   /* paragraphs end up green */
Notes
  • Get into the habit of finishing a stylesheet edit by scrolling the Styles panel in developer tools. A struck-through declaration means a bad value; a rule that is missing entirely means a selector problem. Those are two different bugs and the panel tells them apart instantly.
Ask AI