Skip to content

Your First Lit Component

This lesson walks through building a complete, working Lit component step by step: defining the class, adding a reactive property, rendering a template, and using the finished tag in HTML.

Building a Simple Greeting Component

The clearest way to learn Lit is to build one small, complete component end to end. This example accepts a name property, greets the user, and includes a button that increments a personal counter, exercising templates, properties, events, and styles together.

Follow along by creating a new file, pasting the class definition, registering the element, and then using the resulting tag directly in an HTML file.

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

@customElement('greeting-counter')
export class GreetingCounter extends LitElement {
  static styles = css`
    :host { display: block; font-family: sans-serif; }
    button { margin-left: 0.5rem; }
  `;

  @property() name = 'friend';
  @state() private clicks = 0;

  render() {
    return html`
      <p>Hello, ${this.name}! You clicked ${this.clicks} time${this.clicks === 1 ? '' : 's'}.</p>
      <button @click=${this.#handleClick}>Click me</button>
    `;
  }

  #handleClick() {
    this.clicks += 1;
  }
}

@property() exposes name as both a JS property and an HTML attribute; @state() keeps clicks reactive but private to the component.

Using the Finished Component

<script type="module" src="./greeting-counter.js"></script>

<greeting-counter name="Ada"></greeting-counter>
  • Importing the module file is what registers the <greeting-counter> tag with the browser.
  • The name attribute in HTML sets the name property automatically, because it was declared reactive.
  • You can also set name from JavaScript: document.querySelector('greeting-counter').name = 'Grace';.
  • The component re-renders automatically any time name or clicks changes, from any source.

First Component Checklist

The steps every new Lit component follows, in order.

Step What to Do
1. Import import { LitElement, html, css } from 'lit';
2. Extend class MyEl extends LitElement { ... }
3. Declare properties @property() fields or static properties
4. Add styles static styles = css...`
5. Render render() { return html...; }
6. Register @customElement('my-el') or customElements.define()
7. Use it <my-el></my-el> anywhere in HTML

Using Private Class Fields for Handlers

The #handleClick method in the example above uses native JavaScript private fields (the # prefix). This is optional, but it's a good habit for internal event handlers and helper methods that should never be called from outside the component.

  • Private methods (#method()) can't be accessed or overridden from outside the class, unlike a conventional underscore-prefixed convention.
  • Public reactive properties (like name) remain your component's actual API surface.
  • Keeping handlers private makes it clear, at a glance, which parts of a component are implementation detail.

Verifying the Component Renders

After registering the element, open your browser's DevTools and inspect the <greeting-counter> element. You should see a #shadow-root node containing the <p> and <button> — confirmation that Lit rendered into an encapsulated Shadow DOM tree rather than directly into the light DOM.

<greeting-counter name="Ada">
  #shadow-root (open)
    <p>Hello, Ada! You clicked 0 times.</p>
    <button>Click me</button>
</greeting-counter>

Clicking the button should update the count text in place without recreating the paragraph or button elements.

Common Mistakes

  • Forgetting to import the component's module file, so customElements.define()/@customElement never runs and the browser treats the tag as an unknown element.
  • Declaring a field without @property() or @state() and expecting it to trigger re-renders.
  • Setting a boolean attribute like disabled="false" in HTML and expecting it to be falsy — HTML attributes are strings, covered in the properties/attributes lesson.
  • Writing DOM manipulation code inside render() instead of returning a declarative html template.

Key Takeaways

  • A complete Lit component needs an import, a class extending LitElement, reactive properties, a render() method, and registration.
  • @property() syncs with HTML attributes; @state() is for properties that are reactive but internal.
  • Once registered, a Lit component's tag can be used exactly like any built-in HTML element.
  • Inspecting the Shadow DOM in DevTools is the fastest way to confirm a component rendered correctly.

Pro Tip

Keep your very first few components deliberately tiny (one property, one event handler) until the full render-on-change cycle feels automatic. It's much easier to debug a five-line component than a fifty-line one when something doesn't update as expected.