back to module

Smashing Workshops

Order of Operations

Arrows navigate, cmd/ctrl-k for settings

One value For every property
on every element

When browsers apply styling to a website, they have to determine a single value for every CSS property on every DOM element in the page.

Every element has…

  • accent-color
  • align-content
  • align-items
  • … +643 more …
  • y
  • z-index
  • zoom
CSSWG, Index of CSS properties

Every individual button, every span, and every paragraph has an accent-color and align-content, and a z-index, and a zoom, and so on.

Even if we don’t write a line of CSS, every element has all these styling properties attached.

Browser dev tools showing the computed styles tab with a list of CSS properties. The first one, accent-color, shows an rgb color value. From there the list is slightly transparent - align-content normal, align-items normal, align-self auto, and so on.

We can see them all in the browser dev tools under computed styles. Every browser has an option to show all the CSS properties –

The same dev tools tab but with a 'show all' checkbox un-checked. Now the list jumps straight from accent-color to box-sizing border-box, color, color-scheme, and so on - all of them at full opacity.

To filter just the properties that have been declared explicitly. But that doesn’t make them less real!

Every element has ~650 Style Properties

CSSWG, Index of CSS properties

There’s something like 650 properties – and every one of those properties must have a single value to render the page.

Span with no styles: present, debug

If we inspect a <span> element on an un-styled page, we’ll see that there are no CSS rules applied – but we can still read the full list of properties, and see the value used for each one.

World Wide Web
World Wide Web

We can also load the first website, which was published several years before CSS was even proposed.

The browser loads an HTML page, and creates a DOM tree of the elements. And each element comes with all these properties, already defined before any CSS is applied. And the browser can render that page, without any additional instructions!

(Browser styles are being applied, but there are no document styles)

css
* { box-sizing: border-box; }
button { background-color: orange; }
article {
  background-color: teal;
  background-color: deepPink;
  background-color: OddBird;
}

When we do write CSS and attach it to the document, or when the browser adds element defaults, or the user changes their preferences – we now have multiple (potentially conflicting and sometimes invalid) values on some of the properties.

  1. Filtering
  2. Cascading (includes specificity)
  3. Defaulting (includes inheritance)
  4. Resolving (includes computed values)
CSSWG, Value Processing Spec

So before rendering, the browser has to get us back where we started: filtering down to what’s relevant and valid, cascading to resolve conflicts, defaulting whatever properties are leftover, and then resolving all the values in relation to each other.

Filtering to find Relevant Declarations…

CSSWG, Value Filtering Spec

Filtering has several parts, and they’re mostly straightforward. The browser is starting from an element in the DOM, and going through each property to find relevant CSS declarations.

To be relevant, a declaration has to be–

In a stylesheet That Applies to the Document

In a stylesheet that applies to the document.

Right? That seems obvious.

html
<link rel="stylesheet" href="print.css" media="print">

That’s mostly just looking at the media attribute.

If a stylesheet is only for print media, and we’re looking at a screen, we can ignore that stylesheet –

Not in a False Conditional Rule

But we also have to look at conditional rules inside the stylesheet. Not in a false condition.

diff-css
/* conditional rules must be 'true' */
  body {
+   @media (width >= 0) {
+     background: green;
+   }

-   @media (width < 0) {
-     background: red;
-   }
  }

To avoid the double-negative here: if a declaration is inside a condition (like at-media or at-supports) then the condition has to be true, or we can ignore everything inside.

In a Selector That Matches the Element

Similarly, we can ignore anything in a selector that doesn’t match the element.

diff-css
/* selectors need to match! */
+ * { box-sizing: border-box; }
- button { background-color: orange; }
+ article {
+   background-color: teal;
+   background-color: deepPink;
+   background-color: OddBird;
+ }
Filtering to get 0+ declared values

So if we’re looking for styles relevant to an article element, we can ignore everything in the button selector. Fair enough!

Styles only apply to the elements we select –

diff-css
/* conditional rules must be 'true' */
  body {
+   @media (width >= 0) {
+     background: green;
+   }

-   @media (width < 0) {
-     background: red;
-   }
  }

and under the conditions we specified!

I think that’s what we would expect?

Valid Syntax

And finally, the syntax must be valid.

css
example {
  color: blue; /* <color> */
  opacity: 1; /* <number> or <percentage> */
  aspect-ratio: auto; /* auto or <ratio> */
}

