Customizing elements

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:

Mountain lake Terraced farm Winter trees Forest river Red panda
Demo: Carousel with custom arrow button icons

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

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed molestie molestie enim porta dapibus. Phasellus dolor quam, egestas eu viverra at, porttitor in diam. Donec risus tellus, accumsan eget ipsum sed, vestibulum blandit ante. Nullam rhoncus leo nec lobortis convallis. Donec posuere tellus a nibh dignissim, rhoncus viverra neque rutrum. Suspendisse rutrum at massa vitae venenatis. Suspendisse ut risus pellentesque lacus dictum aliquet. Cras a arcu id odio molestie imperdiet.

Section 2

Pellentesque vitae eros ac nulla aliquam eleifend. Nunc ornare sollicitudin arcu id suscipit. Donec sed nisl libero. Nulla facilisi. Proin ornare feugiat molestie. Mauris velit mi, volutpat sit amet posuere quis, tristique et urna. Donec sit amet tellus magna. Aenean feugiat suscipit neque, ut porttitor diam auctor in. Sed faucibus finibus ipsum et pharetra. In hac habitasse platea dictumst. Cras facilisis justo eu lectus luctus, et interdum velit aliquet.

Section 3

Aliquam vitae nulla efficitur turpis viverra placerat. Mauris fermentum tellus vel elementum aliquet. Integer vitae arcu et mi tristique lacinia. Cras placerat ultrices velit, id interdum ipsum commodo efficitur. Maecenas maximus odio a nisi dapibus, non dapibus nisl venenatis. Morbi tristique interdum leo, non tincidunt sapien efficitur ac. Nunc hendrerit turpis eget enim rhoncus sagittis. Aenean ac euismod magna. Phasellus et posuere nisi.
Demo: ExpandableSection with styling applied to internal parts

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.)

Demo: CalendarMonth with CSS part styling
White bunny Close-up of face of brown bunny Fluffy bunny in lawn enclosure Brown bunny Bunny on grass eating a flower
Demo: Carousel with CSS part styling

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:

Mountain lake Terraced farm Winter trees Forest river Red panda
Demo: Carousel with custom arrow buttons 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::

Mountain lake Terraced farm Winter trees Forest river Red panda
Demo: A combo box with a carousel inside it

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 page has a drawer with custom styling. Among other things, the rotation applied to the "+" button tracks the current position of the drawer during a touch or trackpad swipe.

Drawer elements go here...
Demo: Extensively customized 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:

Black Blue Green Orange Purple Red White Yellow
Demo: DropdownList with a custom element for the 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.