# Build your first hot-reloading component

> Scaffold a Lit app, add the plugin, and watch a list, a counter and a half-typed input survive every edit.

Build a small Lit app and watch its state survive every edit you make to it. Start here if the plugin is new to you; Installation is the short version for a project you already have. It takes about fifteen minutes and needs Node 20 or newer, an editor, and no knowledge of Vite plugins.

1. **Scaffold the app.** The Lit and TypeScript template asks no questions.

   ```sh
   npm create vite@latest my-lit-app -- --template lit-ts
   cd my-lit-app
   npm install
   npm run dev
   ```

   Open the URL Vite prints.

   **Check:** a page with the Lit and Vite logos and a button reading `Count is 0`. Click it and the number goes up.

2. **Feel the problem.** Click the button three times, so it reads `Count is 3`. Then rename the label in the starter component.

   ```ts
     part="button"
   >
     Count is ${this.count}
     Clicked ${this.count} times
   </button>
   ```

   **Check:** the page reloads and the button reads `Clicked 0 times`. Your three clicks are gone.

   Nothing in that module tells Vite it can apply its own update, so the edit walks all the way up to a full page reload.

3. **Add the plugin.**

   ```sh
   npm install -D @oddsquad/vite-plugin-lit
   ```

   The template ships without a Vite config, so write one.

   ```ts
   import {defineConfig} from 'vite';
   import {litPlugin} from '@oddsquad/vite-plugin-lit';

   export default defineConfig({
     plugins: [litPlugin({hmr: {indicator: {count: true}}})],
   });
   ```

   `indicator.count` puts a running total next to the on-page indicator. Every step below reads it.

   Register the plugin’s ambient types in the same sitting. TypeScript’s `types` array replaces its default, so keep `vite/client` in the list.

   ```json
   "types": ["vite/client"],
   "types": ["vite/client", "@oddsquad/vite-plugin-lit/client"],
   ```

   Do it now rather than later: editing `tsconfig.json` forces a full reload, and from the next step on you have state on the page worth keeping.

   Stop the dev server and start it again. Vite does not pick up a config file that did not exist when it started.

   ```sh
   npm run dev
   ```

   **Check:** a small indicator in the corner of the page, showing a `0`.

   ![The indicator at rest in the corner of the page, showing 0.](/_astro/indicator-idle.light.CMIydUfp_Zh58Nt.webp)![The indicator at rest in the corner of the page, showing 0.](/_astro/indicator-idle.dark.dDKkKFdc_Z2t39gs.webp)

4. **Make the same kind of edit, and keep your clicks.** Click the button three times, then rename the label once more.

   ```ts
   Clicked ${this.count} times
   You clicked ${this.count} times
   ```

   **Check:** the label changes to `You clicked 3 times`. The page does not reload, the count stays at 3, and the indicator pulses green and goes from `0` to `1`.

   ![The indicator pulsing green after the label edit; the count reads 1.](/_astro/indicator-green.light.BQnZTItl_1d6OoU.webp)![The indicator pulsing green after the label edit; the count reads 1.](/_astro/indicator-green.dark.C7gbpXOA_Z2dwk6y.webp)

   That is a hot patch: an edit applied to the running page without a reload.

