Lecture 8 More Foundational Concepts of CSS

Topics for today

Don’t
Repeat
Yourself

DRY Principle

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system

The Pragmatic Programmer, Andy Hunt and Dave Thomas

A big part of writing good code is avoidingcodeknowledgeduplication


			def right_triangle_perimeter(a, b, c):
				return a + b + c

			# ... later on
			p = right_triangle_perimeter(3, 4, 5)
		
  • Here, you can see a contrived example of WET Python code. c is dependent on a and b, so providing it is superfluous.
  • Not only is this an example of poor efficiency (both to write and to read), but also poor safety, since we may inadvertently provide arguments that are inconsistent.

			from math import sqrt

			def right_triangle_perimeter(a, b):
				return a + b + sqrt(a**2 + b**2)

			# ... later on
			p = right_triangle_perimeter(3, 4)
		

Keeping CSS DRY

Avoid presentational class names

Presentational class


				.red-button {
					background: hsl(0, 80%, 90%);
				}
			

Semantic class


				.primary-button {
					background: hsl(0, 80%, 90%);
				}
			

Keeping CSS DRY

Use font-relative units


				button {
					border-radius: 5px;
					padding: 5px 12px 6px;
					font-size: 24px;
					line-height: 24px;
				}
				button.large { font-size: 46px; }
			

				button {
					border-radius: .2em;
					padding: .2em .5em .25em;
					font-size: 100%;
					line-height: 1;
				}
				button.large { font-size: 200%; }
			

Keeping CSS DRY

Use shorthands wisely

Hardcover
Paperback
Audiobook

				.tab {
					border-radius: .3em .3em 0 0;
					padding: .1em .5em .1em .5em;
					margin: 0 .1em 0 .1em;
				}
			
Hardcover
Paperback
Audiobook

				.tab {
					border-radius: .3em;
					border-bottom-left-radius: 0;
					border-bottom-right-radius: 0;
					padding: .1em .5em;
					margin: 0 .1em;
				}
			
Note that the DRY version has more code. Short code is not the goal, maintainability is.

Overgeneralization

Rule of three Three strikes and you refactor

Duplication is far cheaper than the wrong abstraction

Sandy Metz

Selectors

h1 { font-size: 300%; line-height: 1; }
  • The selector tells the browser which elements the rule is about, in this case h1 headings.
* { margin: 0; padding: 0; box-sizing: border-box; }
  • The universal selector allows us to apply certain declarations to every element in the page.
  • It can be useful for undoing certain user agent styles, but also really dangerous. E.g. * {font-weight: normal} might seem like a reasonable default, but it means our strong elements also will start from the same non-bold baseline.

Simple Selectors

Most Common simple selectors * element .class #id
Attribute selectors [attr] [attr="value"] [attr^="starts"] [attr$="ends"] [attr*="anywhere"] [attr~="anywhere"]
Pseudo-classes (sample) :hover :focus :checked :target :nth-child() :invalid
  • These are called simple selectors because they represent a single selection criterion.
  • Here, they are listed by frequency of use: the selectors that are less frequently used are more transparent.
  • More frequency of usage data for selectors

Compound selectors

Concatenating simple selectors intersects them

Concatenating simple selectors intersects them (i.e. it is equivalent to an AND operation). The result is called a Compound Selector E.g. a.docs matches an <a> element with the class docs. Note that element selectors can only be first in the sequence (i.e. [title]element is invalid)

input[type="text"]
=
input AND [type="text"]

Later in this lecture we will learn more ways to apply logical operators to CSS selector queries.

Element selectors need to be first

p.notice.tip[title]#tip42 .notice.tip[title]p#tip42
The rest can be in any order

Pseudo-classes

button:hover { background: greenyellow; }
  • Pseudo-classes allow us to style states of an element, like an imaginary class that is added and removed automatically.
  • Does it remind you of JS? Indeed, a lot of pseudo-classes are inspired from functionality that used to require JS. However, using pseudo-classes is a lot easier than writing JS because the browser keeps track of when to apply and unapply the state for you.

Pseudo-classes are simple selectors that allow us to style states of an element.

