ember-component-css
ember-component-css copied to clipboard
Request: guide for extensible component CSS
Background
Design and document for inheritance or else prohibit it. -Joshua Bloch, Effective Java
There are many cases where I have a component with some internal structure:
{{!-- my-component.hbs, outer HTML --}}
<div class='{{styleNamespace}}'>
<div class='{{styleNamespace}}__sub-component'></div>
</div>
The SCSS for that might look like this:
// my-component.scss
&__sub-component {
background-color: white;
color: rebeccapurple;
}
Those class names are generated and not available to a calling component. That is, if the author of my-component didn't anticipate a use-case (bolder, grayer, rotated, or capitalized), then there's no way for consumers of my-component to extend it to get the effect they want.
There are a few standard practices for adding customization affordances:
Passed-In Class
The author of my-component accepts a class and adds it to the sub-component:
{{!-- my-component.hbs, outer HTML --}}
<div class='{{styleNamespace}}'>
<div class='{{styleNamespace}}__sub-component {{subComponentClass}}'></div>
</div>
<div class='some-special-context'>
{{my-component subComponentClass='sub-component-override'}}
</div>
This lets the caller add any arbitrary customizations:
// caller.scss
.some-special-context {
.sub-component-override {
background-color: azure;
font-weight: bold;
border-radius: 3px;
transform: rotate(10deg);
}
}
One benefit of this practice is that it lets callers pass in functional CSS classes:
{{my-component subComponentClass='font--large flex--1 color--cornflowerblue'}}
CSS Variables
NB: this currently doesn't work with ember-component-css. See #48 and #282.
The author of my-component defines (some of) its styles using CSS variables. This allows consumers to customize specific aspects of the sub-component, but not add arbitrary CSS.
// my-component.scss
:root {
--my-component-sub-component-background: white;
--my-component-sub-component-color: rebeccapurple;
}
&__sub-component {
background-color: var(--my-component-sub-component-background);
color: var(--my-component-sub-component-color);
}
The caller can customize those variables, but not anything else:
// caller.scss
.some-special-context {
--my-component-sub-component-background: coral;
--my-component-sub-component-color: darkolivegreen;
}
.some-special-context appears within the DOM between :root and &__sub-component so its variables override those from :root. Note that it wouldn't work to define those variables on & in my-component.scss because that element is within .some-special-context.
Static Classes
The author of my-component adds classes that don't depend on {{styleNamespace}} to each of the sub-components. These classes are only for customization and not used by the component itself.
{{!-- my-component.hbs, outer HTML --}}
<div class='{{styleNamespace}} my-component'>
<div class='{{styleNamespace}}__sub-component my-component__sub-component'></div>
</div>
// my-component.scss
&__sub-component {
background-color: white;
color: rebeccapurple;
}
// .my-component and .my-component__sub-component don't appear in this file
Calling contexts target the "for-specialization" classes with regular CSS rules.
Static Attributes
In order to more clearly show that this is a hook for extensibility, we can turn the classes into attributes. For example,
{{!-- foo.hbs, outer HTML --}}
<div class='{{styleNamespace}}'>
<div class='{{styleNamespace}}__sub-component' data-subcomponent='foo:bar'></div>
</div>
// calling CSS:
& {
[data-subcomponent="foo:bar"] {
color: chartreuse;
background-color: magenta;
}
}
Summary / Request
I'd love to hear about people's experiences with these different techniques. Do you have other ideas? What's a good medium for describing best practices for extensible component-css?
I think i need a like bit of clearification. So are you saying that some addon author gives you access to “my-component” you have no way to customize those styles directly? You would have to do something with in a parent component or at the root level.. Am i reading that right? @jamesarosen
If that is your use case. One thing that I do when dealing with customizing addon component styles, is just create a “my-component/styles.EXT” in my app directory, and customize the styles i want. This will get the same namespace that the addon has, and due to the app styles getting included after the vendor styles on the page, will overwrite the addon styles just due to css using the last declared thing.
It’s not as.. direct, but the times i have used it is just when tweaking one or two things do it hasn’t been an issue..
But maybe this is irrelevant due to your use case being different.
I think i need a like bit of clearification. So are you saying that some addon author gives you access to “my-component” you have no way to customize those styles directly? You would have to do something with in a parent component or at the root level.. Am i reading that right?
Yes. Imagine the following scenario: ember-power-select uses ember-component-css to define its styles.
Its generated markup looks like
<div class='__0f5e3a'>
Select…
<div class='__0f5e3a__icon'></div>
<div>
And its CSS looks like
// .ember-power-select-trigger
.__0f5e3a {
padding: 0 2rem 0 .5rem!important;
background-color: transparent;
border: 2px solid #fff2f2!important;
border-radius: 4px;
line-height: 1.6;
white-space: nowrap;
outline: 0;
}
// .ember-power-select-status-icon
.__0f5e3a__icon {
border-width: 12px 7px 0;
border-color: #fff transparent transparent;
}
Now in my app, I want to style those elements. The first one is easy since it's a top-level element:
{{ember-power-select class='my-custom-power-select'}}
.my-custom-power-select {
background-color: blue;
border-radius: 6px;
color: white;
}
But I have no way of targeting the icon within.
just create a “my-component/styles.EXT”
Are you saying I can do
// my-app/app/styles/components/ember-power-select.scss
&__icon {
...
}
and it will be guaranteed to get the same class (__0f5e3a__icon) as the addon's version, which got compiled into vendor.css?
@jamesarosen yes. The 0f5e3a is a md5 hash of the name of the component so it will be consistent across vendor and your app css. you should be able to do that.