5. **Give it state worth losing.** One number is a thin test. Replace `src/my-element.ts` with a component that holds a list, a counter and an input.

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

   @customElement('my-element')
   export class MyElement extends LitElement {
     static styles = css`
       :host {
         display: block;
         max-width: 32rem;
         margin: 3rem auto;
         font-family: system-ui, sans-serif;
       }

       input:focus {
         outline: 2px solid currentColor;
       }
     `;

     @state() private count = 0;
     @state() private notes: string[] = [];

     private renderHeader() {
       return html`<h1>Notebook</h1>`;
     }

     private renderList() {
       return html`<ul>
         ${this.notes.map((note) => html`<li>${note}</li>`)}
       </ul>`;
     }

     render() {
       return html`
         ${this.renderHeader()}
         <input placeholder="Type a note, press Enter" @keydown=${this.onKey} />
         ${this.renderList()}
         <button @click=${this.onClick}>Clicked ${this.count} times</button>
       `;
     }

     private onClick() {
       this.count++;
     }

     private onKey(event: KeyboardEvent) {
       const input = event.target as HTMLInputElement;
       if (event.key === 'Enter' && input.value) {
         this.notes = [...this.notes, input.value];
         input.value = '';
       }
     }
   }

   declare global {
     interface HTMLElementTagNameMap {
       'my-element': MyElement;
     }
   }
   ```

   Take the template’s placeholder content out of the page. Leave the `<script>` in the head where it is.

   ```html
   <body>
     <my-element>
       <h1>Get started</h1>
     </my-element>
     <my-element></my-element>
   </body>
   ```

   Reload the page once. `notes` is a new reactive property, and reactive properties get their initial value when an instance is constructed, so the instance already on the page does not have one; a reload starts clean.

   Add two notes, click the button twice, then type half a third note and leave the caret in the input. Now edit the heading, and nothing else.

   ```ts
   private renderHeader() {
     return html`<h1>Notebook</h1>`;
     return html`<h1>My notes</h1>`;
   }
   ```

   **Check:** the heading reads `My notes`. Both notes are still listed, the button still reads `Clicked 2 times`, your half-typed third note is still in the input, and the caret is still where you left it.

   `renderHeader()` and `renderList()` are separate `html` literals, and only the header’s text changed. That is template interning: reusing the same template object for identical template text, so Lit sees an unchanged template as unchanged and rebuilds only the header.

6. **Hot-swap a stylesheet.** Write a stylesheet the component can adopt.

   ```css
   .card {
     list-style: none;
     margin-bottom: 0.5rem;
     padding: 0.5rem 0.75rem;
     border: 2px solid dodgerblue;
     border-radius: 6px;
   }
   ```

   Import it with `?css-sheet` and put it at the front of `static styles`. The query hands you a `CSSStyleSheet` object, and `static styles` takes an array, so the component’s own rules stack on top of it. Give each note the `card` class while you are there.

   ```ts
   import {LitElement, css, html} from 'lit';
   import {customElement, state} from 'lit/decorators.js';
   import theme from './theme.css?css-sheet';

   @customElement('my-element')
   export class MyElement extends LitElement {
     static styles = [
       theme,
       css`
         :host {
           display: block;
           max-width: 32rem;
           margin: 3rem auto;
           font-family: system-ui, sans-serif;
         }

         input:focus {
           outline: 2px solid currentColor;
         }
       `,
     ];

     @state() private count = 0;
     @state() private notes: string[] = [];

     private renderHeader() {
       return html`<h1>My notes</h1>`;
     }

     private renderList() {
       return html`<ul>
         ${this.notes.map((note) => html`<li class="card">${note}</li>`)}
       </ul>`;
     }

     render() {
       return html`
         ${this.renderHeader()}
         <input placeholder="Type a note, press Enter" @keydown=${this.onKey} />
         ${this.renderList()}
         <button @click=${this.onClick}>Clicked ${this.count} times</button>
       `;
     }

     private onClick() {
       this.count++;
     }

     private onKey(event: KeyboardEvent) {
       const input = event.target as HTMLInputElement;
       if (event.key === 'Enter' && input.value) {
         this.notes = [...this.notes, input.value];
         input.value = '';
       }
     }
   }

   declare global {
     interface HTMLElementTagNameMap {
       'my-element': MyElement;
     }
   }
   ```

   That is a component edit, so saving it gives you a green pulse and the count goes up. Now change one word in the stylesheet.

   ```css
   border: 2px solid dodgerblue;
   border: 2px solid tomato;
   ```

   **Check:** the note borders turn red. The indicator pulses cyan, and the count does not move.

   ![The indicator pulsing cyan after the theme.css edit; the count has not moved.](/_astro/indicator-cyan.light.Boi-XPPG_Z10TsE4.webp)![The indicator pulsing cyan after the theme.css edit; the count has not moved.](/_astro/indicator-cyan.dark.DmGiwpNy_Z1S8s88.webp)

   Cyan means a shared stylesheet was swapped in place. One `CSSStyleSheet` object is adopted by every shadow root that imported it, and replacing its rules restyles them all without re-rendering a component.

7. **See what a reload looks like.** Change the page title.

   ```html
   <title>my-lit-app</title>
   <title>My notes</title>
   ```

   **Check:** the page reloads. The notes are gone, the input is empty, and the indicator is back to `0`.

   `index.html` is not a module, so there is nothing to patch. A few component shapes cannot be patched either, standard `accessor` decorators among them. [Limitations](/reference/limitations/) names each one, what you see when it fires, and what to write instead. If you would rather keep the page up and reload by hand, `hmr.onIncompatible` has a `'warn'` mode; the [HMR guide](/guides/hmr/) covers it.

## Where next

You have a component whose list, counter, input text and caret ride through every edit, and a stylesheet that restyles the whole app without re-rendering anything. These three pages take it further.

[Keep state across edits](/guides/hmr/)What a hot patch preserves, and the three settings worth changing.

[Choose how to deliver CSS](/guides/stylesheets/)When a shared sheet beats per-component styles, and how to wire each.

[DevTools timeline](/guides/devtools/)Record what your components did, and see why they re-rendered.