CSS is a strongly typed language. Every value has a data type. There are colors, numbers, lengths, percentages, strings, ratios, keywords, and so on.

It’s a long list of types, and many of them are design specific – like colors and lengths. CSS is a design language! Others – like strings and numbers – might be familiar if you write Javascript or a more general purpose language.

opacity syntax… <number> | <percentage>

And every CSS property has a syntax that only allows specific value types in specific combinations.

Opacity must be either a bare number (without any units) or a percentage.

diff-css
/* 1em is a <length> (invalid here!) */
 example {
-  opacity: 1em;
 }

If we provide a value that doesn’t match the expected syntax for the property, then the entire declaration will be ignored.

css
article {
  display: grid;
  display: grid-lanes;

  color: teal;
  color: light-dark(teal, powderblue);
}

This can be really useful! When a new feature comes along like grid-lanes or the light-dark() function, we can simply apply both –

diff-css
/* new syntax filtered out by older browsers */
  article {
    display: grid;
-   display: grid-lanes;

    color: teal;
-   color: light-dark(teal, powderblue);
  }

And let old browsers filter out the new syntax. It’s a great way to handle simple progressive enhancements. But it doesn’t work for everything.

css
.iacvt {
  color: var(--my-variable);
}
Variables can be used (almost) anywhere

We can apply a variable to the color property, and that’s allowed, right? I mean, at this point we don’t know if it will work – we don’t know if my variable has a value, or if that value is a color – but this syntax sure seems valid.

css
.iacvt {
  --my-variable: 2em;
}
Variables can contain (almost) anything

And it’s also valid syntax to put anything we want inside a variable. It could contain a length, and that’s still fine.

css
.option-1 {
  --my-variable: 2em; /* length */
}
.option-2 {
  --my-variable: hotPink; /* color */
}
Variables can be set multiple times

And maybe this variable sometimes has a color, but sometimes it has a length. During the filtering step, we’re not sure which of those will apply to the element!

css
.option-1 {
  --my-variable: 2em; /* length */
}
.option-2 {
  --my-variable: hotPink; /* color */
}
.iacvt {
  color: var(--my-variable); /* 🐈 */
}
Schrödinger’s variable!

We’re not ready to start resolving all the variables yet, we’re just looking at the syntax – so we have to trust for now that the variable could be valid, and leave it alone.

css
.iacvt {
  --my-variable: 2em;
  color: var(--my-variable);
}

You might look at this and know it’s going to fail, but the browser knows we’re only getting started. Someone could still come along and override that variable with something better.

  1. Filtering (“parse time”)
  2. Cascading
  3. Defaulting
  4. Resolving (“computed value time”)
CSSWG, Value Processing Spec

So this syntax is entirely valid! And it will not get removed as part of the filtering process.

Because variables are just properties, and go through this process like any other property, browsers won’t know if they are valid where used until we get all the way through filtering, cascading, defaulting, and resolving.

Declarations containing The var() function

Are not evaluated at parse time

So declarations that rely on a variable are not evaluated during the filtering process. We just assume they’re valid!

Might become… Invalid At Computed Value Time

Sometimes called “IACVT” in Github issues

Even though they might become invalid later, at computed value time. People will call this invalid at computed value time behavior.

Invalid At Computed Value Time: present, debug

And it changes how we have to handle fallbacks.

Declarations containing The var() function
or if() function
or any --custom() function

Are not evaluated at parse time

This isn’t just about variables any more. It applies to anything dynamic in CSS – like the new if() function, or custom functions.

(It’s ok if you haven’t used any of that, neither one is supported across browsers yet.)

diff-css
/* filter out the invalid color */
  article {
    background-color: teal;
    background-color: deepPink;
-   background-color: OddBird;
  }
Filtering to get 0+ declared values

Going back to our earlier example, we can filter out the invalid OddBird background color – but that still leaves two remaining background colors, teal and deepPink.

Both are valid colors in a selector that matches our element. Both are in an applied stylesheet, and neither one is inside a false condition.

diff-css
/* browser and user styles are merged as well */
  * { box-sizing: border-box; }
  article {
    background-color: teal;
    background-color: deepPink;
+   display: block;
  }

Any browser and user styles also get added to the list. In addition to our background colors, the browser has also provided display: block on articles.

Filtering is done, and we’re left with four “declared values” of the article element, describing only three properties.

