Skip to content

Language server

The Lit language server reads your components from source, as you type, and your dependencies’ Custom Elements Manifests, and answers in html and svg templates, in HTML files, in JSX, Astro and MDX, and in css templates. Use the language server sets it up.

It reads the same JSDoc the manifest does, so this component is the example below:

src/x-button.ts
/**
* A button that submits the form it sits in.
*
* @fires submit-start - Before the form is sent.
* @cssprop [--x-button-gap=4px] - Space between icon and label.
* @csspart base - The native button.
*/
@customElement('x-button')
export class XButton extends LitElement {
/** How much room the button takes. */
@property({reflect: true}) size: 'small' | 'large' = 'small';
/** Arbitrary data, never an attribute. */
@property({attribute: false}) data: Record<string, unknown> = {};
#submit() {
this.dispatchEvent(
new CustomEvent('submit-start', {detail: {size: this.size, at: 0}})
);
}
}

In an html template, ‸ marking the cursor:

You type It offers
<x-‸ Your components, the tags of your dependencies’ manifests, then HTML and SVG elements
<x-button ‸ Its attributes not yet written, then the global ones (id, class, slot…)
<x-button .‸ Its properties: size, data
<x-button ?‸ Its boolean attributes, then the global boolean ones (hidden, inert…)
<x-button @‸ Its events, then the events your other components fire, then DOM events (click, input…)
<x-button size="‸" The values of a union of string literals: small, large
<path stroke-linejoin="‸" Fixed values of HTML and SVG attributes: miter, round, bevel…

Events bubble, so @ on any element offers the custom events any of your components fires. A parent can listen for its children’s events:

html`<form @submit-start=${this.#onStart}>…</form>`;
// ^ offered: <x-button> fires it

In HTML files the server completes your components and their attributes, and leaves standard elements to the editor’s own HTML support.

Over It shows
A component’s tag Its summary, description, attributes, properties, events, slots, parts and CSS properties
An attribute or .property Its type, default, the attribute or property it maps to, where it’s inherited from
An @event Who fires it, and what a listener receives: Event CustomEvent · detail {size: 'small' | 'large'; at: number}
An HTML or SVG element or attribute Its description and a link to MDN
A property in a css template Its description and browser support

An event’s type comes from the dispatch when no @fires {Type} names one: CustomEvent<T>’s type argument, else the shape of the detail object, each key typed by the field it reads or the literal it holds. What can’t be told without a type checker is unknown.

The server reads your components in JSX in .jsx and .tsx files, in an Astro component’s template, and in MDX. Completion, hover, go to implementation and find references work there as in a Lit template, with each format’s way of writing a binding:

You write In JSX (.jsx, .tsx, MDX) In Astro
size="small" The size property, else the size attribute The same
selectedItem={item} A property by its name The same
onsubmit-start={fn} The submit-start event, as React 19 and Preact read it An attribute
on:submit-start={fn} The event, Solid’s way; so are prop:, attr:, bool: An attribute

Only your components get completed in these files: standard HTML elements are left to the editor’s own support for the language. In Astro, the templates in the frontmatter and in <script> elements are Lit templates. In MDX, fenced and inline code and import/export blocks are skipped.

The typed event handler features and the event-type check read Lit’s @event=${…} bindings only.

From Go to implementation (and definition) goes to
A component’s tag Its class
An attribute or .property The field, accessor or static properties entry, in the class or the base class declaring it
An attribute with no property Its entry in observedAttributes
An @event The event name in the new CustomEvent('…') the class dispatches, else the class

Find references, from the same places, lists every use in the workspace: opening and closing tags, and an attribute or property under either of its names (size and .size are one member). It searches every script it reads components from, the HTML files at each package’s root, the HTML, Astro and MDX files in the folders it reads, and any open file.

A component from a dependency’s manifest has no source to go to; its uses are still found.

In a template TypeScript gives an inline handler’s parameter no type, since nothing tells it which event the binding is for. The server knows, and helps until the type is written:

html`<x-button @submit-start=${(ev) => ev.detail.‸}></x-button>`;
// ^^ inlay hint: CustomEvent<{size: 'small' | 'large'; at: number}>
// completion after ev.detail.: size, at
  • Inlay hint. The event’s type after the untyped parameter.
  • Quick fix, “Add event type”. Writes it in, after which TypeScript completes and checks the handler itself:
    html`<x-button
    @submit-start=${(ev: CustomEvent<{size: 'small' | 'large'; at: number}>) => go(ev)}
    ></x-button>`;
  • Completion. After ev.: detail and what every event has (target, preventDefault…); after ev.detail.: the keys of its shape.

