Elix components are designed to have a minimalist, themeable visual style. The generic default appearance is intended to be simple and clean so it can blend in with your application. You can extensively customize how the Elix components appear to achieve more distinctive visual effects, such as branding consistent with your application's design language. This page summarizes the basic customization techniques.
Slots
Most Elix elements accept content via their default slot. Some complex elements have multiple slots. E.g., Carousel
defines slots inside its left and right arrow buttons. You can fill those slots with custom arrow icons or other content:
<elix-carousel>
<div slot="arrowButtonLeft">↫</div>
<div slot="arrowButtonRight">↬</div>
<img src="image01.jpg" />
<img src="image02.jpg" />
<img src="image03.jpg" />
<img src="image04.jpg" />
<img src="image05.jpg" />
</elix-carousel>
The custom icons are shown inside the arrow buttons:
Styling element parts
Complex Elix elements like Carousel and Tabs have templates containing many internal subelements. Since those subelements reside in a Shadow DOM subtree, you cannot directly style them from your outside page. However, each Elix component exposes its key internal elements as CSS Shadow Parts. You can't directly manipulate those parts, but you can style them via the new CSS ::part
selector.
For example, the documentation for the Elix ExpandableSection component shows that instances of that component class expose parts such as the header
part across the top that the user can click/tap to expand or collapse the panel and the toggle
part on the right-hand side of that header that contains the default expand/collapse icons.
You can target a shadow part with the CSS ::part()
selector like this:
elix-expandable-section::part(toggle) {
background: pink;
border-radius: 50%;
border: 1px solid red;
}
This applies styling to the component's toggle
part that contains the default icons:
Section 1
Section 2
Section 3
Note: As of November 2019, the above demo requires a pre-release version of Chrome or Edge to completely style the carousel's internal parts. The demo will show partially-customized parts in production Chrome or pre-release versions of Firefox and Safari. The additional styling demos below also require you to enable experimental web platform features by opening chrome://flags/#enable-experimental-web-platform-features
.
This is a powerful technique for styling components. However, as noted above, support for CSS shadow parts (the new ::part
selector) and a related CSS feature called custom pseudo-classes (a new :state
selector) are not yet completely supported in production browsers. For that reasons, Elix provides other styling techniques like template patching (below).
All of the complex Elix components, such as CalendarMonth and Carousel can be customized using the ::part
syntax. (As noted above, these demos currently require pre-release browser features for the full effect.)
It's important to recognize that this type of styling allows you to customize the appearance of Elix components while preserving the reliable encapsulation that only web components can offer. Unless you explicitly use the ::part
syntax to affect the appearance of the component's parts, your page styling will not accidentally conflict with the component's internal styling. Moreover, a component's internal styling will not affect what happens elsewhere on your page.
Replaceable element parts
In addition to letting you style parts with the ::part
syntax, Elix components go a step further and allow you to replace the type of element that will be used for a given part.
For example, PlainCarousel has a key part called the stage
that focuses the user's attention on a single selected item (often an image). By default, that stage
part is an instance of a more fundamental Elix component, SlidingStage, which renders transitions between items with a sliding effect. A Carousel defines another set of parts identified by the name proxy
: those elements will be used to represent the items in the set at a smaller scale. By default, the proxy
parts are instances of PlainPageDot. A Carousel also defines arrow-button
parts for the left and right arrow buttons typically shown on a desktop carousel.
You can replace the types of those parts on individual carousels by setting attributes on the element in markup or on properties through JavaScript. You can specify a type with a descriptor that is either:
- A component class (
FooElement
) that will be instantiated to fill the role. - A string representing the tag name (
"foo-element"
) that will be instantiated to fill the role. - An
HTMLTemplateElement
that will be cloned to the fill the role.
Example: If you define your own custom elements for MyArrowButton
and MyPageDot
, you can indicate that a Carousel
instance should use those classes to create its internal parts. One way to do this would be to instantiate the Carousel
in markup and set attributes for arrow-button-part-type
and proxy-part-type
, passing in the tag names for your own custom elements:
<script type="module" src="node_modules/elix/define/Carousel.js"></script>
<script type="module" src="MyArrowButton.js"></script>
<script type="module" src="MyPageDot.js"></script>
<body>
<elix-carousel
arrow-button-part-type="my-arrow-button"
proxy-part-type="my-page-dot"
>
<img src="image1.jpg" />
<img src="image2.jpg" />
<img src="image3.jpg" />
</elix-carousel>
</body>
By specifying what standard or custom element should be used for that key subelements, you can provide arbitrary customizations of the Carousel's appearance and behavior.
Part types are dynamic, and can be changed at runtime.
You can also set or override part types on a class basis. If you want to create a custom carousel that always uses your custom arrow buttons and page dots, you can arrange for this in your constructor by setting the relevant types as default state:
import Carousel from "elix/define/Carousel.js";
import MyArrowButton from "MyArrowButton.js";
import MyPageDot from "MyPageDot.js";
class MyCarousel extends Carousel {
get [internal.defaultState]() {
return Object.assign(super[internal.defaultState], {
arrowButtonPartType: MyArrowButton,
proxyPartType: MyPageDot
});
}
}
The result has all the functionality of the base Carousel
, with the customized appearance of the arrows and page dots:
You can create some fairly unusual combinations of components with this role technique. For example, an AutoCompleteComboBox defines a list
part that presents the user with a set of choices. You can override that and specify that a Carousel
should be used to create that list
part::
This isn't to say that this particular combination is a great idea, but rather that such combinations are possible and largely work as expected. Here, the carousel's swiping behavior work as expected, as does the input area's auto-completion. The rigorous testing criteria for Elix components, including those in the Gold Standard Checklist for Web Components, help make such combinations work predictably.
See below for how to define replaceable element parts for your own components.
Template patching
Many Elix elements are specializations of other types of elements. Often such relationships are expressed in a class hierarchy. For example, a DropdownList is a specialized type of MenuButton, so DropdownList
is defined as a subclass as MenuButton
. MenuButton
is in turn is a special type of PopupSource, and again the former is defined as a subclass of the latter.
Such specialized classes often need to add additional elements to the template defined by their parent classes. A common Elix pattern is to have a component define its template by obtaining a template from its parent class and return some modified version of it.
This is generally done in a component's internal.template
property. The property implementation will ask for the super
template, perform some modifications, then return the result as its own template.
class CustomElement extends BaseElement {
get [internal.template]() {
const result = super[internal.template];
/* Perform modifications to the result here. */
return result;
}
}
The modifications often take advantage of helper functions in the template module, such as template.concat
in the example below.
Appending an additional stylesheet
One particularly common form of template patching is having a subclass append an additional stylesheet to the template defined by the base class. This has the benefit of simplicity, and ensures the subclass' desired styles can cleanly override styles defined by the base class — since the subclass's appended stylesheet comes after any stylesheet(s) defined by the base class.
Elix components perform this type of template patching using the helper function template.concat, which combines two or more HTML templates into a single template. The example below shows a custom subclass invoking template.concat
:
import * as template from "elix/src/template.js";
class BaseElement {
get [internal.template]() {
return template.html`
<style>
button { background: white; color: black; }
</style>
<button>Ok</button>
`;
}
}
class CustomElement extends BaseElement {
get [internal.template]() {
return template.concat(
super[internal.template],
template.html`
<style>
button { color: red; }
</style>
`
);
}
}
In this example, the resulting CustomElement
subclass will have both the template content defined by BaseElement
and the custom template content it adds through concat
. The resulting template for a CustomElement
instance will look like:
<style>
button {
background: white;
color: black;
}
</style>
<button>Ok</button>
<style>
button {
color: red;
}
</style>
As a result, the button
inside a CustomElement
will have a white background color and a red foreground color.
See the section below for a more sophisticated demo that uses template patching.
Overriding the render method
When the state of an Elix element changes, its internal.render method is invoked. This gives the component the opportunity to update the component's host element and its shadow elements to reflect the new state. This system allows for the appearance and behavior of an element to be collectively defined by the element class, its base classes, and any mixins applied to it.
You can tap into this system by subclassing an Elix element and overriding the render
method to set additional styles or properties on the element or its subelements.
The following demo uses template patching (above) and an overridden internal.render
method to extensively customize the appears of a DrawerWithGrip:
This approach allows greater flexibility than what can be achieved with CSS alone. For example, as the user drags out the drawer with a touch or trackpad swipe, the grip icon tracks the drawer swipe progress smoothly through its transition from a "+" icon to an "×" (close) icon.
Defining replaceable element parts
If you'd like to define replaceable parts (see above) for your own component, this can be done in your component's internal.render method.
For example, DropdownList defines a valuePartType
that lets developers using that component customize what kind of component should be used to render the component's currently-selected value.
class DropdownList extends ReactiveElement {
get [internal.defaultState]() {
return Object.assign(super[internal.defaultState], {
valuePartType: "div"
});
}
[internal.render](changed) {
super[internal.render](changed);
if (changed.valuePartType) {
template.transmute(
this[internal.ids].value,
this[internal.state].valuePartType
);
}
}
get valuePartType() {
return this[internal.state].valuePartType;
}
set valuePartType(valuePartType) {
this[internal.setState]({ valuePartType });
}
}
By default, the component uses a plain div
for the value role. But if you want to use the DropdownList
to show a list of colors with color swatches, you can set the valuePartType
property (or value-part-type
attribute) to a custom element that shows a color swatch next to the color name. Setting that property will update the component's internal state
. ReactiveMixin will then invoke the component's internal.render
method, passing in a changed
object where valuePartType
is true
. The DropdownList
render method then uses the transmute helper to replace the div
with an instance of your color swatch class:
value
part type
⇲
Reusing mixins
Sometimes you want to create a component that's substantially similar to an existing Elix element, but which is different enough that the techniques above are insufficient. In such cases, you may still be able to reuse much of the code for the Elix element in question by creating your own component from the same set of mixins.
The vast majority of the behavior for all Elix elements is defined by mixins. The documentation for each element will indicate what mixins it uses; inspecting the source code is obviously helpful as well. Having identified that set of mixins, you can apply that same set of mixins to a base class like HTMLElement
or ReactiveElement to create a fundamentally new component that nevertheless reuses a considerable degree of code. This allows you to both create components more quickly, and at a higher degree of quality.