« resources

Smashing Workshop

Variables & Value Resolution

controlling the matrix

workshops.oddbird.net/smash24/variables/

Cmd/Ctr-K for Controls

Quick poll (in chat)… Who’s Using Custom Properties?

a.k.a. CSS Variables

  • ✅ all the time
  • 🤔 on occasion
  • ❌ not at all

Variables Store Values

  • Variables are common in programming
  • Capture, store, and access values

Global… Design Tokens

brand colors, sizes, fonts, etc…

Term coined by Jina
  • In CSS that has often meant
  • Global ‘design tokens’
  • brand colors, sizes, fonts, etc

Local… Design Tokens

button-background, primary-grid…

  • Don’t have to be global
  • Don’t have to be simple

Re-use to Avoid Repetition

  • Broadly any CSS value we want to re-use

Provide Meaningful Names

hsl(331deg 42% 30%)

aka color-mix(in oklab, darkRed 73.29%, mediumBlue)
  • What is this color?
  • A mix of darkRed and mediumBlue?
  • With variables we can give it
  • A meaningful name, based on…

deep-raspberry

What it looks like
  • What it looks like
  • (deep raspberry)

best-color

Why we chose it
  • Why we chose it
  • (the best color)

action

What it signifies
  • What it signifies
  • (action, maybe)

link-text
button-background

Where we’ll use it
  • Where we’ll use it
  • (link text and button backgrounds?)
scss…
--deep-raspberry: hsl(322, 92%, 24%);
--best-color: var(--deep-raspberry);
--action: var(--best-color);
--link-text: var(--action);
--button-background: var(--action);
Or all of the above?
  • Or all of the above?
  • We can use one variable to name the next
  • Creating different layers of meaning
Physical patch bay with wires
  • Like audio patch bays
  • Connecting an input to an output
  • With a physical cable
Support data from CanIUse.com on the css-variables feature
[Can I Use]
  • Widely supported since 2016…
screenshot
[CSS Custom Properties for Cascading Variables]
  • Two names
  • These are Custom Properties, that we define
  • Which can be used as Cascading Variables
  • That’s in some ways a strange combination
  • And means they don’t work the same as variables in most languages
  • Instead, they act…

Properties Of Elements

(in the cascade)

  • Like any other property in CSS
  • They are directly attached to elements,

--name: value;

  • We can declare a value in CSS
  • Using a normal declaration syntax,
  • (property and value)
  • But we come up with the property name
  • And it has to start with a double-dash

Called --dashed-idents

For mixing custom & built-in identifiers

All <dashed-ident>s are <custom-ident>s, but not vice versa
  • This is called a ‘dashed ident’ in CSS
  • Not only used for custom props
  • But anywhere a ‘custom identifier’
  • (something we name as authors)
  • Might share space with built-in CSS keywords
css…
built-in-property: value;
--custom-property: value;
  • In this case, sharing space with built-in properties
  • Need to tell the difference
  • Avoid conflicts in the future

-webkit-property
-moz-property
-ms-property
--property

All prefix, no browser: -<empty>-property
  • The syntax comes from browser prefixes
  • Which have (mostly) been phased out
  • Remove the browser name, and now it’s ours
css…
.warning {
  --state-color: maroon;
}
Define on any elements
  • We can attach that declaration to elements
  • And it will go through the whole process of cascading and defaulting
  • On each element, the same as a built-in property
  • Except that it won’t do anything…
css…
.alert {
  border: medium solid var(--state-color);
}
Access on any elements
  • Until we apply it to an actual built-in property…
  • That’s the whole point!
  • We get to decide where and when to use these values

var( --property )

  • Which we access from the var function
  • Var, parenthesis, the name of the custom property
Physical patch bay with wires
  • Like the audio signal in these cables,
  • We don’t hear any sound
  • Until we plug that signal into a speaker of some kind
  • An amplifier,
  • Something that turns signal into noise
scss…
button {
  background-color: var(--button-background);
}
Connecting the final patch cord to output
  • In CSS
  • Normal properties are the amplifier
An orange Fender guitar amp
with someone just out-of-frame
plugging in a cable photo: Aleisha Kalina
  • Turning silent ‘tokens’ into rendered styles

By default No Initial Values

