-
Notifications
You must be signed in to change notification settings - Fork 61
Description
WIP
This proposal tries to unify the way selector API is defined in stylable. It's replacing previous st-part proposal for a broader solution aims to make the syntax simpler while adding capabilities that have been long requested and required for future experience we talked about.
Goals
- Allow deep pseudo-element definition under the same stylesheet
- Allow multiple components in a single stylesheet
- A way forward to define native CSS/DOM APIs (e.g. custom elements and attributes)
- Unified syntax
Base Proposal
Single at-rule directive to enrich CSS definitions.
Define a Component
/* mark the .btn class as a component */
@st .btn;
/* or more verbosely */
@st-comp .btn;Define Pseudo States
Nested pseudo-states are defined as nested @st following a pseudo-class syntax (colon+name).
To set a non boolean state the name can be followed with parentheses with type definition similar to how states are defined today.
@st .btn {
/* boolean state */
@st :toggled;
/* enum state */
@st :size(enum(small, medium, large));
}Equivalent today
/* btn.st.css */
.root {
-st-states:
toggled,
size(enum(small, medium, large));
}usage
/* selector api usage */
.btn:toggled {}
.btn:size(small) {}Define Pseudo Elements
Nested pseudo-elements are defined as nested @st following a pseudo-element syntax (double colon+name) with fat-arrow mapping them to a selector a relative selector list.
@st .gallery {
@st ::navBtn => .btn;
@st ::multi => nav > :is(.a, .b);
@st ::compound => &.x;
@st ::compoundMulti => &:is(.x, .y);
}Equivalent today
/* gallery.st.css */
.root {}
@custom-selector :--navBtn .btn;
@custom-selector :--multi nav > .a, .b;
@custom-selector :--compound .root.x;
@custom-selector :--compoundMulti .root.x, .root.y;Usage
.gallery::navBtn {}
.gallery::multi {}
.gallery::compound {}
.gallery::compoundMulti {}
/* transforms to*/
.ns__gallery .ns__btn {}
.ns__gallery :is(nav > .ns__a, .ns__b) {}
.ns__gallery.ns__x {}
.ns__gallery:is(.ns__x, .ns__y) {}
Notice that mapping selectors can either be compounding to the component or relative to it, but not mixed. This is a limitation of selectors. To allow combination of such cases, we would need to transform into multiple selectors and would loose the unified specificity that is achieved by grouping the selectors inMultiple selector mapping is not allowed, to achieve this, the multiple selectors can be written into:is().:is()/:where()/:has()
Map to Class (syntactic sugar)
In most cases components need to map parts to simple classes. For this, this proposal offers to allow definition of pseudo-element with a nested @st following a class name.
@st .gallery {
@st .navBtn;
/* equivalent to */
@st ::navBtn => .navBtn;
}There is some negative feedback about this syntax - we might want to find an alternative
Equivalent today
/* gallery.st.css */
.root {}
.navBtn {}Deep Structure
Nested definitions or parts and states can help define deep API. This is a new capability that the current syntax doesn't allow without splitting into multiple stylesheets.
@st .gallery {
@st .navBtn {
@st :position(enum(first, middle, last));
@st .label;
}
}
/* selector api usage */
.gallery:navBtn::label {}
.gallery:navBtn::position(first) {}Inheritance
In order to reuse definitions, a component or an inner part can extend a known definition.
@st .hasX {
@st .x;
}
/* extend .comp with .hasX */
@st .comp:is(.hasX) {
/* extend .part with .hasX */
@st .part:is(.hasX);
}Equivalent today
/* comp.st.css */
.root {
-st-extends: hasX;
}
.part {
-st-extends: hasX;
}Usage
/* selector api usage */
.comp::x {}
.comp::part::x {}Inheritance hide
Inherited states and parts can be blocked from usage with @st-hide
@st .base {
@st .a;
@st .b;
}
/* extends .comp with .base */
@st .comp:is(.base) {
/* block ::a from being used as a selector*/
@st-hide ::a;
}Inheritance override
Inherited states and parts can be overridden by re-defining them. While override like this is possible with the current syntax, we probably want to add explicit "override" syntax to show intent and prevent unintended API overlap.
@st .base {
@st .a;
@st .b;
}
/* extends .comp with .base */
@st .comp:is(.base) {
/* explicit override */
@st-override ::a => .x;
/* extends ::part with .base */
@st .part:is(.base) {
/* explicit override */
@st-override ::b => .y;
}
}Equivalent today
/* comp.st.css */
/* definition with parts must be defined in a separate stylesheet */
@st-import Base from './base.st.css';
.root {
-st-extends: Base;
}
/* override part and map to selector */
@custom-selector :--a .x;
.part {
-st-extends: Base;
/* override of part API is not possible in the same stylesheet
In order to get a similar API, another stylesheet is required */
}Usage
.comp::a {}
.comp::b {}
.comp::part::a {}
.comp::part::b {}
/* transforms to*/
.ns__comp .ns__x {}
.ns__comp .ns__b {}
.ns__comp .ns__part .ns__a {}
.ns__comp .ns__part .ns__y {}Global
To escape out of namespacing, for targeting non-stylable CSS, mapping to global namespace can be used.
/* target external component */
@st .comp => :global(.lib__comp) {
/* map state to template like today */
@st :state('[data-$0]', enum(checked, unchecked, indeterminate));
/* POTENTIAL ALTERNATIVE */
@st :state(enum(checked, unchecked, indeterminate)) => "[data-$0]";
/* define part that is transformed to global class */
@st ::part => :global(.lib__part);
}Equivalent today
/* lib-comp.st.css */
.root {
-st-global: '.lib__comp';
-st-states: state('[data-$0]', enum(checked, unchecked, indeterminate));
}
@custom-selector :--part :global(.lib__part);Usage
.comp:state(checked)::part {}
/* transforms to*/
.lib__comp[data=checked] .lib__part {}Base styling
While Stylable encourage separation of component interface and style, some cases might want to include styling because they are part of the base look/structure. So styles nested within @st definitions are allowed and preserved in transpilation.
@st .comp {
/* style comp */
color: red;
@st .part {
/* style part*/
color: green;
}
/* style nested part */
&:hover {
color: blue;
}
}
/* transforms to*/
.ns__comp {
color: red;
.ns__part {
color: green;
}
&:hover {
color: blue;
}
}Ambient root
This change allows us to define multiple components in a single stylesheet, and makes the default .root class unnecessary.
In addition it opens up the possibility for components that have different parts that aren't nested under a single root, like a tooltip with the anchor part and the popup part.
/* tooltip.st.css*/
@st .anchor {}
@st .popup {}@st-import Tooltip from './tooltip.st.css';
Tooltip::anchor {}
Tooltip::popup {}
/* transforms to*/
.tooltip__anchor {}
.tooltip__popup {}Open questions / considerations
Base styling specificity
Any styling set within the @st definition is considered as base style, and should be easily overridden by customization, however the deep structure causes the styles to gain specificity that can be hard to override.
There are 2 possible options that we are currently considering:
- wrap transformed selectors with
:whereto minimize base styling specificity - wrap components definition in
@layerto isolate it from any higher level component
A layer can always be added manually by a user (and is also less supported atm), and the where can be optional with some added configuration.
Base styling inheritance strategy
When setting nested styles within the @st definition, does pseudo selectors refer to the defined component or the extended definition?
Option 1 - refer to the extended definition:
/* assuming .base also has "::part" */
@st .comp:is(.base) {
&::part {
/* style base part */
color: red;
}
@st-override .part {
/* style comp part */
color: blue;
}
}Option 2 - nesting parts refer to extended until override (order matters):
/* assuming .base also has "::part" */
@st .comp:is(.base) {
&::part {
/* style base part */
color: red;
}
@st-override .part {
/* style comp part */
color: blue;
}
&::part {
/* style comp! part */
color: green;
}
}@st in @media?
If base styling is allowed, is there a way to define @st parts and states within media/conditional rules?
@st .comp {
@st ::part;
@media (height > 600px) {
/* is this allowed? does it conflict with another definition? */
@st ::part;
}
}Export classes with "." prefix
We wanted to change the way @st-import works to have classes prefixed with . for a while and it might be a good chance to change the mode when opting-in to use this new mode. This will remove the ambiguity with imported elements and collisions with other symbols that cannot start with a dot.
@st-import [.class] from '.some.st.css';
/* instead of */
@st-import [class] from '.some.st.css';This change can be made in 2 places:
- in the module itself that opts-in to use the new
@stat-rule in modules import statements from the opt-in stylesheet
The first is more self contained and the second one will probably prevent some stylesheets from migrating to the new syntax, so their consumers won't have to change anything.
Metadata
Metadata
Labels
Type
Projects
Status