We still have a conflict on the background-color that needs to be resolved.

  1. Filtering
  2. Cascading (includes specificity)
  3. Defaulting (includes inheritance)
  4. Resolving (includes computed values)
CSSWG, Value Processing Spec

Since there’s still a conflict after filtering is complete, those two values are put through the cascade to determine a winner.

We’ll talk about the cascade in depth tomorrow, but the goal is to narrow it down even farther, and get a single winner.

The Cascade (as a funnel): present, debug

The cascade is the “merge” step of the process, where things like layers, importance, specificity, and source order are used to resolve conflicts.

That might be conflicts between us and the browser defaults, but it might also just be conflicts within our code.

diff-css
/* the cascade ensures a single winner */
 article {
-  background-color: teal;
+  background-color: deepPink;
 }
Cascading to get 0 or 1 cascaded value

In this case the source-order is all that matters, and the second style wins.

css
* { box-sizing: border-box; }
button { background-color: orange; }
article {
  background-color: teal;
  background-color: deepPink;
  background-color: OddBird;
}
How it started…

So we’ve gone from the styles we wrote – four different background colors and a box-sizing value, on three different selectors –

css
* { box-sizing: border-box; }
article {
  background-color: deepPink;
  display: block;
}
How it’s going…

To a single background color, our box-sizing, and an additional display value that was provided by the browser.

css
article {
  box-sizing: border-box;
  background-color: deepPink;
  display: block;

  /* color? padding? margin-inline-end? */
}
Defaulting to get exactly 1 specified value

That’s only three properties with “cascaded” values, out of 650 total properties. What about the color, and the padding, and the margin-inline-end?

  1. Filtering
  2. Cascading (includes specificity)
  3. Defaulting (includes inheritance)
  4. Resolving (includes computed values)
CSSWG, Value Processing Spec

And this is where defaulting steps in.

Some properties will inherit a value from the parent element – ~165 inherit by default – and all the rest will reset to their initial value.

Defaulting is only needed on properties where nothing valid was explicitly specified – where nothing made it through the cascade.

Sometimes the cascade isn’t involved: present, debug

I think often we know this intuitively, but I’ve seen it surprise even the most experienced CSS experts. We can get so focused on ideas like specificity and importance, that we forget the cascade is only one part of a larger process.

Wireframe of a website with nested blue boxes, all of them have dashed borders

We start from a tree of un-styled elements,

cascade:is(.selectors) - same wireframe, but some of the borders are now solid, and some have become pink

Then attach styles to those elements, using inline styles or selectors – and resolving conflicts in the cascade. But only conflicts that are on the same element. The cascade doesn’t get involved if one selector is styling the root, and another selector is styling paragraphs.

inheritance from context - same diagram, but the boxes that weren't styled before now match the color of their parent box

By the time we get to defaulting and inheritance, the cascade is already over, so things like specificity and importance are no longer relevant.

Only the boxes and properties without a cascaded style will need to inherit a style instead.

Sometimes the cascade just isn’t involved: present, debug

So it doesn’t matter how much importance we use when setting root color to teal. If we also set the body color to deepPink, then that’s the color we’ll see. The body element already has a color applied, with no cascading conflicts, and we don’t need inheritance to go find one.

We can see that teal is applied to the root, it’s just not inheriting because directly applied styles will always override defaulting styles.

Even large stylesheets Leave Most Properties Empty

But even the largest stylesheets on the web can’t set all the properties on all the elements – no one is writing out all that code!

So most properties will use a default value.

avatar

Define some constraints. Let the language work out the details.

— Keith J Grant, Resilient, Declarative, Contextual

And that’s a good thing. We want to rely on the defaults anywhere we can. This is one of the most common mistakes people make in CSS – and a mistake that large language models tend to make even worse.

You really want to do as little as possible in CSS, because leaving extra code around – even code that doesn’t seem to do anything – can have surprising consequences down the road!

That’s what the next lesson is all about.

  1. Filtering
  2. Cascading
  3. Defaulting
CSSWG, Value Processing Spec

So these steps – filtering, cascading, and defaulting –

  1. Filtering
  2. Cascading
  3. Defaulting
CSSWG, Value Processing Spec

find all the relevant declarations in all the relevant stylesheets –

  1. Filtering
  2. Cascading
  3. Defaulting
CSSWG, Value Processing Spec

