Skip to content

The editor

If you write TypeScript or JSX, the types help you: lt. shows the whole system, and a variant that does not exist stops the build. See Type-safe use. If you write markup without JavaScript, in a Go template, a Razor view, PHP or hand-written HTML, no compiler looks at the attributes. Then the editor has to tell you, and it does.

The extension LO-TEK Interfaces is on the Marketplace. Search for “LO-TEK” under Extensions, or run:

Terminal window
code --install-extension lo-tek.interfaces

What you get, in HTML files and in the template languages:

  • Completion of <lt-field>, <lt-popover> and the other elements, with their attributes and allowed values. Inside class="…" the lt- classes are completed, and inside an attribute a class takes, such as data-variant on lt-button, the values the builder function knows.
  • Hover help when you hold the mouse over an lt- class, and in HTML files also over an element, with a link to the component page, or over an attribute.
  • Error messages, as red and yellow squiggles: an element that does not exist, an attribute the element does not have, a value outside the list, a class such as lt-buton, and data-variant="secundary" on a button. <lt-field> without a control or without a label gets the same message the component gives in the browser. And what the template must write itself for the components to work without JavaScript and survive an update from the server: a label that neither has for pointing to the field nor stands around it, a panel in <lt-popover> without an id, a tab that points to a panel that does not exist, a tab list without a name, role="tab" on something that is not a link, help text inside <label>, data-value without .lt-suggestion__value, a hidden error text next to aria-invalid="true", the removed invalid, required-marker and control-id on <lt-field>, and an id the component made itself, copied in from DevTools.
  • Quick fixes where there is one right answer: the light bulb changes lt-buton to lt-button, removes invalid="false" and the removed attributes, inserts a label, gives the tab list a name, and gives the field an id and the label for in one go, from the field’s name.
  • Snippets: type lt-popover and pick the snippet, and the markup from the component page is there. The id is a tab stop: type it once, and for on the label and href on the tabs follow along.

The template languages it works in without settings: PHP, Razor, Astro, Svelte, Vue, Twig, Blade, Jinja, Django, Handlebars, ERB, Go templates, EJS, Liquid, Nunjucks and Edge. Where there is template syntax, it stays out of the way: a template is not whole, and the control can be in a partial. The list is the setting lotek.languages.

The error messages and quick fixes come from the language server, lotek lsp, the same one Neovim and Helix use. It checks against the LO-TEK version the project has installed, and against components you have taken over, see The language server. Completion is generated from the components in the repository. The extension has its own version number and does not follow the package, since it only changes when an element, a class or an attribute does.

IntelliJ IDEA Ultimate and WebStorm need no extension. From @lo-tek/interfaces 0.15.0 the npm package includes web-types.json, JetBrains’ own format, and the IDE picks it up as soon as the package is in the project’s package.json. You get completion of the elements, attributes, values and lt- classes, and hover help with a link to the component page. Checked in IntelliJ IDEA Ultimate 2026.2.

web-types.json is read only for files. If the markup is in a string, such as an HTML template in Kotlin or TypeScript, you get no suggestions from it. The plugin LO-TEK Interfaces covers that: search for “LO-TEK” under Settings → Plugins → Marketplace. It completes the lt- classes inside class="…" in a string marked as HTML:

// language=HTML
fun saveButton() = """
<button class="lt-button" type="submit">Save</button>
"""

// language=HTML and @Language("HTML") are IntelliJ’s own markers, and need no dependency. Without such a marker the string is just text to the IDE, and then there is nothing for the plugin to attach to. Completion gives only the classes, not the elements or attributes. Checked in Kotlin and TypeScript in IntelliJ IDEA Ultimate 2026.2.3.

From version 0.4.0 the plugin has live templates: type lt-field and press Tab, and the markup from the component page is there, with the id as a variable and for following along. They are made from the same recipes as the snippets in VS Code.

