Hilla Documentation

Component Basics

Creating views using reactive web components with Lit in Hilla.

To create views on the frontend, Hilla uses web components, a native technology that allows you to create custom HTML elements. It’s almost like a framework built into the browser, and it’s usually faster and smaller than other frameworks, while still sharing their advantages.

The Lit library is a convenient way of authoring web components, and it’s recommended over the low-level native browser APIs.

Lit provides a base class named LitElement, and a set of tools to create reactive web components. Under the hood, it uses lit-html, a tiny library to render and re-render HTML templates in an efficient and declarative way.

Minimal Lit Component

To create a minimal working component, you must do two things: extend the LitElement class and implement a render() method.

Template

The render() method is responsible for rendering and re-rendering a component template. It should return a TemplateResult instance produced by the html tagged template literal. The string tagged with the html tag is displayed in the browser as the component’s content.

Learn more about tags and tagged template literals from MDN Web Docs.

Custom Element Definition

A web component class must be added to the browser’s custom element registry and assigned a unique element name. The @customElement decorator is a convenient way of doing this with LitElement. It receives an HTML element name that should contain at least one dash - (as required by the HTML specification). For example, my-view, some-fancy-button or vaadin-checkbox.

Example

import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('minimal-view')
class MinimalView extends LitElement {
  render(): TemplateResult {
    return html`<h1>My View</h1>`;
  }
}

export default MinimalView;

Data Binding

A web component produces a view based on the data it receives from outside or preserves within. That data is bound to the component template declaratively, and each time the data is changed, the template changes as well. This section describes approaches to receiving and storing the data, and to binding the data to the template.

Fields, Methods, and Accessors

Since the web component is a simple JavaScript class, you can define any number of fields, methods, and accessors you need and call them as you are used to doing. TypeScript, as a superset of JavaScript, also allows you to define private and protected methods.

There are no specific requirements for naming, but you may want to follow the convention that any non-public class members should have an underscore _ at the beginning of their names.

Properties

Components can have properties that receive data from the outside (from HTML or HTML templating system) or preserve the internal state. Changing these properties triggers an asynchronous re-rendering of the component to fill the template with the new data.

Lit provides two decorators that can transform a regular class field into a component data property.

@property

Makes a field a data property that’s allowed to receive data from the outside. This means that you can send the data to the component via one of the following options:

  • Using the html tag from Lit: html`<my-view .myProperty=${this.myValue}></my-view>`

  • As an attribute of an instance of the component: document.querySelector('my-view').setAttribute('myProperty', 'myValue').

  • As a property of an instance of the component: document.querySelector('my-view').myProperty = 'myValue'.

@state

Makes a field a data property that contains an internal state of the component. These properties must not be used from outside. For example, you can use it if you need to trigger re-rendering on some events fired by children.

Read more about data properties in the Lit documentation.

Template Binding

The html function is designed to assign JavaScript values to HTML elements in an obvious and easy way. Lit provides four ways to do this:

Assign to an attribute

Only values of simple types can be assigned to an attribute: String, Number or Boolean. The value is converted to a string (Boolean values are corrected directly to their string representations) and also reflected in the HTML code. For this approach, you can use the regular HTML attribute syntax.

Assign to a property

Allows any value type. The value isn’t reflected in the HTML code. It’s recommended to use this approach over assigning to an attribute, because it’s simpler and more performant. For this approach, you must insert a dot . before the property name: .property.

Assign a boolean attribute

Allows only boolean values. The only difference from assigning to an attribute is that, instead of being assigned directly, it’s removed and added again according to the value. For this approach, you must add a question mark ? before the property name.

Listen to an event by the name

Allows only functions. When the element fires an event, the function assigned to the name is called with the event as an argument. For this approach, you must add an at sign, @, before the event name: @event.

Note
Correct this reference in event listeners
You don’t need to bind this to the event listener method. It refers to the component class in LitElement listeners automatically.

Learn about how to write templates from the Lit documentation.

Example

import { html, LitElement, TemplateResult } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';

@customElement('data-binding-view')
class DataBindingView extends LitElement {
  @property() message = '';
  @property() name = '';

  @state() active = false;

  render(): TemplateResult {
    return html`
      <example-template-header ?active=${this.active} name=${this.name} .message=${this.message}>
        <button @click=${this.onClick}>Toggle</button>
      </example-template-header>
    `;
  }