Except the ‘guaranteed invalid value
  • But since these are ‘custom’ properties
  • They don’t have any initial value
  • Except the ‘guaranteed invalid value’ which we’ll discuss

By default Variables Inherit

  • And since we’re not usually defining them on every element
  • We’re often going to rely on inheritance
css…
html { /* or :root */
  --all-my: variables;
  --defined: on the;
  --root: element;
}
(Not quite) global variables, inheriting everywhere
  • It’s very popular to define some or all custom properties
  • In one place, on the root element
  • And expect them to inherit everywhere.
  • So let’s look at inheritance!

Cascading Removes Extra Values

result: 0-1 cascaded values

  • Once the cascade is done filtering out
  • Conflicting values…

Defaulting Inserts Missing Values

result: one specified value

Defaulting spec
  • The ‘defaulting’ process fills in missing values
  • Leaving us exactly one value per property per element
  • That’s true for built-in and custom properties…

Defaulting process Depends on the Property

  • But the details
  • depend on the specific property in question

Only some Properties Inherit

value from direct parent

  • Some properties inherit a value from context
  • This is not true for the majority of properties
  • To visualize how this works…
Wireframe of a website with nested blue boxes, all of them have dashed borders
  • Start with all these nested boxes
  • Assume they are un-styled boxed
cascade:is(.selectors) - same wireframe, but some of the borders are now solid, and some have become pink
Cascaded values from selector mapping
  • Cascade goes first
  • Explicit selectors & declarations
  • Styles attached to individual elements
  • Not everything gets style…
inheritance from context - same diagram, but the boxes that weren't styled before now match the color of their parent box
Inherited values from parent context
  • So after the cascade is complete,
  • And only when values are missing,
  • Then we can apply inheritance
  • Each (un-styled) box looking at it’s parent
  • Values can inherit from very high up, the root of the document
  • All the way in to an element nested deeply
  • But to get there, properties have to inherit one nested element at a time

Inheritance Requires Lineage

  • Without any cascaded values breaking that lineage
  • You don’t inherit a trait from your grandparents,
  • Unless your parents inherited that trait from their parents
  • So we can always look at the computed value of the direct parent
  • To find the value that inherits

Cascade Takes Priority

  • We could say the cascade takes priority over inheritance
  • In the same way origins and layers take priority over specificity
  • Because the cascade goes first
  • So inheritance only fills in what the cascade leaves open
  • And values higher up can’t inherit through a cascaded value
  • That context is lost and a new context is established
Cascade vs Inheritance [present, debug]

demo:

  • Here the color inherits fine on everything, from the root
  • But the font (even though it’s important) can’t inherit
  • Wherever it is set by the cascade (blockquote, and code)
Inspecting Cascade & Inheritance [present, debug]

[demo]:

  • cascaded declarations first
  • (our element is the subject)
  • (sorted by cascade priority)
  • inherited declarations
  • (our element is a descendant)
  • (sorted by inheritance proximity)
  • together: all the relevant declarations
  • and which one applies!
  • computed… still missing values for most props

Custom properties Can Carry Context

  • So now we have properties
  • That can capture the context in one location,
  • And carry that context silently to descendants
  • Until it’s needed
Custom Properties Inherit [present, debug]
  • Let’s look at a property that doesn’t inherit
  • Like the background
  • We can either be explicit to target with nested selectors
  • Or we can pass the value through a variable!

Inheritance Rewards Proximity

  • Like scope, inheritance rewards proximity
  • When we nest, the closer context is what inherits
Inheritance Proximity vs Cascade [present, debug]

We can Inherit Across Generations

  • We can also use custom properties
  • To carry some context past intervening cascade values
  • We can use this to revert to context higher up
End-Run Inheritance [present, debug]

Most properties Don’t Inherit

Generally… Typography Inherits

(including text colors)

Generally… Layouts Do Not Inherit

(including anything box-related)

Generally… Un-styled Inline Boxes (spans)
Should Not Impact Layout

If I added a span in the middle of this paragraph, what styles would I expect to apply automatically inside the span so that the span doesn’t change anything?

— Internal Monologue

  • So we can ask ourselves
  • If I inserted a span in the middle of this paragraph
  • What styles would need to inherit
  • So that the span doesn’t look different?