From version 0.3.0 the plugin also gives the error messages, in HTML files and in marked strings: an element, a class, an attribute or a value that does not exist, with a quick fix where there is one right answer. The check is the same Rust core that lotek check and VS Code use, run in the IDE’s own JVM. It needs neither Node nor the IDE’s JavaScript support, and looks for the manifest the way the language server does, from the file upwards to the project folder, so components you have taken over are known. Completion reads the classes from the same manifest, so it suggests the components you have taken over and never a class the check rejects. A finding you want to keep is suppressed with <!-- lotek-ignore-next rule-name --> above the tag.

The error messages and quick fixes are available as a language server, lotek lsp, for every editor that speaks LSP. It is written in Rust, is the same check as lotek check, and is started in whichever way suits the project:

If you haveThe command
Nodenpx @lo-tek/interfaces lsp
Java 17 or newerjava -jar lotek.jar lsp
wasmtimewasmtime run --dir=/::/ --env LOTEK_PACKAGE=/path/to/package lotek.wasm lsp
cargo installlotek lsp

Where you get lotek.jar, lotek.wasm and the executable is described on Command line. wasmtime sees only the folders it is given, and --dir=/::/ gives it the whole file system, with the same paths the editor uses. It is the weakest of the hosts. See The drawbacks of wasmtime.

The server checks against what the project actually has. It goes from the file’s folder upwards, the way Node finds a package, and looks in each folder for build/lotek/manifest.json, which the Gradle plugin writes, and then for node_modules/@lo-tek/interfaces/manifest/manifest.json, the installed package. The nearest folder with one of them wins, and it does not look above the root of the workspace. In a monorepo, a page in apps/web is therefore checked against the package apps/web has installed. If the file is outside the workspace, such as a scratch file, or the editor has not given a workspace, it does not search from the file at all: then the manifest at the root of the workspace applies, if there is one. If none of them exist, npx, java -jar and VS Code check against the package they bring with them. The executable and wasmtime check against the package LOTEK_PACKAGE points to, and without it they check nothing and say so. The file is read again when it changes, so a new build or an upgrade reaches the editor without a restart.

In Neovim 0.11 or newer:

vim.lsp.config("lotek", {
cmd = { "npx", "@lo-tek/interfaces", "lsp" },
filetypes = { "html", "htmldjango", "php", "gotmpl", "eruby", "jinja", "twig", "blade" },
root_markers = { "package.json", "build.gradle.kts", ".git" },
})
vim.lsp.enable("lotek")

In Helix, in languages.toml, next to the HTML server:

[language-server.lotek]
command = "npx"
args = ["@lo-tek/interfaces", "lsp"]
[[language]]
name = "html"
language-servers = ["vscode-html-language-server", "lotek"]

Zed starts only language servers that a Zed extension has registered, and no such extension for LO-TEK exists yet.

The server gives findings and fixes. It does not do completion or hover help yet, so those still come from the extension in VS Code, and in JetBrains from web-types.json for files and from the plugin for HTML in strings. initializationOptions can have manifest, a path to a manifest that takes precedence over the others, and rendered: true, which checks every file as a whole page, with the wiring.

The same check exists without an editor. lotek check reads files, standard input or an address, and writes each finding as file:line:column: error: message, or warning: where it is a warning. One finding gives an error code, so it can run in CI:

Terminal window
npx @lo-tek/interfaces check templates/*.html
npx @lo-tek/interfaces check http://localhost:8080/form

If the server makes the HTML in code, as strings in a Kotlin app, there is no template file to check. Then you check the page the server sends, with the address or with --rendered in the app’s own tests. Flags, output and examples for tests in JavaScript and Kotlin are on Command line.

The editor sees one file at a time. A label in one partial and a field in another only fit together once the page is assembled. check sees that on a whole page, where it requires every id that is pointed to to exist and every field to be wired. See Command line. In the browser, the web component reports it if the wiring is missing. See Markup and behaviour.