- Here we can see some example pseudo-classes in action: - `:enabled` and `:disabled` (what's the advantage over just using a `[disabled]` attribute selector?) - `:hover` matches when the pointer is over the element - `:active` matches when the pointer is pressed on the element - `:hover` and `:active` behave differently on mobile - `:focus` matches when the element is focused, iff it can receive focus - Note that by chaining simple selectors together we are intersecting them. We will learn more about this later today.
Here is a different style for this list. We want to eliminate the line on the last item. What can we do?

Pseudo-classes can be dynamic (activity-based) or structural (DOM tree-based)

Pseudo-classes

Dynamic

  • Mouse over element :hover
  • Mouse down on element :active
  • Currently focused :focus
  • Contains currently focused :focus-within
  • Checked radio or checkbox :checked
  • Elements targeted by the url #hash :target

Structural

  • first child :first-child
  • last child :last-child
  • only child :only-child
  • odd children :nth-child(odd)
  • every 4th paragraph after the 8th from the end p:nth-last-of-type(4n + 8)
  • Structural pseudo-classes match elements based on their position among their siblings in the tree. Here we explore the :*-child family of pseudo-classes.
  • :first-child and :last-child match elements who are first and last among their siblings, respectively.
  • :only-child is equivalent to :first-child:last-child
  • :nth-child() is a generalization. It accepts an argument of the form An+B (e.g. 2n+1) and matches elements whose position among their siblings matches the argument for some non-negative integer n. A and B can be negative.
    • :nth-child(even) is equivalent to :nth-child(2n) and matches each 2nd, 4th, 6th etc child.
    • :nth-child(odd) is equivalent to :nth-child(2n+1) and matches each 1st, 3rd, 5th etc child.
    • :nth-child(3n+1) matches every 3rd child starting from the first.
    • :nth-child(1) is equivalent to :first-child
    • Activity: Select the first 4 items. Now select items 2-5!
  • :nth-last-child() is exactly the same as :nth-child() but starts counting from the end.
    • :nth-last-child(1) is equivalent to :last-child
    • Activity: Select the first item, only when the total count of its siblings is ≥3
  • There is also a :*-of-type family of pseudo-classes, with exactly the same syntax, that only counts elements of the same type. You can experiment with it here

Combinators

We have now added a nested ordered list, and our rule seems to be applying to that too. What can we do?

Combinators allow us to match on relationships between elements

:checked + label

Next sibling combinator

label directly after a or

label that contains a or

label directly after or

h1 ~ h2

Subsequent Sibling combinator


					<h1>…</h1>
					<h2>…</h2>
				

					<h1>…</h1>
					<p>
					<h2>…</h2>
				

					<h1>…</h1>
					<section>
						<h2>…</h2>
					</section>
				

					<h2>…</h2>
					<h1>…</h1>
				

Compound selectors + combinators = complex selectors

Why can’t any combinators look backwards?

  • “style paragraphs that contain images”
  • “style h1s that come before h2s”
🤔🤔🤔
This will change soon, see [`:has()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has)

Logical operations

CSS selectors express sets of HTML elements

How to perform set operations? (¬, ∪, ∩)

Logical operations we've seen

  • AND: p.warning = p.warning
  • OR: td, th = tdth
  • NOT:
    a { text-decoration: none }
    a:hover { text-decoration: underline }
We've already seen some ways to perform logical operations. And for many years in CSS' history, these were the only ways. But they are quite limited. How do we express AND or OR between complex selectors? And how do we negate without having to explicitly set both states?

Negation pseudo-class: :not()

With overrides


					a {
						text-decoration: none;
					}

					a:hover {
						text-decoration: underline;
					}
				

With negation


					a:not(:hover) {
						text-decoration: none;
					}
				
The negation pseudo-class allows us to express our intent more clearly. It also means we can negate without having to set both states.

AND/OR between complex selectors:
The :is() pseudo-class


				:is(h1, h2, h3, h4, h5, h6):is(section *, article *) {
					border-bottom: .1em solid slategray;
				}
			

Pseudo-elements

Pseudo-elements allow us to style parts of an element

- Similar to how certain pseudo-classes represent additional state information not directly present in the document tree, a pseudo-element represents an element not directly present in the document tree. They are used to create abstractions about the document tree beyond those provided by the markup. - [Specification on pseudo-elements](https://www.w3.org/TR/selectors/#pseudo-elements)

Examples

List markers


					li::marker {
						color: red;
					}
					­
				

Input placeholders


					input::placeholder {
						color: skyblue;
					}
					­
				

Text selection


					::selection {
						background: rebeccapurple;
						color: white;
					}
				
Note that some pseudo-elements can be very limited in what properties they support. E.g. try applying `filter` on `::marker` or `::selection`.

Cascading & Inheritance

CSS

The Cascade is an algorithm that defines how to combine property values originating from different sources and how to resolve conflicts.

Components of the Cascade

Importance


			body {
				background: yellow;
			}
		

			body {
				background: pink !important;
			}
		

Origin


			button {
				border: none;
			}
		

			button {
				border: 2px outset #777;
			}
		
- The second tier of the Cascade looks at where the rule was defined. We have already overridden many User Agent styles and didn't think much of it. In this case, the cascade follows expectations: author stylesheets (i.e. from websites) generally have higher priority than User Agent (UA) stylesheets (i.e. from the browser). - This is by design: UA styles are merely a fallback for pages that have not defined any CSS. - There is also a third origin: User, for stylesheets defined by the website *visitor*, i.e. the browser user, which sits right between them. Originally, CSS was envisioned as a way for web users to also customize the websites they visit, and browsers used to provide this functionality out of the box. Unfortunately, it did not pan out, and browsers removed the UI for this functionality. It can still be accessed through extensions (e.g. [Stylus](https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=en))

Origin


			button {
				border: none;
			}
		

			button {
				border: 2px outset #777 !important;
			}
		
Fun fact: The hierarchy here is actually reversed for `!important` rules, meaning that an `!important` browser default rule wins over an !important website rule, i.e. you can never override it no matter what you try.

Any author styles
on a given element
override any UA styles

(Unless !important is involved)

Inheritance

Inheritance
  • Notice how the li, strong, em, mark elements have the same font, font size, color as the entire list, but not the same background or padding. Why?

  • Notice how even though there is an inherited font-weight, the strong element doesn't inherit it.
  • We can apply a different color on a list item and it takes precedence over the inherited one

  • Use the value inherit to inherit from a non-inherited property.
  • However, then they are considered explicitly specified, and have the same priority as a directly applied value
  • Want the opposite? You can reset an inherited property with initial.

Some properties are inherited, and some aren’t. Why? It's a heuristic.

What kinds of properties inherit?

background font border text-decoration display color padding white-space margin text-shadow box-shadow box-sizing outline --*

Inherited Not inherited

Inherited values have lower priority than directly applied values

  • What happens when there are conflicting declarations about the same element?
  • Here it’s quite obvious, since the selector is the same, so last one wins.
- But what about here? - Surprised at the result? The reason is that `#container *` has higher *specificity* than `div.box`.

Selector specificity is a heuristic for inferring importance from querying logic

Calculating specificity

  • CSS resolves conflicts via selector specificity, not source order.
  • Specificity is calculated as follows:
    1. Count the number of ids in the selector (= A)
    2. Count the number of classes, attribute selectors, pseudo-classes (except :not()) (= B)
    3. Count the number of tag selectors (= C)
    4. Specificities are compared by comparing the three components in order: the specificity with a larger A value is bigger; if the two A values are tied, then the specificity with a larger B value is bigger; if the two B values are also tied, then the specificity with a larger C value is bigger; if all the values are tied, the two specificities are equal.
  • If specificity is the same, then the conflict is resolved based on source order.

Specificity: Special cases

Selector Specificity
style="background: red" (∞, ∞, ∞)
:not(em, strong#foo) (1, 0, 1)
:is(em, #foo) (1, 0, 0)
  • Declarations within the style attribute have infinite specificity
  • :not() does not contribute to specificity as a pseudo-class, but based on the specificity of its most specific argument
  • :is() does not contribute to specificity as a pseudo-class, but based on the specificity of its matching argument

🤺 Specificity battle! 🤺

>

Selector specificity is a heuristic for inferring importance from querying logic
So what do we do when it’s wrong?

Adjusting specificity

Increasing specificity

  • Repetition: .foo.foo.foo
  • Negation: :not(#nonexistent)

Decreasing specificity

  • #foo to [id="foo"]
  • :where()

CSS Variables / Custom properties

  • CSS variables (aka Custom Properties) start with -- and behave like normal CSS properties.
  • We read their value via the var() function and we can use that anywhere (including inside calc()), except inside url().
  • They are dynamic, and can be set in the inline style or even via JavaScript!
  • They help reduce repetition, and thus make code more maintainable. They also help encapsulation: Other people can use your components and style them via the variables you have exposed, without having to know and override the internal structure of your CSS.
  • Notice the repetition here: We have to repeat the button color 3 times! And changing it requires repeating so much code. How can CSS variables help us reduce this repetition and create themes more easily?