Styling and Themes - IgniteUI/igniteui-webcomponents GitHub Wiki

Preface

This documentation is intended for users/developers who want to contribute new components/themes. It could, however, be useful to know how we write and bundle the CSS rules and themes so that any user is welcome to read this document.

Ignite UI for Web Components ships with 4 themes - Bootstrap, Material, Fluent, and Indigo. A theme is a collection of style rules that use some globally defined custom CSS properties (CSS variables) to give the components some personality based on the currently selected theme. Those CSS variables are declared in the global theme files and are responsible for delivering the colors, shadows, typography, and some other global settings such as roundness, spacing, etc.

The nature of custom web elements(web components) is such that a component is a self-contained unit that includes all of its Business Logic(JavaScript), Styles(CSS), and Markup(HTML) in a single module. As such all styles related to a specific component need to ship with the component itself. This means that if a component has several themes, those themes will have to be bundled with the component itself.

Adding styles to a component

Since Ignite UI for Web Components is built with Lit, standard practices for defining styles apply.

To add a base stylesheet create a file in the component's folder named test-component.base.scss. This will trigger the creation of a new file in the same folder called test-component.base.css.ts, which can be imported into the component's typescript file.

// test-component.ts
import { html, LitElement } from 'lit';
import { styles } from './test-component.base.css';

export default IgcTestComponent extends LitElement {
    public static override styles = styles;

    protected override render() {
        return html`<slot></slot>`;
    }
}
// Sample style file for the IgcTestComponent component
// test-component.base.scss
@use "../../../../styles/utilities" as *;

:host {
  display: block;
  color: color(primary, 500);
}

If the component's specification defines that it will look the same in all four themes, a single base styles file can contain all styles for it.

In some instances, however, component styles vary from theme to theme. This necessitates the creation of different files to store the style rules for each theme. Furthermore, slight changes to the component markup(template) may be needed to achieve the desired design in different themes. The need to have different markup and styles based on the currently active theme means that the component instance must have access to that information at any time. To manage all this Ignite UI for Web Components includes a theming module that allows you to define and switch between themes with ease.

Dynamic Themes

If more than one theme is needed, the themes class decorator from the theming module must be used. Keeping a similar file structure across components is beneficial for better maintenance and improved legibility. For that reason, if a component requires more than a single base scss file(theme), we should create a folder called themes and store all related theme files there.

Create a new file named test-component.material.scss in the aforementioned themes folder. All style rules related to the Material theme can be stored in that file. Like before, this will trigger the creation of a new file called test-component.material.css.ts.

Import that file and use the newly created material theme in the test component.

// test-component.ts
import { html, LitElement } from 'lit';
import { themes } from '../../theming';
import { styles } from './themes/your-component.base.css';
import { styles as material } from './themes/your-component.material.css';

@themes({ material })
export default IgcTestComponent extends LitElement {
    public static override styles = styles;

    protected override render() {
        return html`<slot></slot>`;
    }
}

The themes decorator expects a configuration object of type Themes where the key is the name of the theme of type Theme, i.e. material, bootstrap, fluent, or indigo and the value is an instance of CSSResult. The test-component.material.css.ts has a named export called styles that is an instance of CSSResult and since styles was renamed to material on import, it can simply be passed to the themes decorator as is.

The themes decorator creates an instance of ThemingController, which in turn takes care of appending the style rules in each theme to the existing statically defined styles in your component. This happens on component instantiation and on subsequent runtime theme changes. By default, the bootstrap styles are adopted(if defined). The material theme styles defined in the test component above will only be adopted once upon configuring material as the theme to be used. You can learn more about how component themes are configured the Configuring Themes section of this document.

Reactive Theme Interface

Since decorating the class of a component creates a ThemingController instance, a handle to it can be grabbed by implementing the ReactiveTheme interface. When implemented, the themeAdopted method on the class is called with the ThemeController passed to it.

Here's a sample implementation of the ReactiveTheme interface.

// test-component.ts
import { html, LitElement } from 'lit';
import { themes, ReactiveTheme, ThemeController } from '../../theming';
import { styles } from './themes/your-component.base.css';
import { styles as material } from './themes/your-component.material.css';

@themes({ material })
export default IgcTestComponent extends LitElement implements ReactiveTheme {
    public static override styles = styles;
    protected tc: ThemeController;

    public themeAdopted(controller: ThemeController) {
        this.tc = controller;
    }

    protected override render() {
        return html`<slot></slot>`;
    }
}

