An ordered list (cascade) of style sheets.
— Håkon Lie, Cascading HTML style sheets – a proposal
button { background: gray; }
.action { background: darkBlue; }
[type=“submit”] { background: darkGreen;
background: var(--submit, black); }
#send { background: maroon; }
* including Editor’s Draft proposals
<button>
Needs
One Background-Color
Color
Border-Top-Left-Radius
color
,border-top-left-radius
,removes extra values
fills in missing values
0+
declared values)0|1
cascaded value)1
specified value)1
computed value)1
used value)1
actual value)(Read it from the specification)
.short { border: thin double; }
.long {
border-top: thin double;
border-left: thin double;
border-bottom: thin double;
border-right: thin double;
border-image: none;
}
border-image
Border
is shorthand for setting all of them.short { border: thin double; }
.long {
/* declared */
border-top-width: thin;
border-top-style: double;
/* reset to 'initial' value */
border-top-color: currentColor;
border-image: none;
}
width
, style
, and color
of eachcurrentColor
border-image
which is reset-only hereproperty: value;
in transitions and animations
hotPink
»
teal
hotPink
rgb(93 138 220)
?!
teal
hotPink
and teal
button {
background: teal;
transition: background 1s;
}
button:hover { background: hotPink !important; }
:target { animation: bg-change 3s ease-in both; }
@keyframes bg-change {
from { background: pink; }
to { background: cyan; }
}
animation
property is in the cascade!important
keyframes [present, debug]Just to restate:
<img src="..." width="160px" height="90px" hidden>
<s>strikethrough</s>
width
, or height
, or hidden
[hidden] { display: none !important; }
styling web components
Context
in the cascade/* shadow DOM styles */
:host(my-element) { outline: thin solid red; }
[part=title] { outline: thin dashed red; }
::slotted(span) { outline: thin dotted red; }
/* light DOM styles */
my-element { outline: thick solid green; }
my-element::part(title) { outline: thick dashed green; }
my-element > span { outline: thick dotted green; }
similar to user agent origin
!important
to protect essential styles,and reverses priority
(demo if needed)
<button style="
background: mediumVioletRed; /* winner! */
color: white !important; /* winner! */
border: thin solid;
">…</button>
button {
background: white;
color: teal !important;
border: thick dashed !important; /* winner! */
}
@layer settings { … }
@layer tools { … }
@layer generic { … }
@layer elements { … }
@layer objects { … }
@layer components { … }
@layer overrides { … }
demo:
@layer
{ … }
@layer
<name>
{ … }
initial
, inherit
, unset
, revert
, revert-layer
, etc@layer reset {
audio[controls] { display: block; }
[hidden] { display: none !important; }
}
@import url(…)
layer
;
@import url(…)
layer(<name>)
;
@layer reset { /* least powerful */ }
@layer default { /* … */ }
@layer theme { /* … */ }
@layer components { /* more powerful */ }
/* unlayered styles: most powerful */
@layer reset { /* least powerful */ }
@layer components { /* more powerful */ }
@layer reset { /* still least powerful */ }
@layer
<name>
,
<name>
,
<etc>
;
<style>/* keep this before linked styles */
@layer reset, framework, components, utilities;
</style>
<link rel="stylesheet" href="…">
<link rel="stylesheet" href="…">
!important
->
Layers ->
Importance
@layer reset {
[hidden] { display: none !important; }
}
* { /* universal */ }
p { /* type */ }
.summary { /* attribute */ }
#call-to-action { /* id */ }
* { /* universal */ }
button, div, span {
/* 'type' (element) */ }
::before, ::after, ::part() {
/* 'pseudo-element' */ }
.action, .summary {
/* 'class' */ }
:hover, :user-invalid {
/* 'pseudo-class' */ }
[type=“submit”] {
/* 'attribute' */ }
#send { /* 'id' */ }
h1#page-title { /* … */ }
p.summary a:hover { /* descendant */ }
p.summary > a:hover { /* child */ }
p.summary + a:hover { /* next */ }
p.summary ~ a:hover { /* future */ }
h2:has(> button[aria-expanded="false"]) + div {
display: none;
}
div
that comes after an h2
button
child, that is not expanded)/* 1 IDs, 2 attributes, 2 element type */
/* specificity: 1,2,2 */
form#contact button[type='submit']:active {
border-color: seaGreen;
}
border-color
declaration has a selector1, 2, 2
[
1
,0,2]
vs [0,3,2] vs [0,2,3]
[0,
3
,2]
vs [0,
2
,3]
Especially “At Scale”
(still meaningful and expressive)
#example { /* [1,0,0] */ }
[id="example"] { /* [0,1,0] */ }
.example { /* [0,1,0] */ }
.example.example.example { /* [0,3,0] */ }
:is()
and :where()
Select The Same Things
:is()
and :where()
selectors/* (a) AND ALSO (nav .active) */
a:where(nav .active) { color: black; }
a:is(nav .active) { color: black; }
active
class inside a nav
element)/* (h1 a) AND ALSO (main > *) */
h1 a:where(main > *) { color: black; }
h1 a:is(main > *) { color: black; }
:is()
and :where()
Can Group Selectors
h1 a:hover, h1 a:focus,
h2 a:hover, h2 a:focus,
h3 a:hover, h3 a:focus,
h4 a:hover, h4 a:focus,
h5 a:hover, h5 a:focus {
text-decoration: underline;
}
:is(h1, h2, h3, h4, h5) a:where(:hover, :focus) {
text-decoration: underline;
}
:is()
and :where()
Also Impact Specificity
:where()
Removes Specificity
/* nav a.active { 0,1,2 } */
nav a:where(.active) { /* 0,0,2 */ }
a:where(nav .active) { /* 0,0,1 */ }
*:where(nav a.active) { /* 0,0,0 */ }
:is()
takes
Highest Specificity
:is(a, .b, #c .d) { /* 1,1,0 */ }
a:is(.b, #c .d) { /* 1,1,1 */ }
a:where(#logo, .sponsor .logo) {
/* specificity: 0,0,1 */
}
a:is(#logo, .sponsor .logo) {
/* specificity: 1,0,1 */
}
<a class="sponsor logo">Still 1,0,1</a>
a:is(#fakeID, .sponsor .logo) { /* … */ }
Also relies on :is()
selector
header {
h1 { /* header h1 */ }
> .title { /* header > .title */ }
+ p { /* header + p */ }
}
header {
h1 { /* header h1 */ }
> .title { /* header > .title */ }
+ p { /* header + p */ }
body & { /* body header */ }
}
.card {
@media (width > 20em) {
display: flex;
}
}
ol, ul {
> p { /* :is(ol, ul) > p */ }
.sidebar & { /* .sidebar :is(ol, ul) */ }
}
:is()
under the hood/* :is(button, .btn, #my-btn) */
button, .btn, #my-btn {
&:focus,
&:hover,
&:active { /* 1,1,0 */ }
}
:not()
and :has()
:is()
& :not()
& :has()
Use Same Specificity
:not()
For Excluding Elements
(inside matches are removed from outside matches)
/* (p) UNLESS (.warning) */
p:not(.warning) { /* … */ }
:has()
For Selecting Context
(we can move the subject!)
form:has(:focus) { /* form with focus */ }
button:has(svg) { /* button with icon */ }
:has()
Selectorform:focus-within { /* form with focus */ }
.card:has(> figure:first-child) { /* image card */ }
.card:not(:has(img)) { /* card without image */ }
input:has(+ .error) { /* input followed by error */ }
article:has(h1, .title) a {
color: red;
}
article h1 a {
color: green;
}
Rolling out this year!
(across large teams & projects)
(through lower boundaries & proximity)
.light-theme a { color: purple; }
.dark-theme a { color: plum; }
@scope
(
<root>
) { … }
@scope (.light-theme) {
a { /* similar to simple nesting… */ }
}
@scope (.dark-theme) {
a { /* but the _closer_ scope root wins… */ }
}
.title { /* global */ }
.post .title { /* nested */ }
.post__title { /* BEM */ }
BEM, CSS Modules, Vue, JSX, Stylable, etc
.post__title { /* BEM */ }
.title[data-JKGHJ] { /* Vue */ }
@scope (<root>) to (
<boundary>
) {…}
@scope (.media) to (.content) {
img { /* only images that are "in scope" */ }
}
<article>
<style scoped>
p { color: green; }
</style>
<p>This paragraph will be green.</p>
</article>
<p>This paragraph won't!</p>
<article>
<style>
@scope { p { color: green; } }
</style>
<p>This paragraph will be green.</p>
</article>
<p>This paragraph won't!</p>
Always child or descendant
@scope (.block-name) to (.block-content) {
.title { /* only inside the block! */ }
}
button {
&:hover { background: hotPink; }
&[aria-pressed=true] { border: thick solid teal; }
@media (width > 30em) { padding-inline: 1em; }
& + & { border-inline-start: 1ch; }
}
button {
background: red;
background: oklch(0.5 0.2 0);
}
oklch()
color syntaxAnd that’s the cascade, in 7 steps…
for each property of each element