A handler whose parameter expects something the event doesn’t send is an error, event-type. It checks method references and typed inline handlers:

class App extends LitElement {
render() {
return html`<x-button @submit-start=${this.#onStart}></x-button>`;
// ^^^^^^^^^^^^^ `#onStart` takes `CustomEvent<{thing: string}>`,
// but `submit-start` sends `CustomEvent<{size: 'small' | 'large'; at: number}>`: `detail.thing` is missing.
}
#onStart(e: CustomEvent<{thing: string}>) {}
}

It reports a detail key the handler expects that the event doesn’t send, and one primitive type where another is sent (string for number), nested keys included. Fewer keys than sent, optional keys, named types such as CustomEvent<Item>, unions and other event classes are never reported: a reported conflict is a real one.

Unknown properties and invalid values in a css template are warnings, css. Lines with a ${…} aren’t checked, since the CSS around a hole is often incomplete on purpose.

static styles = css`
:host { colo: red; }
/* ^^^^ Unknown property: 'colo' */
.gap { margin: ${unsafeCSS(gap)}; }
/* not checked */
`;

A mistake in the settings, such as an unknown key or severity, is a warning on the first line of the file that holds it. So is a lit-devtools.config.ts that a Vite config overrides.

In a css template the server answers with VS Code’s CSS language service: properties, values, selectors and at-rules complete, and hover shows their docs. It adds what only your components’ docs know:

static styles = css`
.row { gap: var(--x‸); }
/* offered: --x-button-gap = 4px, from <x-button> */
x-button::part(‸) {}
/* offered: base */
`;

Set them in litPlugin({languageServer}) in a Vite config, or as languageServer in a lit-devtools.config.{ts,mts,js,mjs,json} in a project without one. When both exist, the Vite config’s settings win as a whole and the config file is reported as ignored.

Setting Default Meaning
scan the manifest option’s scan, else ['src'] Folders to read components from, relative to the package
manifests [] More manifests to read: paths relative to the package, or URLs
ignoreTags [] Tags no problem is reported for, with * wildcards
ignoreAttributes [] Attributes accepted on any element, with * wildcards
diagnostics see below Each rule’s severity: error, warning, info, hint or off
templateTags ['html', 'svg'] Tags of the markup templates to read
cssTemplateTags ['css'] Tags of the CSS templates to read
Rule Default Reports
event-type error A handler whose parameter doesn’t fit its event
css warning A problem in a css template

ignoreTags, ignoreAttributes, manifests, entries and the rules unknown-element, unknown-attribute, unknown-binding, invalid-value and missing-import are read and validated but not acted on yet.

@oddsquad/lit-devtools/oxlint (or @oddsquad/oxlint-plugin-lit on its own) reports the same problems in a lint run, one rule per problem kind, named lit-devtools/<rule> so they don’t clash with eslint-plugin-lit’s lit/ rules:

Rule Reports
lit-devtools/event-type A handler whose parameter doesn’t fit its event
lit-devtools/css A problem in a css template

Its default export has configs.recommended, a config for extends in oxlint.config.ts or in Vite+’s lint: the plugin, with each rule at its default severity from the table above (warning becomes warn). Without it, the rules are off until turned on in Oxlint’s rules. Either way Oxlint’s config sets their severity; the diagnostics setting applies to the editor only. The other settings are read as above. // oxlint-disable comments silence them. The plugin also registers lit-devtools/unknown-element, lit-devtools/unknown-attribute, lit-devtools/unknown-binding, lit-devtools/invalid-value and lit-devtools/missing-import, which report nothing yet; recommended already turns on the ones the editor does. It reads JavaScript and TypeScript files only. Check it in CI with Oxlint sets it up.

Source For
Your sources, read live from open buffers Your components, in every package of the workspace
Your dependencies’ Custom Elements Manifests Components from libraries, in the packages that depend on them
VS Code’s HTML data HTML elements, attributes, global attributes and DOM events
svg-element-attributes, plus short descriptions SVG elements and attributes
TypeScript’s DOM event maps The interface each DOM event dispatches (click: MouseEvent)
VS Code’s CSS language service CSS in css templates