A native Rust LSP server that powers the CSS Lens Zed extension.
It scans every .css file in your workspace and wires up a full set of HTML authoring features — completions, hover documentation, diagnostics, navigation, and refactoring — all backed by a real understanding of your stylesheet.
- Class completions — typing inside
class="..."suggests every class name found in your CSS files, filtered by what you've already typed (case-insensitive) - Already-used filtering — classes already present in the same attribute are excluded from suggestions
- Smart spacing — inserted names automatically get a leading or trailing space when the cursor is flush against another class name
- ID completions — typing inside
id="..."suggests ID selectors; suggestions stop once a value is present (IDs are single-value) - CSS variable completions — typing
--insidestyle="..."suggests custom properties (--primary-color, etc.) found in your CSS - Property and value completions — typing inside
style="..."suggests CSS property names and keyword values (e.g.display: flex) - Animation name completions — when the cursor is in an
animation-name:value insidestyle="", suggests every@keyframesname defined in your CSS
Hovering a class name in class="...", an ID in id="...", or a variable name in style="..." shows:
- The full selector (e.g.
.btn:is(.active, .focused)) - The source file and line number (e.g.
styles.css:42) - The
@mediaor@supportscontext if the rule is inside one - The
@layercontext if the rule is nested inside a named cascade layer (e.g.@layer base) - The CSS specificity score as
(a,b,c)— IDs, classes, and element types all counted correctly - Color values — hex,
rgb(), andhsl()values in the rule's properties are shown inline - The full property block in a syntax-highlighted code fence
- All definitions listed when the same name is defined in multiple files
- Declared value of a CSS custom property when hovering a
--variable-nameinstyle="" - CSS custom property hover in
.cssfiles — hovering--variable-namedirectly inside a CSS file shows its declared value, not just in HTML attributes
- Undefined class/ID — class and ID names in HTML are checked against only the CSS files linked from that specific page via
<link rel="stylesheet">(and their@importchains); a class that exists in an unlinked file is still flagged as an error on pages that never load it - Duplicate selector — if the same class or ID is defined more than once in the same CSS file, every definition after the first is flagged as a warning
- Unused-selector hint — CSS selectors not referenced in any currently-open HTML file receive a soft hint; suppressed when no HTML files are open; also suppressed for classes referenced in plain JavaScript via
classList.add/remove/toggle,getElementsByClassName, orquerySelector/querySelectorAllso vanilla DOM manipulation never triggers false positives - Duplicate class in attribute —
class="btn btn"with the same token listed more than once is flagged as a warning at the duplicate occurrence
- Go to definition —
Cmd+Clicka class inclass="..."or an ID inid="..."to jump to its definition in the source CSS file; when multiple files define the same name the editor presents a picker; also works for@keyframesnames inanimation-name: - Find all references — from a class or ID in an HTML attribute, finds every HTML file in the workspace that uses the same name
- Workspace symbol search — the Zed symbol palette lists every CSS class and ID selector across the entire workspace, searchable by name, with the source filename shown as context
- Rename — rename a class or ID from either an HTML attribute or a CSS selector definition; the LSP updates the CSS selector and every HTML attribute reference across the workspace atomically
- Create class/ID — when an undefined class or ID is flagged in HTML, a Quick Fix code action appends the new rule to the nearest linked CSS file
- Remove unused rule — an unused-selector hint offers a Quick Fix to delete the entire rule block from the CSS file; a guard ensures the action is only offered when every co-selector in the block is also unused
- Follows
@importchains recursively (with cycle detection) - Handles
@media,@supports,@layer, and other block at-rules via a proper brace-depth-aware parser; tracks@layernames so selectors nested inside named layers are attributed correctly - Extracts class and ID selectors from inside functional pseudo-classes —
:is(.foo, .bar),:has(.child),:where(.group),:not(.active)— so those names are fully indexed for completions, hover, and diagnostics - Extracts both class selectors (
.btn) and ID selectors (#hero) - Parses CSS custom properties (
--name: value) for variable completions and hover - Parses
@keyframesnames for animation-name completions, hover, and go-to-definition - Strips block comments while preserving line numbers
- Skips files larger than 500 KB to avoid stalling on minified bundles
- Excludes
node_modulesand hidden directories
51 HTML snippets are bundled with the extension and load automatically — no manual setup needed.
What's included:
html5— full page boilerplate with meta tags, Open Graph, theme-color, icons, skip-navigation link, and a semantic<header>/<main>/<footer>layoutlink—<link rel="stylesheet">for quickly adding a stylesheet referencemeta— meta tag with name and content attributestemplate—<template>element for vanilla JScloneNodepatternsslot— named<slot>for web components- Element snippets for every common HTML element, each pre-wired with the attributes that matter —
typeandaria-labelon buttons,altandloadingon images,autocompleteon inputs,scopeon table headers,datetimeon<time>,citeon blockquotes,low/high/optimumon meters,defer/asyncon scripts, and more
The LSP server is a plain Rust binary that speaks the Language Server Protocol over stdio. It is downloaded automatically by the Zed extension at install time — you do not need to install it manually.
On startup it walks every .css file in the workspace, parses all selectors and custom properties into an in-memory map, and responds to LSP requests from the editor. File changes are picked up incrementally via workspace/didChangeWatchedFiles — only the changed file is re-parsed. Diagnostics for open HTML files are pushed automatically whenever the CSS map changes.
Requires Rust installed via rustup.
git clone https://github.com/joshuaerney/css-lens
cd css-lens/lsp
cargo build --release
# binary is at target/release/css-lens# macOS Apple Silicon (native)
cargo build --release --target aarch64-apple-darwin
# macOS Intel
cargo build --release --target x86_64-apple-darwin
# Linux x86_64 (requires cargo-zigbuild or a GNU cross-toolchain)
cargo zigbuild --release --target x86_64-unknown-linux-gnuNew releases are built and published automatically by the GitHub Actions workflow in .github/workflows/release.yml when a v* tag is pushed.
GitHub release assets must be named exactly as follows for the Zed extension to find them:
| Platform | Asset filename |
|---|---|
| macOS Apple Silicon | css-lens-{version}-aarch64-apple-darwin.tar.gz |
| macOS Intel | css-lens-{version}-x86_64-apple-darwin.tar.gz |
| Linux x86_64 | css-lens-{version}-x86_64-unknown-linux-gnu.tar.gz |
Each .tar.gz must contain a single executable named css-lens.
MIT — see LICENSE.