Introduction
Master the fundamentals and advanced mechanisms of cascading style sheets to separate document content from presentation and build robust web interfaces.
You face a web design environment where mixing HTML content with presentation code creates fragile, unmaintainable pages. Cascading style sheets solve this by decoupling structure from visual design through modular specifications evolved from early proposals into modern styling engines.
Eric A. Meyer guides you through rules, selectors, and stylesheets to control document appearance predictably. You learn how browsers resolve conflicting declarations and manage typography, values, and units.
The book answers these core questions:
- How do stylesheets target specific document elements?
- What mechanisms resolve conflicting style declarations?
- How do relative units and typography properties affect layout?
1. Selectors and Document Display
HTML elements divide into replaced elements, whose content comes from external sources, and nonreplaced elements, whose content is generated within a browser box. Replaced elements like img or input have their content substituted by external files or form controls. Nonreplaced elements like spans and paragraphs display their text directly inside a box generated by the element itself.
Elements possess display roles that generate block or inline formatting contexts, which can be altered using the display property. Block-level elements generate an element box filling the parent area with breaks before and after, while inline-level elements flow within a line of text. The display property allows you to override default element display types, which is especially useful for styling XML documents.
Setting paragraphs to inline and emphasis tags to block via p { display: inline; } and em { display: block; }.
Stylesheets target document elements using a variety of selectors and display formatting rules.
Type selectors apply styles globally to all elements of a specified node type within a document. A type selector targets HTML or XML element names directly, such as h1, p, or quote, allowing you to globally shift styles across the document. You can change styles from h1 to p by updating the selector from h1 { color: gray; } to p { color: gray; }.
Grouping selectors and declarations compacts stylesheets and prevents redundant code. Selectors can be grouped using commas on the left side of a rule, and declarations can be grouped using semicolons on the right side. Combining both allows complex assignments with minimal code.
h2, p { color: gray; } applies gray color to both h2 and paragraph elements simultaneously.
- Place multiple selectors on the left separated by commas.
- Group multiple declarations on the right separated by semicolons and terminated with a final semicolon.
Class selectors enable style application independent of element type by referencing class attribute values. Class names are preceded by a period (.) in CSS and target elements carrying a corresponding class attribute value. They can be generic or element-specific.
Using p.warning to select only paragraph elements with the warning class, leaving span elements unaffected.
- Modify document markup to include class attributes, such as class="warning".
- Prefix the class name with a period in the CSS stylesheet to target the elements.
ID selectors target unique document elements using an octothorpe prefix. ID selectors are preceded by a hash or octothorpe (#) and reference the unique id attribute of an element. While HTML specs mandate unique IDs per document, CSS allows matching multiple elements if duplicate IDs exist.
#lead-para { font-weight: bold; } makes the element with id="lead-para" bold.
Attribute selectors target elements based on the presence, exact value, or partial substrings of their attributes. CSS2 introduced attribute selectors to style elements using markup attributes. Types include simple presence, exact value matching, and various substring matchers like tilde, asterisk, caret, and dollar sign.
a[href$='.pdf'] styles links pointing to PDF documents by matching the end of the href attribute.
- Use [att] for simple attribute presence.
- Use [att="val"] for exact value matching.
- Use tilde, asterisk, caret, or dollar sign operators for substring and list-item matching.
Appending an 'i' inside an attribute selector forces case-insensitive matching regardless of document language rules. Adding the character i before the closing bracket of an attribute selector allows it to match attribute values regardless of their capitalization.
- Insert the letter i before the closing bracket of an attribute selector to ignore case.
2. Combinators and Pseudo-Classes
Advanced combinators and pseudo-classes enable precise targeting based on structural relationships and element states.
You can connect simple selectors using combinators to target elements based on how they relate to one another in the document tree.
Combinators
Descendant selectors target elements that reside anywhere inside the hierarchy of an ancestor element. You separate selectors with a space to match elements found within another element. Browsers read these patterns from right to left without regard to element proximity.
- Write two or more space-separated selectors from right to left to target nested elements.
Eric Meyer gives the example of writing h1 em { color: gray; } to style emphasis elements specifically inside headings.
The child combinator restricts style application strictly to direct children rather than arbitrary descendants. You place a greater-than symbol between selectors to ensure the rule matches only elements that are direct children of the specified parent.
- Place a greater-than symbol (>) between selectors to target direct child elements.
Writing h1 > strong { color: red; } makes strong red only if it is a direct child of h1. Sibling combinators select elements that share the same parent and follow each other in source order.
- Use a plus symbol (+) between selectors to target the immediately adjacent sibling.
- Use a tilde symbol (~) between selectors to target any following sibling sharing a parent.
The adjacent-sibling combinator targets an element immediately following a sibling. The general sibling selector targets any following sibling sharing the same parent. Writing h1 + p { margin-top: 0; } removes the top margin from paragraphs that immediately follow an h1.
Pseudo-Classes and Elements
Pseudo-classes act as phantom classes inferred by document structure, element state, or user interaction. They attach directly to the target element rather than its descendants.
When Eric Meyer's first child was born in 2003, people sent CSS jokes with the selector #ericmeyer:first-child. That rule would select Eric himself rather than his daughter.
The :empty pseudo-class selects elements that have no children of any kind, including text nodes and whitespace. It is unique as the only CSS selector taking text nodes into consideration. Comments do not prevent an element from being considered empty, but single spaces or newline characters do.
The :nth-child() pseudo-class allows selecting arbitrarily numbered child elements using integers or algebraic expressions in the form an + b. The variable n represents the series 0, 1, 2, 3, 4 into infinity, allowing the browser to solve for recurring patterns. Negative multipliers for a can count backward from a limit set by b.
- Use :nth-child(2n) to select even-numbered children.
- Use :nth-child(2n-1) or :nth-child(2n–1) to select odd-numbered children.
- Remove the minus sign when using a negative number for b in an expression.
For security and privacy reasons, styles applied to visited links are strictly limited to color-related properties. DOM queries return unvisited values for these elements. Browsers restrict :visited styling to prevent malicious scripts from extracting user browsing history through visual styles or background images.
The :target pseudo-class enables unique styling of document elements whose ID matches the fragment identifier in the current URL. It applies styles when a page loads with a matching hash fragment or when the hash updates.
The :focus-visible pseudo-class applies focus styles only when the user agent determines the element should receive visible focus. This occurs during keyboard navigation, preventing unwanted focus rings when interactive elements are clicked with a mouse.
The :has() pseudo-class acts as a relational selector, allowing you to style an element based on its descendants, siblings, or structural relationships. Inside :has(), you can provide a comma-separated list of relative simple or complex selectors. While extremely powerful, complex queries can cause performance hits if document structures change dynamically.
Pseudo-elements use a double-colon syntax to insert and style fictional elements, separating them from pseudo-classes. Browsers accept legacy single-colons for backward compatibility, but the double-colon is standard. Pseudo-elements like ::first-letter and ::first-line construct virtual nodes on the fly.
3. Specificity, Inheritance, and the Cascade
Conflicts between competing declarations are resolved through an eight-level cascade sorting algorithm driven by specificity and explicit weights.
Specificity and Inheritance
A selector's specificity is evaluated using a three-part value system based on the types of components it contains. Browsers calculate specificity by counting components from left to right. ID attribute values add 1,0,0. Class attribute values, attribute selections, and pseudo-classes add 0,1,0. Elements and pseudo-elements add 0,0,1. Combinators, universal selectors, and :where() pseudo-classes add 0,0,0. The specificity of :is(), :not(), or :has() equals the most specific selector in its argument list.
User agents treat grouped rules and grouped selectors as ungrouped individual declarations when conferring specificities. When a rule contains multiple properties or grouped selectors, the user agent splits them into separate single declarations, conferring the exact same calculated specificity value to each individual declaration before resolving conflicts.
For example, the selector html>body table tr[id="totals"] td ul>li results in a specificity of 0,1,7, while li #answer results in 1,0,1, making the second selector win because 1,0,1 beats 0,1,7.
To calculate specificity correctly, you follow specific counting procedures:
- Add 1,0,0 for every ID attribute value given in the selector
- Add 0,1,0 for every class attribute value, attribute selection, or pseudo-class given in the selector
- Add 0,0,1 for every element and pseudo-element given in the selector
- Assign 0,0,0 for combinators, universal selectors, and :where() pseudo-classes
- Set the specificity of :is(), :not(), or :has() equal to the most specific selector in its selector list argument
Adding the !important flag to a declaration groups it separately from normal declarations, allowing it to bypass standard specificity rules. Declarations marked with !important do not receive a special specificity value. Instead, all important declarations are grouped together and resolved relatively among themselves, always winning over non-important declarations regardless of specificity.
When you write declarations, you must manage importance correctly:
- Insert the !important flag just before the terminating semicolon in a declaration
- Provide a separate !important flag for each individual declaration you wish to mark as important
Inheritance propagates property values down the structural tree from parent elements to descendant elements, but box-model properties do not inherit. Values propagate downward through descendants but never upward to ancestors, with rare exceptions like background styles passing from body to html. Most box-model properties, including margins, padding, backgrounds, and borders, are not inherited to avoid layout clutter.
Inherited values possess no specificity at all, meaning universal selectors with zero specificity can defeat them. Because inherited values have no specificity weight, a universal selector with 0,0,0 specificity will override inherited black text and render elements gray, demonstrating why indiscriminate use of the universal selector can short-circuit inheritance.
The Cascade
The browser resolves conflicting declarations by running an eight-level sorting process known as the cascade. The cascade sorts declarations successively by explicit weight, origin, encapsulation context, element attachment, cascade layer, specificity, and order of appearance. Transition declarations hold the highest precedence, while user agent declarations hold the lowest.
To trace how the browser resolves styles, you follow the multi-step cascade mechanism:
- Find all rules containing a selector matching a given element
- Sort all declarations applying to the element by explicit weight (!important)
- Sort declarations by origin (author, reader, user agent)
- Sort declarations by encapsulation context (shadow DOM)
- Sort declarations by element-attachment (style attribute)
- Sort declarations by cascade layer
- Sort declarations by specificity
- Sort declarations by order of appearance in the style sheet
Cascade layers let authors group styles to share a precedence level, with later-declared layers overriding earlier ones for normal styles. Cascade layers allow balancing component libraries against page styles. For normal-weight rules, later-declared layers take precedence. For important-weight rules, the order is reversed, and earlier layers or named layers behave differently depending on whether they are wrapped in explicit directives.
When you structure modular style sheets, you utilize layer tools:
- Define named layers using the @layer directive and specify their order
- Assign imported stylesheets to layers using layer() syntax
Link pseudo-class styles must be ordered correctly using the LVFHA rule to prevent specificity and order conflicts from overriding interactive states. Because link pseudo-classes share identical specificity (0,1,1) and explicit weight, order of appearance dictates the winner. Ordering them as link, visited, focus, hover, active ensures that activation and hover states are not incorrectly masked by preceding rules.
To ensure your interactive states render properly, you organize your rules:
- Write link styles in the order link, visited, focus, hover, active (LVFHA)
4. Values, Units, and Text Properties
Styling fine-grain typography requires mastering absolute and relative length units, mathematical functions, custom properties, and text layout properties.
Values and Units
CSS3 defines four global keywords—inherit, initial, unset, and revert—that can be accepted by every property in the specification. The inherit keyword forces inheritance from a parent element, initial sets a property to its defined initial value, unset acts as inherit for inherited properties and initial for non-inherited ones, and revert resets a property value as if author styles do not exist.
The 'all' property is a universal stand-in for all properties except direction and unicode-bidi, accepting only global keywords. Declaring 'all: inherit' forces every single property on the element to inherit from its parent, bypassing normal non-inherited properties like borders or margins.
CSS lengths are divided into absolute units tied to physical measurements and relative units tied to fonts, roots, or viewports. Absolute units include inches, centimeters, millimeters, quarter-millimeters, points, picas, and pixels defined to yield 96 pixels per inch. Relative units scale based on context, such as em relative to font size, rem relative to the root element, and vw or vh relative to the viewport.
The calc() function enables basic mathematical expressions combining compatible units using rules derived from operand types. Addition and subtraction require compatible unit types or numbers on both sides with mandatory surrounding whitespace. Multiplication and division require at least one number operand, and division by zero is strictly invalid.
Functions like min(), max(), and clamp() allow setting dynamic upper bounds, lower bounds, or preferred target values for properties. min() picks the smallest value among alternatives to set a maximum limit, max() picks the largest value to set a minimum limit, and clamp() combines a minimum, a preferred ideal value, and a maximum allowed value.
Colors can be specified using named keywords, RGB, hexadecimal codes, HSL, HWB, Lab, LCH, and color spaces via the color() function. In addition to standard sRGB functional and hex notations with alpha channels, modern CSS supports device-independent color spaces like CIE Lab and LCH, alongside color() to target wide-gamut displays.
Custom properties allow defining author-specific variables prefixed with double hyphens that can be retrieved via the var() function. Custom properties support contextual scoping where redefining a variable inside an element like an aside automatically updates all dependent descendents. Fallbacks can be provided inside the var() syntax.
To apply custom properties in your stylesheets, you follow these steps:
- Define custom properties on a selector using double hyphens as prefixes like --base-color.
- Retrieve the value later using the var() function.
- Specify an optional fallback value separated by a comma inside the var() function.
Text and Spacing
The text-indent property indents the first line of block-level elements, with percentages calculated relative to the containing block's width. It applies along the inline direction and supports negative values for hanging indents. Experimental keywords include hanging and each-line. Inheritance of text-indent varies across browsers, with some inheriting computed values and others inheriting declared values.
div { width: 400px; } p { text-indent: 10%; } indents the first line of the paragraph by 40px.
To handle hanging indents safely, you use your styles:
- Use text-indent with a negative value and matching padding-left to create a safe hanging indent.
The text-align property controls horizontal alignment of inline content in block elements, while text-align-last styles the final line before a break. Values include start, end, left, right, center, justify, justify-all, and match-parent. text-align-last aligns lines preceding forced breaks or element closures, and takes precedence over text-align if the first line is also the last.
.justify-all { text-align: justify; text-align-last: justify; } solves the lack of browser support for justify-all.
To simulate full justification across different user agents, you apply your styles:
- Apply both text-align: justify and text-align-last: justify to simulate full justification across supported browsers.
Word and letter spacing properties modify inter-word and inter-character gaps using lengths, but inherited computed values can cause scaling issues. word-spacing adds length to standard spaces between non-whitespace strings, while letter-spacing modifies character gaps. Because computed values are inherited rather than scaling factors, child elements with smaller font sizes retain the parent's absolute spacing length.
p { letter-spacing: 0.25em; font-size: 20px; } small { font-size: 50%; } makes tiny text inherit wide letter spacing.
To keep your typography proportional, you adjust your rules:
- Explicitly set letter-spacing on child elements when font sizes change to maintain proportional spacing.
The line-height property controls leading by defining the minimum distance between baselines, centering content areas within inline boxes. Leading is the difference between line-height and font-size, divided equally and applied above and below the content area. Unitless numbers act as scaling factors that inherit properly, avoiding the overlap issues caused by inheriting absolute length values.
body { font-size: 10px; } div { line-height: 1em; } p { font-size: 18px; } causes overlapping text due to inherited 10px height.
To prevent baseline overlap in nested blocks, you configure your layouts:
- Specify unitless numbers for line-height on parent elements to ensure child elements calculate proportional line heights based on their own font sizes.
The vertical-align property shifts inline elements and table cells along the block direction relative to their parent's baseline or line box. Keywords include baseline, sub, super, top, bottom, text-top, text-bottom, and middle. It accepts lengths and percentages relative to the element's line-height. Rereplaced elements without baselines align their bottom edges to the parent baseline.
sub { vertical-align: -100%; } sup { vertical-align: 100%; } shifts text into adjacent lines.
To adjust script elements properly, you modify your rules:
- Use font-size alongside super or sub keywords if you need to scale down superscript or subscript text.
Text decorations can be customized using separate properties for line placement, color, thickness, and style, or combined via the text-decoration shorthand. Properties include text-decoration-line, text-decoration-color, text-decoration-thickness, text-decoration-style, and text-underline-offset. Shorthand values replace each other entirely rather than accumulating. Underlines can skip ink intersecting glyphs using text-decoration-skip-ink.
h2 { text-decoration: overline purple 10%; } combines placement, color, and thickness in a single shorthand declaration.
To override inherited decorations cleanly, you define your styles:
- Declare individual text-decoration properties on child elements if you need different colors or styles that override a parent's decoration.
Text shadows add colored, offset, and blurred drop effects, while text-emphasis properties apply repeating marks to individual glyphs. text-shadow takes horizontal offset, vertical offset, an optional blur radius, and color. text-emphasis-style applies shapes like dot, circle, triangle, or custom strings above or beside characters, affecting line height unlike decorations.
text-shadow: green 5px 0.5em; creates a solid green shadow offset to the right and down.
To maintain smooth rendering on screens, you check your effects:
- Test text shadows thoroughly on low-power mobile devices to avoid performance slowdowns from heavy blur values.
The white-space property controls how browsers handle spaces, newlines, and tabs, while tab-size defines the space equivalent of tab characters. Values include normal, nowrap, pre, pre-wrap, pre-line, and break-spaces. Preserving whitespace honors line feeds and spaces, whereas normal collapses them. tab-size defaults to 8 spaces but accepts integers and lengths.
p { white-space: pre-wrap; } preserves whitespace sequences while allowing normal line wrapping.
To manage raw spacing in text blocks, you update your definitions:
- Use white-space: pre-wrap when you need to preserve source indentation and line breaks without disabling text wrapping.
Properties like hyphens, word-break, line-break, and overflow-wrap manage soft wrapping, word splitting, and container overflow behavior. hyphens: auto permits automatic hyphenation based on language rules. word-break controls wrapping between characters or words, while overflow-wrap inserts breaks within unbreakable strings to prevent box overflow.
.cl01 { hyphens: auto; } enables automatic browser hyphenation on long words.
To protect specific strings from breaking awkwardly, you adjust your code:
- Suppress automatic hyphenation in code samples, variables, and keyboard inputs by setting hyphens to manual on those elements.
Writing-mode and text-orientation properties adapt layouts for non-Western vertical and bidirectional writing directions. writing-mode sets the block flow direction using values like horizontal-tb, vertical-rl, and vertical-lr. text-orientation and text-combine-upright control character rotation and compact horizontal embedding within vertical text lines.
.verts { writing-mode: vertical-lr; } sets a vertical block flow direction.
To guarantee robust bidirectional rendering when styles fail, you write your markup:
- Use HTML dir attributes and bdo elements rather than CSS direction and unicode-bidi properties to ensure correct bidirectional layout when CSS is disabled.
Conclusion
Selectors, combinators, specificity, inheritance, and text properties unite to form a predictable styling system that separates document content from visual presentation. When you understand how the cascade resolves conflicting declarations through weight, origin, specificity, and source order, you stop guessing why a rule fails to apply.
You must follow these practical rules for daily CSS authoring:
- Write clean markup with semantic elements so your selectors remain simple and robust.
- Rely on inheritance for typography and color properties to keep your stylesheets concise.
- Keep selector specificity low by avoiding unnecessary class chains and IDs to ensure effortless overrides later.
- Use custom properties for repeated values to maintain consistency and simplify future updates across large stylesheets.
- Test your display and text formatting rules across multiple viewports to verify that relative units and mathematical functions scale predictably.
10 Key Ideas
- Stylesheets target specific document nodes globally using various selectors and display formatting rules.
- Advanced combinators and pseudo-classes enable precise element targeting based on structural relationships and element states.
- Competing declarations are resolved through an eight-level cascade sorting algorithm driven by specificity and explicit weights.
- Inheritance propagates property values down the structural tree from parent elements to descendant elements without affecting non-inheriting box-model properties.
- Four global keywords—inherit, initial, unset, and revert—can be accepted by every property in the CSS specification.
- Custom properties allow defining author-specific variables prefixed with double hyphens that can be retrieved via the var() function.
- Explicitly setting letter-spacing on child elements when font sizes change maintains proportional typography.
- Specifying unitless numbers for line-height on parent elements ensures child elements calculate proportional line heights based on their own font sizes.
- Link pseudo-class styles must be ordered correctly using the LVFHA rule to prevent specificity and order conflicts from overriding interactive states.
- Using HTML dir attributes and bdo elements rather than CSS direction and unicode-bidi properties guarantees robust bidirectional rendering when styles fail.