Inheritance intuition [present, debug]

[demo]:

  • Try all:initial on the span
  • Try inheriting different properties
css…
/* 😭 Nooooooo */
html { box-sizing: border-box; }
* { box-sizing: inherit; }

/* 🥳 Much better */
* { box-sizing: border-box; }
  • This is one reason I recommend the simplest box-sizing reset
  • We shouldn’t be inheriting layout!
  • Especially not as a default!

No inheritance? Use Initial Value

Initial Value Spec
  • Spec here is brief (see next slide)…

Each property has an initial value, defined in the property’s definition table.

— Cascade & Inheritance, § 7.1. Initial Values

[Demo lookup]:

  • pick a property (display?)
  • Look it up on MDN
  • Check ‘formal definition’
  • (shows inherited, and initial)
  • Link to spec
  • (same)

If the property is not an inherited property, and the cascade does not result in a value, then the specified value of the property is its initial value.

— Cascade & Inheritance, § 7.1. Initial Values

Initial Value Of A Custom Property?

css…
html {
  font-size: var(--i-never-specified-a-value);
}
  • What happens if we access a property
  • That we never defined?

Guaranteed Invalid Value

like Undefined in JS

Custom properties only
  • All custom properties get the same initial value by default
  • We call it ‘the guaranteed invalid value’
  • No syntax for it, yet (I think we should add one)
  • It’s only used in custom properties
  • And this will surprise you…
  • it’s guaranteed to be invalid on every built-in property

Properties using var() become… Invalid At Computed Value Time

as a result, the property is _unset_
  • If it ends up on a property,
  • Invalid at computed value time
  • Since it’s guaranteed, we know earlier in the process
  • Not early enough discard before the cascade…
  • We have to finish the cascade and inheritance
Invalid At Computed Value Time [present, debug]
  • We’ll come back to this idea
  • When we talk about progressive enhancement
css…
--my-variable: initial;
color: var(--my-variable, CanvasText);
  • But at that point when we are about replace our var function
  • with a value we now know is invalid
  • We can use a fallback instead
css…
var(--my-color, teal)
  • The fallback comes after the name and a comma
css…
/* fallback: 'Georgia, Palatino, serif' */
var(--my-font, Georgia, Palatino, serif)
Allows List Values
  • Everything after that first comma is the fallback value
  • Even if it contains more commas
  • That way we can fallback to list values
css…
var(--btn-color, var(--action, teal))
Nested Fallbacks
  • We can also fallback to other variables
  • And those variables can have a fallback
  • So we can create chains of fallback options
  • By nesting var functions

A fallback, but… Not For Invalid Declarations

  • This is not validation that the value is allowed on the property
  • We only get the fallback
  • If our custom property has no value at all
  • Or is using the initial guaranteed invalid value

Warning Not for Legacy Support

  • So it also doesn’t work for legacy support fallbacks
Invalid, Unsupported, or Undefined [present, debug]
  • Here’s a demo if you want to dig into the nuance
  • We’ll look at it in more detail
  • When we talk about progressive enhancement
Simplified space toggles [present, debug]

We’ve Done It!* 🥳 Exactly One Specified Value

Either from…

  1. The Cascade (declarations)
  2. Inheritance (parent)
  3. Initial Value (spec)

We’ve Done It!* (* Almost)

  • Ok, we’ve almost done it
  • Still need to deal with…

explicit defaulting… “Global Keywords

available on every CSS property, including the all shorthand
  • Explicit defaults
  • These are the global keywords in CSS
  • Which are allowed on every property
  • including the all shorthand property

Explicit Defaults

  • initial
  • inherit
  • unset (inherit or initial)
  • revert (previous cascade origin)
  • revert-layer (previous cascade layer)
  • In fact, these are the only values allowed on all
  • Since they are the only values allowed everywhere

initial & inherit

choose your default, explicitly

  • Rarely use these
  • Only if you are very sure it’s what you want

unset

whichever default is appropriate

  • Prefer unset instead
  • To get either the initial or inherited value
  • Depending on the property
css…
body { margin: unset; }
  • This is a great option for CSS resets
  • When you really want a clean slate.
  • Just ‘pretend no one ever defined this property,’
  • ‘and see what happens’
