Open Burzmalian opened 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?)
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!
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
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.
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?
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.
I agree that the primary use case for custom properties is for sharing values across components. A couple points:
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.
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.
Can we use custom properties for things like global color variables?