merge them together, resolving any direct conflicts –

  1. Filtering
  2. Cascading
  3. Defaulting
CSSWG, Value Processing Spec

and then fill in the blanks, if anything is missing.

Which gets us back where we started –

One value For every property
on every element

Every property on every element has a single value.

  1. …
  2. …
  3. …
  4. Resolving
  5. Formatting
  6. Constraining
Continued… [CSSWG, Value Processing Spec]

And then the browser runs another series of steps to apply these styles and determine what it all means.

What’s the actual result of applying this style configuration to the page?

First resolving each value based on its type – lengths can be converted to pixels, and colors convert to RGB. Then formatting the page to see how properties interact, so we can determine things like percentages and grid fractions and flex values in context. And constraining the results to what can actually be rendered, like rounding to the nearest pixel.

What are the suggestions?

  1. Filtering
  2. Cascading
  3. Defaulting

How do they fit together?

  1. Resolving
  2. Formatting
  3. Constraining
CSSWG, Value Processing Spec

I think of value processing in two groups. The first set of steps – filtering, cascading, and defaulting – are all about merging and resolving conflicts to get a single value for every property. Once we have that, the second set of steps – resolving, formatting, and constraining – apply those styles to the page.

We’re writing A Config File

We have tools that behave differently at every step, but we don’t control the steps involved, or the order that things will happen.

We’re not writing the program in the traditional sense, we’re writing a configuration that will be merged with other configurations, and then used by the program when it runs.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

But the program runs the same way, following the same steps, with or without our input.

There is no “Real” Programming

I don’t mean that CSS isn’t real programming or something. That’s an absurd debate, and a waste of time. “Real” programming isn’t a real thing, it’s only a phrase used for starting fights. So let’s move on.

  1. Filtering
  2. Cascading
  3. Defaulting

  1. Resolving
  2. Formatting
  3. Constraining
CSSWG, Value Processing Spec

I think this value resolution process is often where authors get tripped up and confused, especially coming from other programming languages. Because it’s a complex series of steps that our code will have to go through before it applies to the page.

Styles that survive Are Changed by the Process

A lot can happen along the way! Styles rarely make it through unchanged.

Properties Resolve In Context

Even if that declaration survives the cascade, what it means will depend on other properties – and even other elements!

So the meaning of a property – like the meaning of a word – always depends on context.

Properties interact (live demo): present, debug

This is most clear when we’re using ‘relative’ units like percentages, ems, or line-height (lh) units. The font-relative units will change when the font-size changes – even if that change is inherited from a parent or ancestor.

And percentages act differently depending where they are used – but always relative to something else, like the inherited font-size, or the width of the parent content-box.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

These interactions are also part of the value resolution process – especially the resolving and formatting steps.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

At this point the browser knows what styles have been requested.

One value For every property
on every element

We have a single specified value for every property, on every element.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

So now the browser has to figure out how all those specified styles interact with each other, and how to render the page based on that information.

This is a bit like solving a puzzle, following a provided set of rules –

css
.feature {
  font-size: 200%;
  width: 30ch;
}

If the font-size is set to 200%, a browser needs to find the inherited font-size, and then multiply it.

If the specified width is 30 characters, then a browser has to resolve the font-size first, along with the font-family – to find out what font is being used – then check the font metrics to find out the width of a character at the current font-size, and multiply that by 30 to get the width.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

That’s all part of resolving the width to a px value, but the browser still has to consider box-sizing to determine how the width is applied when formatting the page.

All those steps have to happen in the correct order, or we would get the wrong answer. And we want every browser to get the same answer, so these interactions – and the order of operations – have to be defined clearly in the specification.

css
.square-or-widescreen {
  /* any two are possible, but not all three */
  aspect-ratio: 16/9;
  width: 100px;
  height: 100px;
}

But our styles don’t just interact, sometimes they come into direct conflict! And the browser has to find a solution – some way of rendering the page – even when we request styles that would be impossible to achieve.

There’s no way to draw a square box, with equal height and width, that also has a 16-over-9 aspect ratio! So if we set the width and height explicitly, and then provide an aspect ratio, the browser has to ignore one of these properties in order to lay out the page.

A set of interlocking gears
being designed in OnShape,
with icons and dimensions that represent
each constraint on the drawing

