# Troubleshooting

> Symptoms, causes and fixes for reloads, lost state, stylesheets, the DevTools panel, the overlay and the CLI.

Find the symptom you are seeing, read the cause, apply the fix. Every entry links to the page that explains the behaviour.

## Edits reload the page

### Every save reloads the page

**Symptom.** The page reloads on every edit, whatever file you touch.

**Cause.** You edited a module that is not self-accepting: one that never tells Vite it can apply its own updates. `main.ts` and `index.html` are the usual ones. The edit walks up to a full reload.

**Fix.** Edit the component module instead. See [Keep state across edits](/guides/hmr/).

### The console says “standard accessor decorators”

**Symptom.**

```text
[lit-plugin] <my-element>: standard accessor decorators — performing full reload.
```

**Cause.** Reactive properties declared with standard TC39 `accessor` decorators close over private slots created fresh on each class evaluation.

**Fix.** Use `experimentalDecorators: true` with `useDefineForClassFields: false`, or declare `static properties`. Both patch in place. See [Limitations](/reference/limitations/).

### The console says “patching failed”

**Symptom.**

```text
[lit-plugin] <my-element>: patching failed: Cannot read private member #count from an object whose class did not declare it — performing full reload.
```

**Cause.** Copying a reactive-property accessor onto the canonical class made it read state the old instances do not carry.

**Fix.** Read the detail after `patching failed:`; it is the real error. [Limitations](/reference/limitations/) lists the shapes that produce one.

### The console says observedAttributes changed

**Symptom.**

```text
[lit-plugin] <my-element> changed observedAttributes; the platform registry can't pick this up — reload recommended.
```

**Cause.** The platform snapshots `observedAttributes` when the element is defined. The patch itself succeeded, so nothing reloads, whatever `onIncompatible` says.

**Fix.** Reload by hand. The old attribute list stays in effect until you do. See [Limitations](/reference/limitations/).

### I cannot read the reason before the page reloads

**Symptom.** A line flashes past and the reload wipes the console.

**Cause.** The default `hmr.onIncompatible` is `'reload'`, which logs and reloads in the same breath.

**Fix.** Set `onIncompatible: 'warn'` while you investigate. Note it refuses the patch, so the old template stays on the page. The Components tab and `lit_hmr-incompatibilities` also keep a record. See [Options](/reference/options/).

### The first edit after editing tsconfig.json reloads

**Symptom.**

```text
[vite] changed tsconfig file detected: /app/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.
```

**Cause.** Vite clears its transform cache when a tsconfig changes, so the next edit cannot be patched.

**Fix.** Nothing. This is expected, and only the first edit pays for it. See [How HMR works](/concepts/how-hmr-works/).

## Patched, but state still resets or breaks

### A signal, cache or context key resets on every edit

**Symptom.** The component keeps its state, but a shared value goes back to its initial one.

**Cause.** A hot patch re-executes the edited module, and module-level `const` declarations are re-created with it.

**Fix.** Move the signal, cache or context key into a module that holds no component. See [Keep state across edits](/guides/hmr/).

### Editing a @lit/task body does nothing

**Symptom.** The task’s result is unchanged after you edit its function.

**Cause.** `Task` captures the function at construction time, so the live controller still holds the old one.

**Fix.** Reload once to pick the new body up. Results survive edits to the rest of the component. See [Use signals, context, tasks and the virtualizer](/guides/ecosystem/).

### Editing the parent re-renders the child, or an input loses focus

**Symptom.** A change to the parent’s header rebuilds a child element below it. The child keeps its count, but focus, listeners added by hand, or a reference you held to the element are gone. With `hmr.childState: 'reset'`, its state resets too.

**Cause.** Everything in one template literal shares one template object, so editing any part of it invalidates the whole. The child is a new element; the default `'transfer'` only carries its reactive properties and `#private` fields across.

**Fix.** Put the part you edit often in its own template literal, returned from a helper method. For a child without bindings, `hmr.childState: 'reuse'` puts the original element back. See [Keep state across edits](/guides/hmr/).

### Every update throws “Cannot read private member”

**Symptom.** After one edit, every update throws. No `[lit-plugin]` line, no reload, and the page looks like it worked.

```text
Uncaught TypeError: Cannot read private member #x from an object whose class did not declare it
```

**Cause.** `render()`, `updated()` or a method reads a native `#private` field that the dev rewrite did not cover: `hmr.privateFields` is off, or the module has a decorated private member. The throw lands inside Lit’s asynchronous update.

