Archive: August 2026

  • CSS value resolution process

    Once the browser has parsed a document and built the DOM tree, it has to assign a value to every property for every element in the flat tree 1, and in turn for every box those elements generate.

    The final value of a CSS property for a given element is the result of a multi-step calculation:

    1. Filtering:
    Collect all declarations that apply to a given element
    → Output: declared values (zero or many per property)
    2. Cascading:

    For each property, sort by precedence and resolve competing declarations

    → Output: cascaded value (at most one; may be none)
    3. Defaulting:

    Supply a value where the cascade produced none (inheritance or initial value), and resolve the CSS-wide keywords 2 when they are the cascaded value of a property

    → Output: specified value

    This results in exactly one specified value for every property (including custom properties) on every element. The remaining stages work out how those styles interact and what actually gets painted.

    4. Resolving:

    Absolutise the specified values as far as possible without layout 3

    → Output: computed values (this is what inherits to children)
    5. Formatting:

    Complete the remaining calculations, which require laying out the document

    → Output: used values 4
    6. Constraining:

    Adjust the used values based on what the rendering environment can actually do

    → Output: actual values

    Footnotes

    1. The DOM tree with any shadow trees flattened in

    2. initial, inherit, unset, revert

    3. For e.g. width: auto computes to auto, because you can’t resolve it without knowing the ancestors’ layout; the used value is a real length like 100px.

    4. Used values only exist for properties that apply to that element or box type. For e.g. flex has no used value on an element that isn’t a flex item.

  • Pseudo-elements are not valid inside :is(), :where(), and &

    A pseudo-element is not a valid selector inside :is() or :where(). As per the Selectors Level 4 spec:

    Pseudo-elements cannot be represented by the matches-any pseudo-class; they are not valid within :is().

    The nesting selector & behaves the same way. It desugars to the parent rule’s selector list wrapped in :is().

    However, neither of them fails loudly. :is() and :where() take a forgiving selector list, so an invalid argument is dropped and the rest of the rule carries on. Similarly, nesting inside a pseudo-element rule parses fine. The nested style rule just never matches anything, unless the parent list also contains non-pseudo-element selectors.

    /* exmaple 1 */
    .card::before {
    	content: '';
    
    	.dark & {
    		color: red; /* `&` can't represent `.card::before`, so this matches nothing */
    	}
    }

    Either hoist the element out to the top level.

    /* exmaple 2 */
    .card::before {
    	content: '';
    }
    
    .dark .card::before {
    	color: red;
    }

    Or restructure so & refers to the element and the pseudo-element is nested in it.

    /* exmaple 3 */
    .card {
    	&::before {
    		content: '';
    	}
    
    	.dark &::before {
    		color: red;
    	}
    }

    A pseudo-element in the parent selector list doesn’t poison the whole list. & drops just that entry and still represents the rest.

    /* exmaple 4 */
    .card,
    .card::before {
    	color: black;
    
    	.dark & {
    		color: red; /* matches `.card`, but not `.card::before` */
    	}
    }

    The nested rule desugars to .dark :is(.card, .card::before), and the forgiving list throws out the pseudo-element, leaving .dark :is(.card). So the element turns red while its ::before stays black.

    From Chrome 130 / Safari 18.2 / Firefox 132 and onwards, bare declarations nested inside an at-rule are the exception. They do apply to the pseudo-element.

    /* exmaple 5 */
    .card::before {
    	content: '';
    	color: black;
    
    	@media (prefers-color-scheme: dark) {
    		color: white; /* applies to `.card::before` */
    	}
    }

    Wrap those same declarations in & { ... } and they stop applying.

    /* exmaple 6 */
    .card::before {
    	content: '';
    	color: black;
    
    	@media (prefers-color-scheme: dark) {
    		& {
    			color: white; /* matches nothing */
    		}
    	}
    }

    So why does example 6 not work?

    The reason is that declarations can’t sit loose inside a nested at-rule as far as the CSSOM is concerned. A CSSMediaRule exposes rules, and declarations only live on a style rule, so the parser has to wrap them in something. Examples 5 and 6 differ in what that something is.

    Example 5 wraps them in a nested declarations rule:

    It matches the exact same elements and pseudo-elements as its parent style rule, with the same specificity behavior.

    Whereas example 6 wraps them in & { ... } and & obeys selector rules: it desugars to :is(.card::before), which can’t represent the pseudo-element, so nothing matches.

    Further reading:

  • Make It Work vs. Make It Good - Jim Nielsen’s Blog

    I love how Jim Nielsen articulates the difference between making it work and making it good. “Does it work?” is exactly the kind of signal an LLM can be trained against: the tests pass or they don’t. There’s no equivalent for “Is it good?”, is there?

    “Make it work” is the first 90% of the work. “Make it good” is the other 90%.

  • Accidental anonymity - macwright.com

    Tom MacWright on putting yourself out there and the fear of being found out for being imperfect:

    putting your art, writing, expression out to be judged by others is an act of bravery as much as talent, and a lot of people lack bravery. Sorry to say it but if you need your work to be polished and beyond reproach, that’s a determination and character problem, not a skill problem.

  • Note - Posted on

    Today I learned, through Sara Soueidan’s course, Practical Accessibility, that spectacles/eyeglasses are a form of assistive technology.

    I’ve worn glasses for most of my life, but I’ve never really thought of myself as someone who uses assistive technology.

    It made me realize how easily we can think of assistive technology as something that exists for “people with disabilities,” rather than something many of us may rely on ourselves.

  • Don't be a meat proxy

    Niklas Gruhn, on relaying AI output verbatim:

    By all means, prompt AI. But don’t just relay the output. Read it, understand it, validate it, and then write a response in your own words (a decent certificate that you’ve done the prior steps). Making that effort is value you can add.

    This reminds me of Simon Willison’s rule, I will not publish anything that takes someone else longer to read than it took me to write.

    Among friends I’ll sometimes skip the rewrite and share a link with the prompt in it instead. As Dave Rupert mentioned, he wants the prompt because it shows the context you gave the machine and your understanding of the problem: I’d rather have the original prompt so I can just ask the machine myself. It’s the least bad way to relay, not an alternative to doing the work.