If you’ve ever used tools for 3d modeling – I know most of you probably haven’t – there’s an approach called parametric design, which works a lot like CSS. These tools have a helpful way of visualizing what constraints have been added, throwing errors when they come into conflict.

A closed shape with four sides,
and an icon in each corner representing
the attachment constraint

Here’s a polygon made out of four lines with constraints at each corner ensuring they stay attached.

The sides across from each other are now parallel,
with an icon on each representing that constraint -
but the corners still have non-right angles,
so it isn't a rectangle

If we want a rectangle, there are several ways we could get there. We could say that the sides across from each other have to be parallel. That’s a reasonable constraint. But it can still squish.

An icon in one corner represents the 'perpendicular'
right-angle constraint.
That combined with the parallel sides
ensures that the shape is a rectangle,
no matter how we orient it

If we make two of the sides perpendicular – adding a right angle – the points are all still free to move, but now the relationships are constrained to ensure we always have a rectangle.

A 'horizontal' constraint on the bottom side
locks in the orientation,
while 9cm and 16cm dimension constraints
ensure a fixed size and aspect ratio,
and the bottom left corner
now has an attachment constraint
holding it in place.

We can also add things like a horizontal constraint to lock the orientation – a height and width to maintain dimensions – and attach one corner to the ‘source’ of the layout.

At this point, our box is fully constrained. There’s only one ‘solution’ possible that achieves everything we explicitly requested, and no lines or points are free to move around. To make any changes, we have to alter or remove those constraints.

After trying to add another 'perpendicular' constraint
to one of the other right angles,
an error window pops up warning
that the sketch geometry is over constrained -
even though the lines are already perpendicular

If we try adding new constraints, we get an error! Even if we’re asking for a constraint that works with the existing solution, like another perpendicular angle.

The tool is encouraging us to choose what we care about most, and avoid unnecessary constraints.

The parallel constraints are removed,
and now three perpendicular constraints are used
to achieve the same result.

But there are other ways we could achieve the same result. If we remove the parallel constraint, and add three right angles, we’re back where we started.

There are multiple ways to achieve the same design, depending on what we consider the most important. This isn’t the only way to do 3d modeling, but the advantage is how maintainable these designs are. If we set up the constraints to match our goals, it becomes easy to adjust one dimension or angle, and watch everything else adapt.

Responsive to… Browsers And Maintenance

CSS is built on the same idea. By documenting all the important relationships and constraints in a design, we make it responsive – not only to the size of the browser, but to changes we might make in the future!

Responsive components… Become Modular

Responsive CSS is maintainable CSS, and responsive components are modular.

When a component is constrained only in the ways we care about, but remains free to adapt in every other way – that component can move from one context to another without issues.

css
.square-or-widescreen {
  aspect-ratio: 16/9;
  width: 100px;
  height: 100px; /* error: over-constrained! */
}

Each declaration that we write is a constraint on the element. And that’s great when the constraints are expressing our goals. But browsers don’t have good ways to warn us that an element is over-constrained.

Instead, some rules have to get priority over others.

Over-constrained layout: present, debug

Generally, rules that are more explicit or direct (like width and height in this case) will override rules that are a bit more conceptual like the aspect-ratio – which just describes a relationship between the width and height.

Even if we change the aspect-ratio to 1/1, a value that works with the others, we’re still over-constraining the layout. Since the width and height are already defined, the aspect-ratio has no open parameters to fill in – it’s not doing anything.

As we add properties… Constraints Can Multiply

At this point it’s not hurting much, but the more explicit we get – the more constraints we add – conflicts between properties can multiply at a much faster rate. By leaving unnecessary constraints in our code, we’re inviting unexpected problems down the road.

css
.square-or-widescreen {
  aspect-ratio: 16/9;
  width: 1600px;
  height: 900px;
}

So looking at code like this, I would always ask what’s most important to me, and remove any constraints that I can. Our goal is to establish the right priorities to maintain a design across different contexts – including the potential for other styles to apply that might be out of our control.

Big 3d text in space says we're not alone.

Not just making it work on my device, but communicating clearly what it is we care about in this collaborative context.

Inspecting the box with an aspect ratio, height, and width. None of them are marked as being irrelevant.

Sadly browser tools don’t really show us these conflicts when they come up. Because there’s not a simple on/off switch for any of the properties. It makes this difficult to debug.

But it’s still an important issue to watch for! If one property isn’t applying the way you expect, try turning other properties on and off to see what changes.