The original CSS proposal document with all styles removed so that it's an unreadable wall of text
  • But this still has the problem
  • Of mostly returning the initial value,
  • Like display:inline
  • When it’s more common that we would want…

revert

defer to previous origin

  • Is the browser default styles
  • Revert will unset only the author styles
  • But keep browser defaults in place
  • ‘Pretend We didn’t define this, and see what happens’
  • But even this can be more than we want
  • Ideally, we could return to our own defaults!
  • Not only that, but revert

Also… Reverts Hints & Shadow Styles

  • Also eliminates presentational hints
  • This is the situation where they act like part of the author origin
  • And shadow DOM styles
  • That’s still pretty destructive if we’re not sure about it
  • But we can solve all these issues by using…

revert-layer

defer to previous layer (or previous origin)

  • revert-layer
  • This is the most chill of the explicit defaults
  • It will only unset our current cascade layer
  • Leaving everything else in place
  • So we can fall back to a lower layer,
  • or shadow DOM, or presentational hints, or browser defaults
  • or even the initial or inherited value
  • If a property isn’t defined in a lower layer
Explicit Defaults [present, debug]
  • We usually use unset or one of the revert values

Global keywords Resolve on Custom Properties

  • Global keywords on all properties
  • Including custom properties
css…
html {
  --color: teal;
  --color: initial;
  background: black;
  background: var(--color, red);
}
Global Keywords on Custom Properties [present, debug]
  • No way to pass keywords along as values
  • Importance also applies to the variable!

(ok, now) We’ve Done It! 🥳 Exactly One Specified Value

  1. The Cascade (declarations)
  2. Inheritance (parent)
  3. Initial Value (spec)
  4. Explicit Defaults (keywords)
  1. Filtering (0+ declared values)
  2. Cascading (0|1 cascaded value)
  3. Defaulting (1 specified value)
  4. Resolving (1 computed value)
  5. Formatting (1 used value)
  6. Constraining (1 actual value)
making relative values absolute
  • The next step of value resolution
  • Is to get the ‘computed’ value
  • making any relative values absolute
css…
p {
  color: hotPink; /* rgb(255, 105, 180) */
  border-color: currentColor; /* rgb(255, 105, 180) ? */
  font-size: 1.2em; /* 19.2px ? */

  /* these require formatting */
  width: 80%; /* still 80% */
  height: auto; /* still auto */
}

Computed Values Inherit

(except currentColor!)
  • Computed values inherit
  • Which might sound backwards, since we already handled inheritance!
  • But this whole process happens outside-in
  • The parent has to resolve, so children can inherit,
  • And then they have to resolve before the next generation can inherit

I Am Not A Lawyer

and this is not legal advice

  • This is all starting to sound like genetics
  • Or maybe estate law?
  • I am not a biologist or a lawyer,
  • So don’t take this as legal advice.

currentColor Inherits as Keyword

It should re-calculate when the color changes!

  • There are exceptions
  • CurrentColor inherits as a keyword, which is handy
  • It will re-calculate as the color changes
currentColor in SVG fill [present, debug]

Custom Properties… Remain As Specified

css…
p {
  --my-size: 1.5lh; /* 1.5em */
  --my-color: hotPink; /* hotPink */
}
  • Value hasn’t been parsed/validated
  • Not clear yet what type this is
  • Can’t make it absolute without a type
  • Or, for relative units like lh knowing what line to measure

Custom Properties… Substitute, Then Inherit

  • Custom properties don’t resolve before inheriting
  • But they do substitute before inheriting
  • Not
css…
html {
  --my-size: 1.5lh; /* 1.5lh */
  --calc: calc(var(--my-size) * 2);
}

p {
  /* --calc: calc(1.5lh * 2) */
  /* not calc(var(--my-size) * 2) */
}
Variables substitute then inherit [present, debug]

Built-in properties… Validate & Compute var()

css…
p {
  /* inherited calc(1.5lh * 2) */
  color: var(--calc); /* unset */
  min-height: var(--calc); /* 3lh */
}
Inspecting Cascade & Inheritance [present, debug]

demo:

  • see ‘computed’ panel
  • custom properties aren’t resolving?
  • only one! let’s look into that…
