Skip to content

Installation

Install the plugin, add one line to vite.config.ts, and register the ambient types. If you have never used the plugin, follow the tutorial instead; it includes these steps.

Working with a coding agent? Hand it this prompt and it does the steps below, then reports which files it changed.

Terminal window
npm i -D @oddsquad/lit-devtools

Lit and Vite stay your dependencies. Compatibility lists the versions each release supports and the one dev mode it does not.

@oddsquad/lit-devtools brings every part under one name: the Vite and Astro plugin, the language server’s settings, and its Oxlint rules. Each part is also its own package, for a project that wants only one: @oddsquad/vite-plugin-lit, @oddsquad/lit-language-server and @oddsquad/oxlint-plugin-lit. The plugin’s imports then drop the subpath: @oddsquad/vite-plugin-lit in place of @oddsquad/lit-devtools/vite.

Add litPlugin() to the plugins array. Nothing else moves.

vite.config.ts
import {defineConfig} from 'vite';
import {litPlugin} from '@oddsquad/lit-devtools/vite';
export default defineConfig({
plugins: [litPlugin()],
});

Hot patching, the on-page indicator and the CSS import queries are on from here. The DevTools timeline and the source overlay are opt-in; the options reference has the switches.

The patching applies to the dev server only, so a production build is untouched. The ?css-sheet and ?hmr-url queries resolve in dev and build alike.

litPlugin() doesn’t load its dev-only code until the dev server (or vite preview) starts. HMR, the source overlay and the DevTools panel are imported in a hook that vite build never calls, so a build loads only the options and the CSS plugins.

What those CSS plugins emit carries no dev code either: under vite build, a ?css-sheet compiles to a sheet that is filled once, with no hot-swap callback, and a ?hmr-url to the plain asset URL.

If you want the build to depend on less than that, use litCss() from the /build entry for builds. It carries only what a build uses: the CSS queries, Lightning CSS for css literals, and the no-op timeline stub.

vite.config.ts
import {defineConfig} from 'vite';
import {litPlugin} from '@oddsquad/lit-devtools/vite';
import {litCss} from '@oddsquad/lit-devtools/build';
export default defineConfig(({command}) => ({
plugins: [command === 'serve' ? litPlugin() : litCss()],
}));

litCss() takes cssSheetBuild and reads LIT_PLUGIN_CSS_SHEET_BUILD the same way litPlugin() does. A unit test walks both entries’ import graphs so neither can start pulling in dev code unnoticed.

The import queries ship ambient declarations in @oddsquad/lit-devtools/client. Without them TypeScript cannot type import sheet from './theme.css?css-sheet'.

The types array replaces TypeScript’s default, so list vite/client next to it rather than instead of it. The rest of this file is the lit-ts template’s, unchanged.

tsconfig.json
{
"compilerOptions": {
"target": "es2023",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "esnext",
"lib": ["ES2023", "DOM"],
"types": ["vite/client", "@oddsquad/lit-devtools/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

One .d.ts anywhere in your include does the same job, if you would rather not touch types:

src/lit-plugin.d.ts
/// <reference types="@oddsquad/lit-devtools/client" />

Start the dev server and open a page that renders a Lit component.

Terminal window
npm run dev

A small indicator sits in the corner of the page. It is idle until an update arrives.

Type into an input inside a component, then edit that component’s template and save. The markup changes, what you typed stays, and the indicator pulses green. That is a hot patch: an edit applied to the running page without a reload.

If the page reloads instead, the edit hit a shape that cannot be patched in place. Limitations names each one.