The Flex highlighter, Flex Container pane, and Flex Item pane in Firefox developer tools
Firefox Flexbox Inspector

I wish browsers would provide more tools like the Firefox inspector for flexbox, which shows exactly how the final layout is determined.

Min, max, and width interaction: present, debug

But sometimes we want one property to constrain another. Properties like min- and max-width only exist to put constraints on the width property. If we define all three, the min and max values will take priority whenever there’s a conflict.

And min-width will override max-width if those two conflict.

Many CSS properties Have Modal Behavior

Some property interactions are less nuanced.

Many properties in CSS have different ways of working – or sometimes they don’t work at all – depending on what mode we’re in. And the mode might be controlled by another property, or even parent and sibling elements.

display property Controls Layout Mode

The display property, for example, often controls what layout mode – or formatting context – we’re in, which can change how other layout properties behave.

css
.modal-width {
  width: 500px;
}

So if we set an element to width: 500px we might get a box that’s 500px wide – but that’s not guaranteed!

Modal properties: present, debug

If that element is inline, then width doesn’t apply at all. We also have a flex value that’s not being applied, because the parent element is not a flex container!

Inspecting the previous codepen demo, both the width and flex properties are greyed out with an icon for more info. Hovering over the width info icon, there's a tooltip that explains The display-inline property prevents width from having an effect. Try setting display to something other than inline.

In this case, the browser dev tools do show us that some properties are being ignored. We’d see something similar if we add grid-template-columns or flex-direction to an element that is not a grid or flex container.

But this only works when a property is entirely ignored – width and flex have no way to impact this element. But most modal changes are more subtle than that.

In the firefox dev tools, with the parent element providing a flex container, we get a diagram that shows a 500px flex-basis (provided by the width value) and then 141.75px of flexible shrinkage to get a final size of 358.25px.

If the parent is a flex container, then the flex value will be applied, and so will the width – but only as a starting point that our flex value will override.

This is where Firefox dev tools can help explain exactly what’s going on.

Width, display, flex, and background are all being applied - but the inline display has no impact

Nothing is greyed out any more, and there are no warnings, but one of these properties is still being ignored!

Flex and grid items can’t be inline. That value has no meaning here. But the display property isn’t entirely ignored – it could still get set to display: none, display: grid, or display: contents for example.

Since it’s only the inline value being ignored, and not the whole display property, we don’t get any warning. Dev tools aren’t designed with that level of nuance.

width: auto

If a width of 500px is that context-dependent, think how much more flexibility is allowed when the width is auto. How big is auto?! That depends on the space available, and the layout context, and the size of the contents, and the margins, and sibling elements, as well as the parent layout mode!

We write auto in our CSS file, and out the other end we get something like 1234.45px.

Everything flows.

— Heraclitus, c. 500 BC, Cratylus

We could be here all day, just talking about properties that change the meaning of another property.

  • The box-sizing changes what width means
  • The writing-mode changes what direction inline-size refers to

We’ll talk about some of that when we get to layouts.

Value resolution Depends on Context

But for now it’s important to understand: the meaning of a property isn’t isolated or stable. Properties and values get their meaning from context, just like words take on different meanings in different sentences.

And that context can be other properties on the same element, but it can also be the context provided by parent and ancestor elements.

CSS (like all design) Is About Relationships

CSS isn’t defined this way just to be confusing. CSS is a design language, and the entire field of design is about managing relationships. Concepts like color and size and shape don’t exist in isolation!

Grid Terminology: present, debug

A grid system isn’t a property of a single element, it’s a relationship between elements.

Primary concerns of a type scale: present, debug

A typographic hierarchy relies on some text being larger or more bold than other text. No matter where we start, it’s the relationship between sizes that matter most – and the relationship those font-sizes have to the overall page.

A cylinder casts a shadow across a chess board, creating an optical illusion - a dark square outside the shadow (A) and a light square inside (B) are actually the same shade of gray
A and B are both #787878

Colors appear different to us based on context –

Color is relative: present, debug

And the colors around them –

OddContrast
OddContrast

Issues like text contrast are based on those relationships, and also impacted by differences in font-size. There’s more than one way to make text stand out on a page.