Image of animated Pinochio, no longer a puppet, saying I'm a real [property]
  • We’ve talked about defaults
  • But some custom properties dream of becoming more…
css…
@property --brand-color {
  syntax: "<color>";
  inherits: true;
  initial-value: hotPink;
}
Register Custom Properties
  • And we can use to the at-property rule
  • To define more about them,
  • The stuff that usually gets defined in a spec
  • For built-in properties
  • The syntax, initial value, and if it should inherit or not

Can I use… @property?

Everywhere, finally!
  • This is finally, recently, supported everywhere!

All three Descriptors Required

syntax, inherits, and (usually) initial

inherits true or false

css…
/* replace 'initial' with any default value */
* { --this-property: initial; }
Legacy inheritance override
  • Remember, if we don’t register the property
  • Inheritance is true by default
  • We can override that by defaulting it explicitly everywhere
  • That doesn’t turn off inheritance,
  • Just ensures we never get around to inheriting
  • Because there’s always a declared value

syntax Supported Names

Or "*" for ‘universal syntax’

Does Not Fix Invalid At Computed Value Time

  • Definitions are still not
  • available at parse time (could be redefined)
  • or stable enough to rely on (if new styles load)

Does Give Us Computed Values

(depending on syntax)

Registered properties compute [present, debug]

demo:

  • remove and re-add registration
  • inspect computed values
  • change to universal syntax (doesn’t compute)
  • change to color syntax (invalid)

Also… Allows Transitions & Animations

Gradient Transitions with Registered Properties [present, debug]

No Type Coercion

css…
.yes-number-to-length {
  --number: 1;
  --length: calc( var(--number) * 1em);
  /* not `var(--number)em` */
}
But we can append units with calc()
css…
.no-length-to-number {
  --length: 1em;
  --number: calc( var(--length) / 1em );
}
Removing units not yet supported*
Lengths to Numbers to Strings [present, debug]
  • There’s a workaround by Jane Ori
  • Using trigonometry functions
  • If you want to play with it

initial-value Must be Absolute

(when the syntax is not *)

Initial ~= Spec
Root/* ~= Browser Default

  • initial values equivalent to spec initial
  • Not right for every situation,
  • But a good fallback
  • Declarations on root/* better for dynamic tokens
Static Initial v Relative Default [present, debug]

Final Steps Used & Actual Values

  • All properties that apply
  • But some props might not apply
  • Because CSS properties are modal!
  • (e.g. flex: 1 on a non-flex item)
  • (or display:inline any time it’s not in flow)
  • Usually the same as ‘computed’ value
css…
html {
  color-scheme: dark light;
  /* ~ used: dark; */
}
Depending on user preference
css…
div {
  width: 80%;
  /* ~ used: 1049.6px; */
  /* ~ actual: 1050px; */
}
Depending on layout

🥳 Values Resolved

  • That’s it,
  • the entire ‘value resolution’ process

Intermission

  • Any questions on that,
  • before we get into
  • more variable use-cases?

Custom properties can Store Global Tokens

Similar to Pre-Processors
css…
:root { --btn-color: maroon; }
Globally (on html/:root, then inherit)
  • These tokens will inherit everywhere
css…
:root { --btn-color: maroon; }
/* html { --btn-color: maroon; } */
Either html or :root
  • Either root or html is fine
  • In any html document, select the same thing
  • Maybe you find it more clear?
  • Mostly just increases specificity
  • (Element type to Pseudo-Class)

Any Document Root

Like <svg> or <xml> or <html>

  • It’s true!
  • But is it actually what you want?
[demo link]

demo:

  • just an svg on it’s own
  • root styles apply to the svg element
The Rootiest Root [present, debug]

demo:

  • same svg in a page
  • (styles commented out)
  • html styles targeting the root & svg…
  • what happens when we uncomment?

I dunno… Proceed With Caution?

  • It’s not bad to put vars on root
  • But careful about using root
  • In nested contexts
  • Not every root, only the (outermost) root
css…
* { --btn-color: hotPink; }
Universally (on everything, then cascade)
  • Another global approach
  • Select everything
  • Rely on cascade, not inheritance

But not all Tokens Are Global

  • Not all design tokens need to be global
css…
button { --btn-color: maroon; }
Define Locally
  • Can also define locally
  • Specific to a component