The ThemeController provides access to the currently active theme via its theme member, which is updated on consequent changes to the theme at runtime. This allows us to make changes to the component template based on the currently active theme.

Changing the layout based on the active theme

Given the previous example, here's an updated version of the render method that takes advantage of the ThemeController's theme member:

// test-component.ts
protected override render() {
    if (this.tc.theme === 'material') {
        return html`<span>material template</span>`;
    }

    return html`<div>default template</div>`;
}

Running business logic when the theme changes

// test-component.ts
export default IgcTestComponent extends LitElement implements ReactiveTheme {
    public static override styles = styles;
    protected tc!: ThemeController;

    @state()
    protected theme!: Theme;

    public themeAdopted(controller: ThemeController) {
        this.tc = controller;
    }

    protected override willUpdate() {
        this.theme = this.tc.theme;
    }

    @watch('theme')
    protected runOnThemeChange() {
       /* executes every time the theme changes */
    }
}

Dark Themes

Adding alterations to the dark theme of a component is a little different from writing the regular theme style rules. Since component theme rules should use colors from the globally exposed CSS variables palette, dark themes should work out of the box in most instances. Sometimes designs vary between light and dark themes and require changes to the dark theme. In those instances additional style changes must be implemented. To cater for this scenario, additional CSS variables can be declared when implementing the theme style rules. Those CSS variables can then be changed based on the design requirements in a separate scss, which will be used in dark themes only.

Let's look at an example.

The test component used in the example above uses the primary 500 color shade for text content. Imagine, however, that the design prescribes that the text color should be secondary 500 in the material theme when dark themes are enabled.

// Sample style file for the IgcTestComponent component
// test-component.base.scss
@use "../../../../styles/utilities" as *;

:host {
  display: block;
  color: color(primary, 500);
}

We can modify the test-component.base.scss file slightly to have greater control over this.

// test-component.base.scss

$text-color: var(--text-color, color(primary, 500));

:host {
  display: block;
  color: $text-color;
}

Then, in a new file called test-comopnent.material.scss placed in themes > dark folder, we can make the necessary changes:

// ./themes/dark/test-component.material.scss
@use "../../../../styles/utilities" as *;

@mixin theme() {
  igc-test-component {
    --text-color: color(secondary, 500);
  }
}

A mixin named theme is created and the custom CSS property --text-color is set to the desired value as per the design requirements.

The mixin can then be added to the global material dark theme:

// src/styles/themes/dark/material.scss
@use "../../../components/test/themes/dark/test-component.material" as test-component;

@include test-component.theme();

Note! Adding the igc-test-component to the shadow DOM of other web components will necessitate the creation of a corresponding material dark theme for that component where the test-component dark theme should also be included. This in turn will force the dark material theme for the other component to be added to the global material dark theme.

Example:

/// other-component.ts
import { html, LitElement } from 'lit';
import { styles } from './other-component.base.css';

export default IgcOtherComponent extends LitElement {
    public static override styles = styles;

    protected override render() {
        return html`
        <div>
            <igc-test-component></igc-test-component>
            <slot></slot>
        </div>`;
    }
}

You will have to create themes > dark > other-component.material.scss and include the test-component material dark theme there:

@use "../../../test-component/themes/dark/test-component.material.scss" as test-component;
/// ./themes/dark/other-component.material.scss

@mixin theme() {
  @include test-component.theme();
}

Then add the other-component dark theme to the global material dark theme:

// src/styles/themes/dark/material.scss
@use "../../../components/test-component/themes/dark/test-component.material" as test-component;
@use "../../../components/other-component/themes/dark/other-component.material" as other-component;

@include test-component.theme();
@include other-component.theme();

In conclusion, the fewer differences there are between the default component themes and their dark counterparts in the design spec, the fewer styles customizations will need to be implemented.

Configuring Themes

Bootstrap is the default theme used by all components. To change the theme adopted at component instantiation you can use the configureTheme function exposed by the Ignite UI for Web Components library.

Call it in the boot script of your application and pass it one of the four valid themes - bootstrap(default), material, fluent, or indigo;

import { configureTheme } from "igniteui-webcomponents";

// Sets material as the default theme
configureTheme("material");

The configureTheme function can be called at runtime, which will update all components with the passed theme.

⚠️ **GitHub.com Fallback** ⚠️