Skip to content

[css-cascade] Atomic CSS #11142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
LeaVerou opened this issue Nov 1, 2024 · 7 comments
Open

[css-cascade] Atomic CSS #11142

LeaVerou opened this issue Nov 1, 2024 · 7 comments

Comments

@LeaVerou
Copy link
Member

LeaVerou commented Nov 1, 2024

The cascading nature of CSS is both a blessing and a curse. In many cases, it results in declarations that were meant to be applied together being applied partially, resulting in visual chaos, poor accessibility, or both.

In some ways, that is why @supports was invented: for cases where the poorly supported feature needed to be applied together with some well supported features. However, this is not just about browser support.

Consider cases where the background color is overridden but not the text color, or the font size but not the line height, or even things not directly related, such as the gap for a button group, but not the border-radius.
image

It seems to me it would help solve a host of use cases if authors could mark groups of declarations (and possibly even nested rules) as "atomic" (in the same sense as db transactions): if even a single declaration in the group is not applied (either because it was overridden or because something was not supported), then the entire group is no longer applied.

E.g.

button-group {
	@atomic {
		gap: 0;
		
		> button {
			border-radius: .3em;

			&:first-child {
				border-inline-start-radius: 0;
			}

			&:last-child {
				border-inline-end-radius: 0;
			}
		}
	}
}

Note that here I’m even setting the gap to its initial value, just to opt it in to the atomic-ness.

This would also simplify many @supports use cases:

h1 {
	color: var(--color-primary);

	@atomic {
		background: linear-gradient(...);
		background-clip: text;
		color: transparent;	
	}
}

instead of the current:

h1 {
	color: var(--color-primary);

	@supports (background-clip: text) {
		background: linear-gradient(...);
		background-clip: text;
		color: transparent;	
	}
}

My main worry is circularities: I can't yet see any, but I have an annoying niggling feeling that they may exist. Or strictly defining the concept of a property being overridden.

@romainmenke
Copy link
Member

I like this and could see myself using this!


Makes me think that checking for support could also have a shorter syntax for common cases.
A @support rule without prelude could implicitly check for support of all features used somewhere within that block.

h1 {
	color: var(--color-primary);

	@supports {
		background: linear-gradient(...);
		background-clip: text;
		color: transparent;	
	}
}

But your proposal goes beyond simply checking for support so I agree that it then also should be a separate at rule :)

@Loirooriol
Copy link
Contributor

Loirooriol commented Nov 2, 2024

if even a single declaration in the group is not applied (either because it was overridden or because something was not supported), then the entire group is no longer applied

I think this is too restrictive in practice, authors will want some kind of OR for parts of the atomic, because it's typical that some old browser only supports a property with a vendor prefix, while other browsers may only support the standard property without any prefix. Similar for pseudo-elements. Then as long some of the options works, the atomic shouldn't be rolled back.

@astearns
Copy link
Member

astearns commented Nov 2, 2024

I think the @supports interaction makes sense but is less interesting than the idea of tracking which declarations in the @atomic block get applied. I’d like to get some implementer feedback on the feasibility of doing this in current engines.

@andruud
Copy link
Member

andruud commented Nov 5, 2024

I’d like to get some implementer feedback on the feasibility of doing this [tracking applied decls] in current engines.

Generally, this sounds hard and expensive. However:

If instead of "applied", we track which atomic declarations lose the cascade, then it might be feasible. During cascading, we could make a note whenever an atomic declaration got overwritten by something, and use this in the apply step to filter out other declarations from the same @atomic rule.

Note that a revert-layer (and revert) declaration would need to count as overwriting a declaration. Example: the declarations within @atomic are not applied in this case, because --x: 1 was overwritten by something:

@layer {
  @atomic {
    --x: 1;
    --y: 1;
  }
}

@layer {
  --x: revert-layer;
}

This is because revert-layer (and revert) are computed-value time effects, despite what it sounds like in current specs. This follows from the requirement that var(--unknown, revert-layer) behaves as revert-layer, which can't be known until substitution (AKA computed-value time).

My main worry is circularities: I can't yet see any, but I have an annoying niggling feeling that they may exist.

I think with the above behavior, the worst that can happen is all rules in the "cycle" end up not applying. Example from @sesse (which would be circular, if we spec @atomic hazardously):

@atomic {
    div {
        color: red !important;
        background-color: white; /* loses */
    }
}
@atomic {
    div {
        color: white; /* loses */
        background-color: black !important;
    }
}

Since both @atomic rules have losses, neither are applied.

@LeaVerou
Copy link
Member Author

LeaVerou commented Nov 5, 2024

@andruud Is the difference between the proposal and what you’re suggesting that even if you override a declaration with the same value, it still counts as overridden? Or are there other cases where it would behave differently?

@andruud
Copy link
Member

andruud commented Nov 5, 2024

@LeaVerou Not sure, it depends on exactly which criteria you had in mind for when the declarations in @atomic should apply. It's possible that I just restated the same thing you were trying to say. :-)

@LeaVerou
Copy link
Member Author

LeaVerou commented Nov 5, 2024

@andruud I think for it to fulfill its intended purpose, it would basically need to consider overrides that just apply the same value as not overridden, which then a) introduces circularities and b) opens the huge can of worms of what values do you consider equivalent? Presumably by comparing computed/used value serialization, but other features that required that have been stuck in limbo because it’s too hard in the general case.

I’ll have a think about what kind of restriction around what groups can be defined would help eliminate the circularities without crippling use cases too much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants