linkedin / css-blocks

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

CSS Custom Properties #150

Open Burzmalian opened 6 years ago

Burzmalian commented 6 years ago

Can we use custom properties for things like global color variables?

amiller-gh commented 6 years ago

Yes you can! Right now CSS Blocks' compilation and property conflict resolution will correctly handle custom props and leave them alone in the final build 👍 Because the guarantee of CSS Blocks / OptiCSS is, at the end of the day, our built css output will preserve the cascade, custom properties used in your application will #justwork.

CSS Blocks does not currently have a concept of a "reset" stylesheet where you can declare global, app-wide styles, but you can easily concatenate another stylesheet with Blocks' output at the end of your build to the same effect. This is actually exactly what we do with the CSS Blocks website, where we deliver global color variables. Just try to keep the specificity of these selectors below single class name specificity to avoid unexpected behavior. We may want to consider adding more robust support for this kind of pattern...

We have begun discussing what the expected behavior of custom properties are once enable Block files to be delivered / imported from node_modules over in this ticket: https://github.com/linkedin/css-blocks/issues/46. We've been toying with the idea of "application scopes" where we would ensure custom properties will not leak out of, but it is still in an early design phase and will probably be developed in tandem with our node_modules strategy. I think we want to get a write up of the proposal posted here soon (right @chriseppstein?)

benkingcode commented 6 years ago

Just a point on the scoping of Custom Properties, we have private npm packages for shared UI components across our applications for things such as buttons, and their colours are determined by the app-level branding Custom Properties. So, we would definitely need a way to opt out of scoping!

madeleineostoja commented 6 years ago

Just another :-1: against automatic scoping of custom props (if I've understood the proposal properly). IMO the biggest use case for them is global shared constants like colors, typography, etc. Even for imported modules it's not uncommon to expose css vars as styling hooks to be used by the global scope.

Transforming and requiring opt-out for that use seems the wrong way around. Internal component vars are a DX thing, and could be covered with a custom construct or postcss plugin or something if you don't want to worry about explicit naming conflicts

chriseppstein commented 6 years ago

We won't make it hard to use css custom properties that are intended to be shared for use across components. But we do want to make it hard for custom properties to conflict unintentionally.

I'd love to hear some ideas for how we can meet both of those competing requirements without making it onerous to do either one. My general sense is that making custom properties private by default is the right thing to do, and that when they are exposed (probably via @block-global), they should be referenced to a source definition so that there's a dependency graph that tells us that two custom properties are the same. CSS Blocks and OptiCSS can then work together to optimize those identifiers, and ensure that conflicts are well considered.

Since custom properties can be set by JS, we'd also need a way to make sure such properties weren't mangled.

But, I'll be honest, I don't see this as a high priority item. It's unlikely to be a feature we have when 1.0 launches.

benkingcode commented 6 years ago

I feel like the use case of custom properties is much more likely to be for sharing variables across components than to be isolated though. I can't really think of cases where isolating them would be ideal, surely then they lose most of their benefit?

madeleineostoja commented 6 years ago

Agree with @dbbk. Custom properties are global by nature, and I would much prefer to have some kind of opt-in scoping mechanism for when/if I want to ensure that they are private. I wouldn't call wrapping up private props in something onerous, especially considering that's the edge case vs globals. And, again, you can always just use a postcss plugin or something for privately scoped vars. That's really not the benefit of CSS custom props.

I'm coming from a web components background, and even shadow dom doesn't encapsulate custom props, they're considered part of a component's public API to hook into. This feels like something that CSS blocks shouldnt be trying to be clever about, and just let me as the author deal with. Otherwise I can see custom props being a pain to use with CSS blocks for no real gain.

chriseppstein commented 6 years ago

I agree that the primary use case for custom properties is for sharing values across components. A couple points:

  1. A CSS Block is not isolated to a single component. Blocks are intended to be re-used across components. In this way, the isolation boundary of a block is in the dimension of a design concept, not a functional concept. A component has it's own design concepts so there is usually going to be a single block for each component, but also, there should be blocks that are shared and reused. One of the ways those blocks are reused is by letting sub-components reference classes and states of those classes within the document subtree where the root of that block was first assigned. Because custom properties are inherited in that same context, creating isolation of a custom property within a block may not be as onerous or useless as you are imagining.
  2. A core use case for CSS Blocks is to allow large teams to avoid stepping on each others toes and to allow for the safe distribution of styles from a style library (E.g. a design system) to applications. CSS Blocks creates isolation of class names which ultimately reside in the global scope. I think everyone agrees this is a good idea. The same risk of custom properties colliding in the global namespace exists. Because custom properties inherit, CSS Blocks won't be able to detect collisions and force a resolution via same-element assignment. But we can still create a namespace so that referencing a custom property is unambiguous -- this would be no more inconvenient than referencing a global state from another block.

I think a code example may elucidate this (I'm spitballing here):

/* themes.block.css */
/* expose a custom property for setting and referencing in other blocks. */
@block-global var(--primary-color), var(--highlight-color);

/* declaring a custom property as external would signal that it's set via js or
   a non-block stylesheet and cause it to be left alone. This would mark that
   property as global as well. */
@external var(--icon);

:scope[state|theme=blue] {
  --primary-color: lightblue;
  --highlight-color: blue;
  --icon: url(https://ourapp.com/icons/bluetheme.png);
}

.element[state|bg] {
  background-color: var(--primary-color);
}
.element[state|text] {
  color: var(--highlight-color);
}

.icon {
  background-image: var(--icon);
}
/* my-component.block.css */
@block-reference themes from "./themes.block.css";

/* this demonstrates how you'd reference a custom property from outside a block */
themes[state|theme=blue] :scope {
  /* the selector scope in this case seems sufficient to scope the variable having been
     assigned in scoped selector. The variable would be validated against locally defined
     variables and those defined as global in the referenced blocks.
     in this way, the best we can do is give an error if several blocks expose the same
     variable as global. */
  border-color: var(--highlight-color); 
}
/* There needs to be a way to override a variable */
themes[state|theme=blue] :scope {
  /* by default you'd be setting the variable exposed as global in a referenced block.
     if that var is removed or there's a typo, this would be come a block-scoped variable. */
  --icon: url(https://ourapp.com/premium-icons/bluetheme.png);
  /* this syntax is scoped so we'd be able to validate it exists but it's illegal syntax so
     probably a non-starter */
  --themes.icon: url(https://ourapp.com/premium-icons/bluetheme.png);
}
// my-component.jsx
import objstr from "obj-str";
import styles from "./my-component.block.css"
import themes from "./themes.block.css";

function myComponent(props) {
  let rootStyles = objstr({
    [styles]: true,
    [themes.element]: true,
    [themes.element.bg()]: true,
  });
  return <div className={rootStyles}>
    <div className="themes.icon">
  </div>;
}

I think the above code allows variables to be scoped and predictable, it doesn't make variables work differently, but allows css-blocks and opticss to work together to prevent accidental collisions as well as give errors where collisions are detected and cannot be resolved otherwise.

kgcreative commented 6 years ago

So, originally I was going to say that we needed something like SCSS variables in order for us to be able to use static placeholder values for things, but the more I think about it, the more I realized this caused some bad habits in CSS that resulted in hundreds of extra lines of code, where now we don't really need them anymore.

In vanilla CSS, custom properties are by default locally scoped to the selector/inheritance of where they are generated, or they can be globally scoped as long as they are instanced as part of the:root {} pseudo-selector (:root is equialent to html, but has a higher specificity). (See this great article by Una: https://una.im/local-css-vars/)

In the case of css blocks, I think as long as we don't namespace the custom properties, this can be resolved by inheritance.

In this case, the values of any custom properties instanced inside the :scope block would be scoped to the same specificity as the :scope block, and would override the global context, which is vanilla CSS behavior.

The really counter-intuitive thing about this, coming from somewhere like sass, is that all you need is to update the custom property definition, You don't need to redeclare the properties when you want to use them.

I made a quick demo here: https://codepen.io/kgcreative/pen/XqLvPw

Because SCSS variables are compiled static placeholders, we have gotten used to redeclaring properties any time we need to use a different value (Say a theme, or a breakpoint), but once we realize that changing the custom property definition applies the custom property where it's defined, then it opens up a lot of really cool possibilities.

Why?

Because it lets us do things like default themes, and override themes using cascading variable values, without re-declaring all the properties per breakpoint or per class.

I've made a demo of this here well: https://codepen.io/kgcreative/pen/OZKLRZ;

How does this all relate back to CSS Blocks?

as long as the custom properties are declared in their correct scope contexts, then the cascade should be able to handle any collisions properly.

If anything, there might need for a method to set :root{} scoped custom properties, or they could be handled via a concatenated global definition file.