**Fix.** Reload to recover. Then turn `hmr.privateFields` back on, or switch to TypeScript’s `private` modifier. See [Limitations](/reference/limitations/#native-private-fields).

### A newly added reactive property is undefined

**Symptom.** The template reads `undefined`, and only the terminal shows `[vite] (client) [Unhandled rejection] TypeError: Cannot read properties of undefined`.

**Cause.** Field initialisers run in the constructor. Instances that already exist never run the new one, and nothing diagnoses it.

**Fix.** Reload once after adding a property. See [How HMR works](/concepts/how-hmr-works/).

### The first edit after a dev-server restart reloads

**Symptom.** Restart the server, edit a component, and the page reloads once.

**Cause.** The interning map that lets Lit recognise an unchanged template lives in the page, not the server, so a restarted server has no record of it.

**Fix.** Nothing. Later edits patch normally. See [How HMR works](/concepts/how-hmr-works/).

### reconnect: true seems to do nothing

**Symptom.** You turned `reconnect` on and see no difference.

**Cause.** It cycles `disconnectedCallback()` and `connectedCallback()` on live instances, nothing more. A component that does no work there has nothing to re-run.

**Fix.** Leave it off unless `connectedCallback()` does real setup. See [Keep state across edits](/guides/hmr/).

### The console shows nothing on a successful patch

**Symptom.** The edit lands, and the console stays empty.

**Cause.** Vite logs `[vite] hot updated: …` at debug level, which Chrome hides unless the console shows Verbose.

**Fix.** Turn Verbose on, or watch the on-page indicator instead. See [Keep state across edits](/guides/hmr/).

## The indicator

### There is no indicator

**Symptom.** No dot in the corner of the page.

**Cause.** `hmr` is off, `hmr.indicator` is `false`, or an env var switched one of them off. The indicator is forced off whenever HMR is.

**Fix.** Check the Settings tab for the resolved values. See [Options](/reference/options/).

### It pulses cyan and the count does not move

**Symptom.** A calm cyan pulse, with the count unchanged.

**Cause.** A shared stylesheet was swapped in place. No component re-rendered, so nothing counts.

**Fix.** Nothing. This is the expected signal for CSS delivered as a shared sheet. See [Keep state across edits](/guides/hmr/).

### The count went back to 0

**Symptom.** The running total resets.

**Cause.** The page reloaded. Only green pulses advance the count, and a reload zeroes it.

**Fix.** Find the reload in the group above. See [Limitations](/reference/limitations/).

### The count went up but my change is not on the page

**Symptom.** The indicator counts an update you cannot see.

**Cause.** `onIncompatible: 'warn'` refused the patch. The old template stays, and the update still counts.

**Fix.** Read the `can't be hot-patched` warning and reload by hand. See [Options](/reference/options/).

## Stylesheets

### TS2307, or the import is typed any

**Symptom.** TypeScript cannot find a module for `./x.css?css-sheet`.

**Cause.** The plugin’s ambient types are not in scope.

**Fix.** Add `@oddsquad/vite-plugin-lit/client` to `types` in `tsconfig.json`, keeping `vite/client` alongside it. See [Import queries](/reference/import-queries/).

### The shared sheet applies nothing

**Symptom.** Adopters render unstyled, and the console shows `[lit-plugin] ?css-sheet: failed to load …` (a built `?css-sheet`) or `[lit-plugin] urlSheet: failed to load …` (a `urlSheet()` you wired yourself).

**Cause.** The sheet is fetch-backed. When the request fails or returns an error status, the `CSSStyleSheet` stays empty (or keeps its last good css after an HMR update).

**Fix.** Check the Network tab for the stylesheet request. See [Share one stylesheet with ?css-sheet](/guides/stylesheets/css-sheet/).

### A flash of unstyled content on first paint

**Symptom.** Components appear unstyled for a moment, then snap into place.

**Cause.** A shared sheet is fetched, by design, so the first paint can beat it.

**Fix.** Add a `<link rel="preload" as="style">` for it, set `cssSheetBuild: 'inline'`, or inline the shared module. See [Choose how to deliver CSS](/guides/stylesheets/).

### Library consumers get no styles

**Symptom.** The package works in your app and ships unstyled to its users.

**Cause.** A fetched URL has nothing to resolve against in someone else’s build.

**Fix.** `build.lib` already inlines the sheet. Without it, set `cssSheetBuild: 'inline'`. See [Share one stylesheet with ?css-sheet](/guides/stylesheets/css-sheet/).

### Lightning CSS errors on @property

**Symptom.** `Unexpected token Function("var")` during dev or build.

**Cause.** The configured CSS transformer rejects syntax browsers accept. `@property` declarations are the common trigger.

**Fix.** Set `cssSheetBuild: 'inline-raw'`, or import the file with `?raw`. Both skip the CSS pipeline. See [Share one stylesheet with ?css-sheet](/guides/stylesheets/css-sheet/).

### urlSheet() edits do not swap

**Symptom.** Editing the CSS file changes nothing until you reload.

**Cause.** The `import.meta.hot.accept` call is missing, hidden inside a helper, or names a specifier built from a variable. Vite resolves the accepted specifier statically.

**Fix.** Put the accept call in the module that imports the CSS, spelled with the same literal. See [Wire a shared sheet by hand with urlSheet()](/guides/stylesheets/url-sheet/).

### A link keeps stale CSS with ?url

**Symptom.** The `<link>` element never picks up your edit.

**Cause.** `?url` returns a stable URL, so the browser serves it from cache.

**Fix.** Import with `?hmr-url`, or call `devCacheBust()` at module scope. See [Style a single component from a .css file](/guides/stylesheets/component-styles/).

### Tailwind or UnoCSS regenerates but nothing swaps

**Symptom.** The generated file changes on disk, and the page does not.

**Cause.** The swap rides the HMR update for the `.css` module. The integration that generates the CSS has to emit one. Tailwind’s Vite plugin does, and so does `unoSheet()`; UnoCSS’s own `virtual:uno.css` has no file to swap.

**Fix.** For UnoCSS, use `unoSheet()`. For another integration, check that it triggers HMR for that module. See [Share one stylesheet with ?css-sheet](/guides/stylesheets/css-sheet/).

### CSSStyleSheet is not defined in jsdom

**Symptom.** A test throws when it imports a component that uses a sheet module.

**Cause.** A sheet module constructs a `CSSStyleSheet` at import time, and jsdom can’t construct one. In Node, as in a server render with `@lit-labs/ssr`, the module builds a stand-in that carries the css text instead, so it does not throw there.

**Fix.** Run those tests in a browser, or in happy-dom, or keep sheet modules out of the code jsdom imports. See [CSS delivery](/concepts/css-delivery/).

## The DevTools panel

### The panel does not appear

**Symptom.** Vite DevTools opens with no Lit dock in it.

**Cause.** `timeline` defaults to `false`, `DevTools()` is missing from `plugins`, or `@vitejs/devtools` is not installed. When `timeline` is on and `DevTools()` is missing, the panel is not in a dock: the dev server serves it at `/__lit/` instead and prints `Lit DevTools:` with its address.

**Fix.** Open that address, or add `DevTools()` to `plugins` to get the dock (under Vite+, a `devtools:` key in the config does the same; use one, not both). See [Set up the DevTools panel](/guides/devtools/).

### Vite errors with DTK0034

**Symptom.** The dev server refuses to start, naming a duplicate registration.

**Cause.** A `devtools:` key in the config already registers the overlay, and `DevTools()` in `plugins` registers it a second time.

**Fix.** Keep one of the two. See [Set up the DevTools panel](/guides/devtools/).

### The dock does not connect when Vite runs in middleware mode

**Symptom.** Behind your own server (`server: {middlewareMode: true}`, as in an SSR setup), the page logs `WebSocket connection to 'ws://127.0.0.1:9777/__ws' failed` and the dock never connects.

**Cause.** With no HTTP server of Vite’s own, Vite DevTools opens a socket of its own on a port from 9777 up, bound to `server.host` (`localhost` when unset). The page dials it at the host it was loaded from. `localhost` can resolve to `::1` while the page is on `127.0.0.1`, so nothing answers.

**Fix.** Pass the host your server listens on as `server.host` too, for example `server: {middlewareMode: true, host: '127.0.0.1'}`. The [SSR example](https://github.com/oddcelot/vite-plugin-lit/tree/main/examples/ssr) does this in `server.mjs`.

### The Components tab says the page runtime has not connected

**Symptom.** The tab shows “The page runtime has not connected to this dev server.”

**Cause.** `timeline` is off, so no runtime script was injected; the page is not served through this dev server; or the script failed to load.

**Fix.** Set `timeline: true` or `LIT_PLUGIN_TIMELINE=true`, reload the page through the dev server, and check the browser console for a failed script.

### The Components tab is empty

**Symptom.** The tab shows “No Lit components found on the page.”

**Cause.** Nothing Lit has rendered yet, the components come from a pre-bundled dependency outside Vite’s transform, or lit loaded after the runtime announced itself.

**Fix.** Wait or navigate (with **Live** on, the tree picks up components as they render; if you paused it, turn it back on), and check Settings, About, for the Lit packages (`lit-html`, `lit-element`, `@lit/reactive-element`) the runtime detected.

### A source link opens nothing in your editor

**Symptom.** In Lit Inspector, clicking **defined** or **rendered** does nothing, and the bottom of the **Lit** tab says files open only if Vite runs in the project folder.

**Cause.** Vite opens a path relative to the folder it was started in. A server started elsewhere, such as `vite app` from a parent folder, looks in the wrong place and still reports success.

**Fix.** Click **Set project folder…** and enter the project folder, or start Vite from the project folder. With the plugin’s `sourceOverlay` on, the plugin’s own endpoint resolves paths itself and the problem does not arise.

### The Components tab warns about more than one copy of lit

**Symptom.** The tab says more than one copy of lit is loaded and names the duplicated package with its versions.

**Cause.** Two copies of `lit`, `lit-element` or `@lit/reactive-element` resolve, typically a dependency with its own nested lit. Components registered against another copy are invisible to the inspector and to HMR patching.

**Fix.** Set `resolve.dedupe: ['lit', 'lit-element', '@lit/reactive-element']` in the Vite config, then reinstall and restart the dev server.

### The Components tab says it is running inside an iframe

**Symptom.** The tab says the runtime is running inside an iframe.

**Cause.** The runtime loaded in a frame, so only that frame’s components are listed.

**Fix.** Open the framed app directly in its own tab.

### The Components tab says another runtime serves the page

**Symptom.** A line above the tree says the Lit Inspector extension serves this page and the dev server’s plugin shares its runtime. The console says the same once.

**Cause.** The extension is enabled for a site that is a dev server already running the plugin, so both brought a runtime. The extension’s loads first and serves the page; the plugin’s adds its connection to it instead of installing a second one. Both panels, the extension’s and the dev server’s, show the same tree, and Pick works from either.

**Fix.** Nothing is broken. What the panels show comes from the extension’s runtime, so if the extension is a different version from the plugin, turn the extension off for that site to use the plugin’s own.

### The tree shows a different page, or the recording vanished

**Symptom.** The Components tree changes to another page’s, or a recording you were reading is gone. A banner reads “Another page connected”.

**Cause.** The panel follows one page at a time. Opening the app in a second tab, an iframe reload or a Cmd-click on a link starts a new page, and the panel switches to it and clears the recording. Traffic from the page it left is ignored. Reloading the tab you are inspecting clears the recording too, but shows no banner.

**Fix.** Close the other tab, or reload the one you want to inspect, to switch back. Dismiss the banner with the close button.

### The timeline is empty

**Symptom.** You interact with the page and no rows arrive.

**Cause.** Recording is off, or the layer you expect is toggled off. Layer toggles are remembered in `localStorage` across sessions.

**Fix.** Press **▶ Record**, then check the layer pills. See [Record and read the timeline](/guides/devtools/timeline/).

### Updates shows a tick with no changed keys

**Symptom.** The row is there, and the Reasons column is empty.

**Cause.** The component overrides `willUpdate` or `updated` without calling `super`, so that phase is never reported.

**Fix.** Call `super` in the override. See [Find out why a component re-rendered](/guides/devtools/updates/).

### A component shows ⚠ in Updates

**Symptom.** The component table shows a ⚠ count, and an update row reads *threw in update* (or `willUpdate`, `updated`), *rejected in updated*, or *task … failed*.

**Cause.** That lifecycle phase threw, an async phase’s promise rejected unhandled, or a `@lit/task` failed. The component usually renders blank or stays stale, and the error also appears in the browser console. The panel records the error’s name and message only, never a stack.

**Fix.** Open the browser console for the stack, fix the throwing method, and record again. See [Find out why a component re-rendered](/guides/devtools/updates/).

### No Pick button, or Meta+Shift+E does nothing

**Symptom.** The Components tab has no **⌖ Pick** button, and the shortcut shows no picker on the page.

**Cause.** Under Vite, picking reuses the source-overlay picker, which is off by default. (A page connected to `lit-devtools dev` brings its own picker.)

**Fix.** Set `sourceOverlay: true`. See [Open a component in your editor from the page](/guides/source-overlay/).

### A source link opens nothing, or the wrong editor

**Symptom.** Clicking a file path does nothing, or lands in another editor.

**Cause.** The shared open service refuses paths outside the workspace root and the panel falls back to the plugin’s own endpoint. The editor comes from `launch-editor` there, not from `sourceOverlay.editor`.

**Fix.** See [DevTools architecture](/concepts/devtools-architecture/).

### Settings I changed are missing in another browser

**Symptom.** Your overrides are gone in a second browser on the same machine.

**Cause.** The page reads a `localStorage` cache at boot, before any connection to DevTools exists.

**Fix.** Open the panel once in that browser; it refills the cache from the durable copy. See [DevTools architecture](/concepts/devtools-architecture/).

### The exported snapshot lacks details or early events

**Symptom.** The frozen panel shows a tree with empty detail panes, or starts mid-session.

**Cause.** Only components you opened have details baked in, and the timeline keeps its most recent events.

**Fix.** Click the components that matter before exporting. `lit-devtools build` cannot export for you. See [Record and read the timeline](/guides/devtools/timeline/).

## The source overlay

### Ctrl+Shift+S (or ⌘⇧S) does nothing

**Symptom.** No outline appears when you press the shortcut.

**Cause.** `sourceOverlay` is off by default, `sourceOverlay.key` was changed, or the browser claimed the combination first.

**Fix.** Enable it and pick a free key. See [Open a component in your editor from the page](/guides/source-overlay/).

### It works locally but not on StackBlitz

**Symptom.** Clicking an element opens nothing in the hosted preview.

**Cause.** Opening goes through a dev-server middleware, and falls back to the editor’s URL scheme when the server is unreachable.

**Fix.** Nothing. This is expected in a hosted preview. See [Open a component in your editor from the page](/guides/source-overlay/).

## The CLI and agents

### The command needs an optional peer

**Symptom.**

```text
[lit-devtools] this command needs the optional peer "cac". Install it and retry:

  npm install -D cac
```

**Cause.** `cac` and `@devframes/agentic` are optional peers, imported only on the path that needs them.

**Fix.** Install the named package. See [CLI](/reference/cli/).

### The agent lists no instances

**Symptom.** `devframe_connect_list-instances` comes back empty.

**Cause.** No dev server is running, or registry discovery was unavailable and the plugin warned about it at startup.

**Fix.** Start the server, or bridge the URL directly with `mcp-remote http://localhost:<port>/__devtools/__mcp`, or `/__lit/__mcp` when the server runs without Vite DevTools. See [Connect a coding agent](/guides/devtools/agents/).

### lit_recent-events returns nothing

**Symptom.** The tool answers with `recording: false` and no events.

**Cause.** Recording is off, and nothing is being captured.

**Fix.** Call `lit_set-recording`, or press **▶ Record** in the panel. See [Connect a coding agent](/guides/devtools/agents/).

## Configuration

### An environment variable is ignored

**Symptom.** `LIT_PLUGIN_*` has no effect.

**Cause.** An explicit `litPlugin()` option wins over the variable. Booleans also only accept `true`, `1`, `false` and `0`.

**Fix.** Remove the explicit option, fix the value, and restart the dev server after editing `.env`. See [Environment variables](/reference/environment-variables/).

### LIT_PLUGIN_CSS_SHEET_BUILD is rejected

**Symptom.** A startup warning names the variable and lists the allowed values.

**Cause.** The value is outside the closed set, usually a typo.

**Fix.** Use `auto`, `url`, `inline` or `inline-raw`. A typo falls back to the default rather than disabling the feature. See [Environment variables](/reference/environment-variables/).

### Rolldown full-bundle dev mode gives plain reloads

**Symptom.** No HMR features apply at all.

**Cause.** The plugin targets the standard Vite dev server pipeline, where each module is served and re-executed on its own.

**Fix.** Use the standard dev server. There is no supported configuration of the full-bundle mode. See [Limitations](/reference/limitations/).

### virtual:lit-plugin/timeline breaks vite build or reports TS2307

**Symptom.** The build fails, or TypeScript cannot resolve the virtual module.

**Cause.** Both were bugs before 0.3.0, which moved the hooks and shipped ambient types.

**Fix.** Upgrade to 0.3.0 or later. See [Changelog](/reference/changelog/).

## Related

[Limitations](/reference/limitations/)Every component shape that cannot be patched, with its console line.

[Keep state across edits](/guides/hmr/)The defaults behind most of the entries above.