css
form:has(:invalid) button[type="submit"] {
  --invalid: light-dark(#ebe7dc, #242119);
  background: var(--invalid);
  color: contrast-color(var(--invalid));
}
Showing Our Work

So writing CSS is a process of showing our work – capturing the goals and relationships behind every design decision. What constraints matter in our desired solution.

So that as other variables change – browser dimensions, user inputs, color-schemes, etc – the browser will hopefully come to the same result that we would, without needing us to write out every single possible iteration by hand.

Figma design mockup for the oddContrast tool, with the sidebar selected -- 593px (about a third of the page) by 1398px tall (the full height)

As a CSS developer, my job translating a design mockup often comes down to a single repeated question: what do we care about most?

I might see a box that is 593px wide. But why? How did we get there? Is it related to the size of the container, or the size of the contents or siblings?

If we’re not the designer, hopefully we can ask them! I find it really useful to write out these questions, and pair-program with a designer – showing them how different approaches move in different situations.

Browser dev tools showing the computed styles tab with a list of CSS properties. The first one, accent-color, shows an rgb color value. From there the list is slightly transparent - align-content normal, align-items normal, align-self auto, and so on.

The big difference between CSS and other constraint-based design programs is in the defaults. A page without any CSS attached already has responsive logic built-in.

Every property already has a default value, and those defaults add up to provide –

Cascading HTML Stylesheets -- a proposal
Håkon Lie, Cascading HTML style sheets – a proposal (Oct 1994)

A baseline design system that might not always be beautiful, but is already extremely flexible – with responsive relationships built-in.

The fonts and font-sizes and colors respond to the user, to the device, to the browser. Text flows in lines, and wraps at the edge of the page – stacking to create blocks.

Everything is fluid. Everything is responsive. Everything can be adjusted by the user. There are just enough constraints to render the page.

The Program Runs Fine Without Us

The program runs fine without us.

avatar

Define some constraints. Let the language work out the details.

— Keith J Grant, Resilient, Declarative, Contextual

Then we come along and add more constraints! That’s not bad, that’s part of design. Adding our voice to the mix is the reason CSS exists.

We’re providing a point of view.

But the more constraints we add, the less flexibility is provided to the browser and the user. Ideally, we only want to say what’s essential, and let the browser work out the details.

avatar

Be the browser’s mentor, not its micromanager.

— Andy Bell, buildexcellentwebsit.es

Or as Andy Bell says – “be the browser’s mentor, not its micro-manager.”

The order of transformations: present, debug

Sometimes the order that properties apply can make a big difference. For example, when we’re moving an element around using translate, rotate, and scale.

If we’re doing this inside the transform property, we can put these functions in any order. If we rotate and then move the element, it moves in the rotated direction. But if we move it first and then rotate, we get a different result. The same is true if we scale before moving an element. Now the movement is scaled along with the element.

css
.mover {
  /* translate, then scale */
  transform: translate(100px) scale(2);

  /* scale, then translate */
  transform: scale(2) translate(100px);
}

If we use the functions, we’re in control of the order they apply! Because all of this, all these functions in order, act as a single value – which will be maintained when (and if) the property is applied.

The order of transformations: present, debug

But if we switch to the stand-alone translate, rotate, and scale properties – the browser will always apply them in a pre-defined order: First moving, then rotating, then scaling the element. And if we define all four properties, the transform property is always applied last!

That pre-defined order is maintained, no matter what order we write our declarations in. That’s intentional! This order has the most predictable result – without allowing rotation or scale to change the direction or distance of the movement.

CSS is Declarative

This is part of what it means for CSS to be a declarative language. We’re not writing a series of steps to perform in order – we’re only changing what value is used for each property on an element.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

And then the program runs, using some or all of the values we provided. We define values, but the browser defines how properties interact – how they apply, and in what order.

That’s different from most other programming languages, including JavaScript or pre-processors like Sass.

scss
/* Sass Variables */
this-slide {
  $background: red;
  $background: yellow;

  background-color: $background;

  $background: hotPink;
  $background: cyan;
}
What’s the background-color?

Here’s a block of Sass, that will output a background color for the slide – using a variable called $background. First we set the variable to red, then yellow, then we apply it to the background-color CSS property, and then change the variable to hotPink, and finally cyan.

js
/* JS Variables */
let backGround = 'red';
backGround = 'yellow';

thisSlide.style.backgroundColor = backGround;

backGround = 'hotPink';
backGround = 'cyan';
What’s the background-color?

We can write the same thing in JavaScript, and follow the same series of steps. After changing the variable from red to yellow, we apply it to the slide backgroundColor property – the same property that we control from CSS.

What color should we expect the slide background to be?

What color from Sass?: present, debug
diff-scss
--- the 'most recent' value is used
 this-slide {
-  $background: red;
   $background: yellow; /* winner! */

   background-color: $background;

-  $background: hotPink;
-  $background: cyan;
 }
Variables in Sass

Both Sass and JavaScript are imperative programming languages, so the code is run in order – a series of steps.

The variable is yellow at the point in that process where it gets applied to the background-color. Changing the variable later in the program doesn’t impact what already happened.

diff-scss
--- the 'most recent' value is used
 this-slide {
-  $background: red;
-  $background: yellow;
+  $background: hotPink; /* winner! */

   background-color: $background;

-  $background: cyan;
 }
Variables in Sass

If we change where the variable gets used, we get a different output.

The variable in this case has different values at different points in time as the program is run. And we control the timing.

But CSS doesn’t work like that.

css
this-slide {
  --background: red;
  --background: yellow;

  background-color: var(--background);

  --background: hotPink;
  --background: cyan;
}
What’s the background-color in CSS???

Let’s try this again with similar code, but this time we’re just using CSS: the variable is set to red, and yellow, applied to the background-color of the slide, and set again to hotPink and cyan.

One value For every property
on every element

Remember, a CSS variable is just a custom property, which works like any other CSS property. And, as we’ve discussed, In CSS every property has a single value on every element.

css
this-slide {
  --background: red;
  --background: yellow;

  background-color: var(--background);

  --background: hotPink;
  --background: cyan;
}
What’s the background-color in CSS???

So the value of our variable doesn’t change colors as the code is executed – first one color, and then another…

diff-css
--- the cascade determines a winner
 this-slide {
-  --background: red;
-  --background: yellow;

   background-color: var(--background);

-  --background: hotPink;
   --background: cyan; /* winner */
 }
What’s the background-color in CSS???

Instead browsers use the cascade to determine a single value for each property, including the variable –

diff-css
--- the cascade determines a winner
 this-slide {
+  background-color: cyan;

-  --background: cyan;
 }
What’s the background-color in CSS???

And then resolves any relationships between them.

What color from CSS?: present, debug

It doesn’t matter when we apply the variable to the background color – because these are not steps in a program. We can declare the background first – before our variable has been mentioned at all – and it doesn’t change the outcome.

Not a series of steps, but A Bucket of Declarations

CSS is a declarative language

We don’t control the order in which things happen, we only control the value on each property.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

How those properties apply is defined by the language, not by our code.

css
this-slide {
  background-color: var(--background);

  /* the cascade resolves conflicts! */
  --background: red;
  --background: yellow;
  --background: hotPink;
  --background: cyan;
}
What’s the background-color in CSS???

If you’re thinking order does matter in CSS, you’re right! Code order is an important feature of the cascade. But that’s only used for resolving conflicts when the same property is defined multiple times.

css
this-slide {
  background-color: var(--background);

  --background: red;
  --background: yellow;
  --background: hotPink;
  --background: cyan;
}
What’s the background-color in CSS???

So the order of our code only matters…

css
this-slide {
  background-color: var(--background);

  --background: red;
  --background: yellow;
  --background: cyan;
  --background: hotPink;
}
What’s the background-color in CSS???

When one property has multiple values, and they need to be merged by the cascade to get us back to a single value.

The order of transformations: present, debug

Going back to the initial demo, then… If I declare the transform property multiple times, with different values, the cascade will prefer the last one by default.

  1. Filtering
  2. Cascading
  3. Defaulting
  4. Resolving
  5. Formatting
  6. Constraining
CSSWG, Value Processing Spec

The order we write our CSS can be very important – we’ll cover that in more detail as part of the cascade – but the order of our code does not describe a timeline of actions. It only helps to resolve conflicts.

The timeline – the series of steps to apply – is already defined by the browser. And that program runs the same way with or without our input. All we can do is customize the settings.

Next module… Balance of Concerns

« back to resources
SlideDeck Controls

View

Keyboard shortcuts
cmd/ctrl-k:
Toggle this control dialog
cmd/ctrl-shift-enter:
presentation (from first slide)
cmd/ctrl-enter:
presentation (from active slide)
alt-enter:
in speaker view (following active slide)

Color Settings