linkedin / css-blocks

High performance, maintainable stylesheets.
http://css-blocks.com/
BSD 2-Clause "Simplified" License
6.34k stars 152 forks source link

Automatic CSS Variable Scoping #46

Open amiller-gh opened 6 years ago

amiller-gh commented 6 years ago

Overview

Currently, CSS variables are treated just like any other property in blocks. This means that conflicting variable names are expected to be explicitly resolved by the developer, but we can do better than that!

Problem

CSS variable can be given special treatment in blocks. Consider two blocks a and b:

/* a.block.css */
:scope {
  --my-var: red;
}
.class {
  color: var(--my-var);
}
/* b.block.css */
:scope {
  --my-var: blue;
}
.class {
  color: var(--my-var);
}

If both blocks are applied to the same element, their --my-var definitions will conflict, causing unexpected behavior and requiring explicit resolution.

Now consider two blocks base and extended:

/* base.block.css */
:scope {
  --theme-color: red;
}
.class {
  color: var(--theme-color);
}
/* extended.block.css */
@block-reference base from "base.block.css";
:scope {
  extends: base;
  --theme-color: blue;
  --local-var: red;
}
.bar {
  color: var(--local-var);
}

Here, the exception is elements with block extended applied will use the re-defined --my-var class for all inherited styles.

Proposal

Compiled blocks should rewrite variable names to unique identifiers. This is easily done by prefixing all vars with their uid:

/* a.block.css */
.a {
  --a-my-var: red;
}
.a__class {
  color: var(--a-my-var);
}
/* b.block.css */
.b {
  --b-my-var: blue;
}
.b__class {
  color: var(--b-my-var);
}

In blocks that extend a base block, conflicting css var names should inherit the name of their base block, while locally defined names should be prefixed with the local uid. So our extended block example becomes:

/* base.block.css */
.base {
  --base-theme-color: red;
}
.base__class {
  color: var(--base-theme-color);
}
/* extended.block.css */
.extended {
  --base-theme-color: blue;
  --extended-local-var: red;
}
.extended__bar {
  color: var(--extended-local-var);
}
chriseppstein commented 6 years ago

It's really not clear to me that css custom properties should be scoped to the block.

The concept of a block-scoped css custom property is interesting, but I'm not sure that it should be the default behavior.

amiller-gh commented 6 years ago

I actually think that custom properties should 100% be scoped to the block! If not, we have the potential to run in to some serious issues once we spit out the final css output.

Source:

/* block-1.css */
:scope {
  --my-var: red;
}
.class {
  color: var(--my-var);
}
/* block-2.css */
:scope {
  --my-var: blue;
}
.class {
  color: var(--my-var);
}
<div class="block-1">
  <div class="block-2">
    <div class="block-1.class">I should be red...but I'm blue!</div>
    <div class="block-2.class">I should be blue...and I am!</div>
  </div>
</div>

Compiled

/* block-1.css */
.block-1 {
  --my-var: red;
}
.block-1__class {
  color: var(--my-var);
}

/* block-2.css */
.block-2 {
  --my-var: blue;
}
.block-2__class {
  color: var(--my-var);
}
<div class="block-1">
  <div class="block-2">
    <div class="block-1__class">I should be red...but I'm blue!</div>
    <div class="block-2__class">I should be blue...and I am!</div>
  </div>
</div>

Because we treat every block stylesheet as its unique scope, we also can't guarantee concatenation order either, so its possible that a change in how we compile Block files down to a single CSS file will change the order of your block class concatenations, changing the styles applied in browser. We don't want this non-determinism.

amiller-gh commented 6 years ago

We still need to think through the inheritance and block-global use cases, if scoping makes sense for them, and if not what kind of access do children get to these otherwise private values.

chriseppstein commented 6 years ago

Concatenation order doesn't matter for css variables in any special way. They are properties and our resolution system for properties handles this.

I also agree that we need to think through the use cases of where a css variable is locally scoped and when it is globally scoped. I think I buy into the notion that css variables should be block-level scoped by default, but the goal of variables is often to share values across an application's css files so it should be easy to do so. Your example above with <div class="block-1.class">I should be red...but I'm blue!</div> makes the assumption that those two blocks intend for the two definitions of --my-var to be completely distinct it's not clear to me that they do. I think both use cases exist and are valid. 🤔

amiller-gh commented 6 years ago

Concatenation order does matter when the selectors that define the css variables have the same specificity – like the selectors that CSS Blocks emits. And conflict resolution resolves this for selectors applied to the same element, but the problem really presents itself when you have components with outlets where the parent component's markup appears inside the scope of another's (like the above example).

I agree, CSS vars are very helpful in the context of an application, but specifically when a block is delivered by a node module (or maybe an in-repo an Ember engine?...) it could be disastrous if custom prop names clash.

This will probably be a concern we need to address in conjunction with #112 and #97 as we figure out what a "namespace" means in CSS Blocks' world.

chriseppstein commented 6 years ago

@amiller-gh Let's talk. I think you're mistaken on this.

kgcreative commented 6 years ago

Whether custom properties should be scoped to the block or not, depends on whether we are thinking of custom properties within the context of SCSS variables, which allows us to statically define a component and control it's final output via css definitions, or whether we want to define that component dynamically, and potentially interact with it in javascript.

I like generating use cases, because they allow me to talk more concretely about concepts, and because they let me validate assumptions about how things work in real scenarios.

As part of this discovery, I think I may have found a CSS custom properties implementation bug. (Or maybe you guys can help me either validate why this works the way it does, or figure out the intended usage here)

So, i'm starting with a base component that contains a header, a footer, and a content block. Scoped properly and following BEM naming conventions, CSS custom properties work pretty nicely out of the box, and keep the amount of duplicated code to a minimum.

https://codepen.io/kgcreative/pen/ELqbOE

In your example above, there were some naming issues, so I also took your example and extended it to another codepen, as well as changed how the properties were being called a bit. This solved the collision issues you were having, while keeping the names tidy.

https://codepen.io/kgcreative/pen/MGNWpJ

In your HTML example above... you forgot to scope the block-1 and block-2 elements per BEM conventions. So, instead of:

<div class="block-1">
  <div class="block-2">
    <div class="block-1__class">I should be red...but I'm blue!</div>
    <div class="block-2__class">I should be blue...and I am!</div>
  </div>
</div>

it should be:

<div class="block-1">
  <div class="block-2">
    <div class="block-1 block-1__class">I should be red...</div>
    <div class="block-2 block-2__class">I should be blue...</div>
  </div>
</div>

that resolves the collisions.

-- (edit:) In other words, I don't think we need to scope variable names, because they scope to the selector they were instanced in.

brandonkal commented 5 years ago

I believe css variables should not be scoped to the block. They provide a nice way to theme components.