css…
button[type='submit'] {
  --btn-type: seaGreen;
}
[aria-pressed=true] {
  --btn-state: rebeccaPurple;
}
Define Local Variations
  • Even variations of a component
css…
[data-grid] > * { --grid-area: main; }
Combine Local & Universal
  • We can combine these approaches
  • Define on everything in a certain context

Define Nowhere???

  • You don’t need to define anywhere to start
  • Sometimes useful to have ‘open’ (undefined) vars…
css…
button {
  background: var(--btn-context, maroon);
}
Use fallbacks for default value
  • Creating a button component
  • We can put the default value in a fallback
  • And leave the variable open for inheritance
  • Then later, we can fill it in…
css…
main { --btn-context: mediumVioletRed; }

[type='submit'] { --btn-context: steelBlue; }
button:focus { --btn-context: seaGreen; }
Override based on context
  • Override based on specific locations
  • Or based on different variations
  • In the component or interaction state
Layering State & Type [present, debug]
Custom Longhand [present, debug]

Custom properties Manage Dynamic Changes

  • Great for dynamic changes, like…
Custom Property Light/Dark [present, debug]
Custom Property Themes [present, debug]
Generated Colors & Contrast [present, debug]
Variables & Media Queries [present, debug]
Variables Grid Areas [present, debug]
html…
<button style="--btn-color: red;">
  You're not the boss of me
</button>
css…
/* --btn-color is not being used */
button {
  background: seaGreen;
  color: white;
}
Safe Inline Styles
Dynamic CSS Layouts [present, debug]

Combine with… Attribute Selectors

  • [attr]Presence (even if empty)
  • [attr="..."]Exact match
  • [attr*="..."]Any match
  • [attr~="..."]Space-delimited (like classes)
  • [attr|="..."]Hyphen-delimited
  • [attr^="..."]Starts with…
  • [attr$="..."]Ends with…
  • [attr="..." i|s]Case sensitivity
More details on MDN
html…
<!-- Element-attached/inline -->
<div style="--ease: var(--in-out-back);">
css…
[style*='--ease'] {
  --in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
  --out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --in-out-back: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
Define On-Demand Options

JavaScript Interactions

Cascading Colors [present, debug]
css…
html {
  @media (prefers-color-scheme: dark) {
    --os-mode: -1;
  }

  @media (prefers-color-scheme: light) {
    --os-mode: 1;
  }
}
css…
[data-colors='light'] {
  --html-mode: 1;
}

[data-colors='dark'] {
  --html-mode: -1;
}
css…
[data-colors] {
  --mode: var(
    --html-mode, var(
      --user-mode, var(
        --os-mode, 1
      )
    )
  );
}
--user-mode set via JS
Cascading Colors Updated… (WIP) [present, debug]
  • I’ve been updating that to use oklch colors
  • With color-scheme and the light-dark function
  • But many of those features are still too new
  • To rely on without a thoughtful fallback
Variables » Functions » Mixins [present, debug]
CSS gradient-art decorations [present, debug]
Angled Background CSS-Only Mixin [present, debug]
Component Arguments (Media Object) [present, debug]
Grid & Variable Temps [present, debug]
Vue Invaders! [present, debug]

CSS Animation

Krystal Campioni sprite animation
Sprite from Monster Slayer animation
html…
<section
  class="sprite-demo"
  :style="{
    '--src': show.sprite.src,
    '--columns': show.sprite.columns,
    '--rows': show.sprite.rows,
}">...</section>
html…
<div
  v-for="action in show.actions"
  :key="action.name"
  :data-action="action.name"
  :style="{
    '--row': action.row,
  }"
/>
Animated sprites from Monster Slayer by Krystal Campioni [demo link]
🙅🏻‍♀️ Please don’t use this 🙅🏻‍♀️ [present, debug]

Custom Properties Are Not Just Variables

(as we’ve known them)

Custom Properties Dynamic & Responsive Styling

Capture values Where Defined

Change values Based on State or Class

Values inherit Based on Context

Values are ignored Until Applied

« resources

Smashing Workshop

Variables & Value Resolution

Bring this workshop to your company.
Slide Controls

View:

Navigate slides using the arrow-keys.