  private onClick() {
    this.active = !this.active;
  }
}

export default DataBindingView;

Lifecycle

Each component has lifecycle methods that you can override to execute code at a specific time. LitElement adds its own methods to handle the asynchronous render() calls to the standard web component lifecycle.

List of lifecycle callbacks
constructor

Specificity: JavaScript class

The constructor is invoked when a class is instantiated. For a component, it happens when document.createElement('my-view') is called.

In most cases, you don’t need a constructor, because LitElement handles everything important. However, if you do create one, try to avoid performing any heavy work; the element might be discarded after invoking the constructor.

Note
The element needs to be connected to run the render() callback.
When the element is created, no render happens.
connectedCallback

Specificity: Web Components

This callback is invoked each time the element is connected to the DOM.

In most cases, the connectedCallback() is used to set up component-level event listeners, etc.

Note
Use firstUpdated callback to execute the code after the first render
Since render() is asynchronous, it’s not over when the connectedCallback(()) finishes.
Note
Remember to call the super.connectedCallback().
You must call super.connectedCallback() in your connectedCallback() method. Otherwise, you may lose the default LitElement lifecycle.
disconnectedCallback

Specificity: Web Components

This callback is invoked each time the element is disconnected from the DOM.

The default role of the disconnectedCallback() is to remove or close everything that’s set up during the connectedCallback().

Note
No DOM access
This callback is invoked after the element is disconnected from the DOM.
Note
Remember to call the super.disconnectedCallback().
You must call super.disconnectedCallback() in your disconnectedCallback() method. Otherwise, you may lose the default LitElement lifecycle.
firstUpdated

Specificity: Lit

This callback is invoked right after the first render is over and before the first updated() callback. It receives an object that contains all changed properties.

updated

Specificity: Lit

This callback is invoked after each render; for the first render, it’s invoked right after the firstUpdated() callback. Just like firstUpdated(), it receives an object that contains all properties changed after the previous render.

import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-button')
class MyButton extends LitElement {
  constructor() {
    super();
    this.onClick = this.onClick.bind(this);
  }

  connectedCallback() {
    this.addEventListener('click', this.onClick);
  }

  disconnectedCallback() {
    this.removeEventListener('click', this.onClick);
  }

  render() {
    return html`<button>Some button</button>`;
  }

  private onClick() {
    console.log('I am clicked');
  }
}

export default MyButton;

Read more about the Lit-specific lifecycle methods in the Lit documentation.

Styling

For styling, LitElement provides a static property styles. You can use either a style sheet imported from a .css file or define an inline style sheet using the css tagged function.

The styles property also accepts an array of style sheets.

import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './my-view.css';

@customElement('my-view')
class MyView extends LitElement {
  static styles = [
    styles,
    css`
      h1 {
        color: red;
      }
    `,
  ];

  render() {
    return html`<h1>My View</h1>`;
  }
}

export default MyView;

Shadow and Light DOM

You can use two main approaches for web components. You can find additional information in the Style Scopes article.

Shadow DOM

A web component can create a shadow tree within. It’s a separate document fragment, almost inaccessible from the regular DOM, existing as a part of the component and displayed in the browser as a regular element tree. With this, you can create elements with styles independent of the global CSS and HTML content not added to children nodes.

Shadow DOM is enabled by default when using the LitElement class.

import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-view')
class MyView extends LitElement {
  render(): TemplateResult {
    return html`<h1>My View</h1>`;
  }
}

export default MyView;

Light DOM

If you don’t want to use the Shadow DOM, you can still use the Light DOM, a regular part of the DOM controlled by the component’s class. It lacks Shadow DOM advantages, such as scoped CSS or non-children HTML, but it’s lighter and simpler for both development and performance.

To enable Light DOM for the LitElement component, add a createRenderRoot() { return this; } method to your component.

import { html, LitElement, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('my-view')
class MyView extends LitElement {
  render(): TemplateResult {
    return html`<h1>My View</h1>`;
  }

  protected createRenderRoot(): Element | ShadowRoot {
    return this;
  }
}

export default MyView;

Best Practices

Shadow DOM is recommended only for "leaf" components (checkboxes, text fields, combo boxes, etc.) and Light DOM for other components such as views or their parts. Deeply nested shadow trees can have a negative impact on performance.

Using in Routes

Read more about how to use components together with Hilla Router in the Hilla Router documentation.