A best-in-class Neovim plugin providing native CSS LSP experience for styled-components using TreeSitter language injection. Match or exceed VS Code features with Neovim's native capabilities!
- π TreeSitter Injection: Native CSS syntax highlighting and LSP support in template literals
- π‘ Full CSS LSP: Completions, hover documentation, and diagnostics from cssls
- β‘ Zero Overhead: Uses Neovim's built-in TreeSitter injection (no virtual buffers, no hacks)
- π― Auto-Setup: Automatically configures injection queries and cssls
- π Native Experience: Works exactly like editing a .css file
- π§ Extensible: Supports
styled,css,createGlobalStyle, andkeyframes
This plugin uses TreeSitter language injection - the same approach VS Code uses, but better! When you type in a styled-component template:
const Button = styled.div`
display: flex;
^^^^^^^^^^^^^^ β TreeSitter marks this as CSS!
align-items: center;
^^^^^^^^^^^^^^^^^^^^ β cssls provides completions/hover/diagnostics!
`;Architecture:
- Plugin installs TreeSitter injection queries
- Neovim TreeSitter automatically detects styled-component templates
- Injected CSS regions get native LSP support from cssls
- You get the same experience as editing a .css file!
No virtual buffers, no position mapping, no race conditions - just native Neovim features! π
- Neovim >= 0.10.0
- nvim-treesitter with TypeScript/JavaScript parser
- nvim-lspconfig (optional for Neovim 0.11+, uses native
vim.lsp.config) - vscode-css-language-server (for LSP features)
Note: Neovim 0.11+ users can use the native
vim.lsp.configAPI withoutnvim-lspconfig. The plugin automatically detects and uses the appropriate API.
npm install -g vscode-langservers-extractedThis provides vscode-css-language-server with:
- Full CSS property/value completions
- Hover documentation
- CSS validation and diagnostics
- Syntax checking
-- In your lazy.nvim plugin spec (e.g., ~/.config/nvim/lua/plugins/styled-components.lua)
return {
-- styled-components.nvim: CSS in JS with TreeSitter injection
{
"crafts69guy/styled-components.nvim",
dependencies = {
"nvim-treesitter/nvim-treesitter",
"neovim/nvim-lspconfig", -- Optional for Neovim 0.11+
},
ft = { "typescript", "typescriptreact", "javascript", "javascriptreact" },
opts = {
enabled = true,
debug = false,
auto_setup = true,
},
},
-- blink.cmp: Configure with styled-components integration
{
"saghen/blink.cmp",
dependencies = { "crafts69guy/styled-components.nvim" },
opts = function(_, opts)
local styled = require("styled-components.blink")
-- Ensure sources table exists
opts.sources = opts.sources or {}
opts.sources.default = opts.sources.default or { "lsp", "path", "snippets", "buffer" }
opts.sources.providers = opts.sources.providers or {}
-- Add styled-components to default sources
table.insert(opts.sources.default, "styled-components")
-- Configure LSP source to filter cssls completions
opts.sources.providers.lsp = vim.tbl_deep_extend("force",
opts.sources.providers.lsp or {},
{
override = {
transform_items = styled.get_lsp_transform_items(),
},
}
)
-- Register styled-components completion source
opts.sources.providers["styled-components"] = {
name = "styled-components",
module = "styled-components.completion",
enabled = styled.enabled,
}
return opts
end,
},
}Why this config?
ft: Lazy loads styled-components on TypeScript/JavaScript filetypes- Uses blink.cmp's official override API (stable, future-proof)
- Filters cssls completions to ONLY appear in styled-component templates
- Zero timing issues, no internal patching
- Result: Reliable, maintainable, works perfectly with LazyVim!
-- Setup styled-components
require("styled-components").setup({
enabled = true,
debug = false,
auto_setup = true,
-- Optional: Completion performance tuning
completion = {
cache_ttl_ms = 100, -- Context detection cache TTL (ms)
},
-- Optional: custom cssls configuration
cssls_config = {
settings = {
css = {
validate = true,
lint = {
unknownAtRules = "ignore",
},
},
},
},
})
-- Configure blink.cmp integration
local styled = require("styled-components.blink")
require("blink.cmp").setup({
sources = {
default = { "lsp", "path", "snippets", "buffer", "styled-components" },
providers = {
lsp = {
override = {
transform_items = styled.get_lsp_transform_items(),
},
},
["styled-components"] = {
name = "styled-components",
module = "styled-components.completion",
enabled = styled.enabled,
},
},
},
})With auto_setup = true (default), the plugin automatically:
- β Installs TreeSitter injection queries
- β Configures cssls to work with TypeScript/JavaScript files
- β Enables CSS completions, hover, and diagnostics in styled-components
Just start typing!
The plugin recognizes these styled-components patterns:
// β
styled.element
const Box = styled.div`
display: flex;
`;
// β
styled(Component)
const StyledButton = styled(Button)`
color: red;
`;
// β
css helper
import { css } from 'styled-components';
const styles = css`
margin: 10px;
`;
// β
createGlobalStyle
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
body { margin: 0; }
`;
// β
keyframes
import { keyframes } from 'styled-components';
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;In any styled-component template, you get:
Completions:
- Type
disβ seedisplay,display-inside, etc. - Type
display: fβ seeflex,flow-root, etc. - Full CSS property and value completions!
Hover Documentation:
- Move cursor to any CSS property
- Press
Kβ see MDN documentation!
Diagnostics:
- Typo:
colr: red;β Error: Unknown property - Invalid:
display: flexxx;β Error: Invalid value
All powered by native cssls!
{
enabled = true, -- Enable/disable the plugin
debug = false, -- Show debug messages
auto_setup = true, -- Auto-setup injection and cssls
filetypes = { -- Supported filetypes
"typescript",
"typescriptreact",
"javascript",
"javascriptreact",
},
cssls_config = {}, -- Custom cssls configuration (merged with defaults)
-- Completion source performance options
completion = {
cache_ttl_ms = 100, -- Context detection cache TTL (ms)
-- Higher = less overhead, but slightly stale detection
-- Lower = more responsive, but more TreeSitter queries
},
}
-- Note: blink.cmp integration is now configured separately
-- See the "blink.cmp Integration" section aboveThis plugin provides a custom completion source for blink.cmp using the official Provider Override API.
Add both plugins to your lazy.nvim config:
return {
{
"crafts69guy/styled-components.nvim",
ft = { "typescript", "typescriptreact", "javascript", "javascriptreact" },
opts = {},
},
{
"saghen/blink.cmp",
dependencies = { "crafts69guy/styled-components.nvim" },
opts = function(_, opts)
local styled = require("styled-components.blink")
opts.sources = opts.sources or {}
opts.sources.default = opts.sources.default or { "lsp", "path", "snippets", "buffer" }
opts.sources.providers = opts.sources.providers or {}
-- Add styled-components source
table.insert(opts.sources.default, "styled-components")
-- Filter cssls completions using override API
opts.sources.providers.lsp = vim.tbl_deep_extend("force",
opts.sources.providers.lsp or {},
{
override = {
transform_items = styled.get_lsp_transform_items(),
},
}
)
-- Register styled-components provider
opts.sources.providers["styled-components"] = {
name = "styled-components",
module = "styled-components.completion",
enabled = styled.enabled,
}
return opts
end,
},
}The Problem:
- styled-components.nvim configures cssls to attach to TypeScript/JavaScript files (required for TreeSitter injection)
- blink.cmp's LSP source shows ALL completions from ALL attached LSP clients
- Without filtering, users see CSS completions everywhere (React components, hooks, normal TypeScript code)
The Solution:
- Uses blink.cmp's official override API to filter cssls completions
transform_itemschecks if cursor is inside TreeSitter-injected CSS region- CSS completions ONLY appear in styled-component templates
- Outside templates, cssls completions are filtered out
Benefits:
- β Uses official, stable blink.cmp API (future-proof)
- β No internal patching or hacks
- β Zero timing issues
- β Transparent and easy to debug
- β Works perfectly with LazyVim
Performance Notes:
- Context detection is cached (100ms TTL) to minimize overhead
- Smart pattern verification prevents false positives
- Only triggers inside styled-component templates
- Typical overhead: ~0.1-5ms per completion request
require("styled-components").setup({
cssls_config = {
settings = {
css = {
validate = true,
lint = {
unknownAtRules = "ignore",
vendorPrefix = "warning",
},
},
},
},
})If you prefer manual control:
require("styled-components").setup({
auto_setup = false, -- Disable auto-setup
})
-- For Neovim 0.11+ (Native API):
vim.lsp.config.cssls = {
cmd = { 'vscode-css-language-server', '--stdio' },
root_markers = { 'package.json', '.git' },
filetypes = { 'css', 'scss', 'less', 'typescript', 'typescriptreact', 'javascript', 'javascriptreact' },
}
vim.lsp.enable('cssls')
-- For Neovim 0.10.x (nvim-lspconfig):
require('lspconfig').cssls.setup({
filetypes = { 'css', 'scss', 'less', 'typescript', 'typescriptreact', 'javascript', 'javascriptreact' },
}):lua require("styled-components").print_status()This shows:
- Is injection available?
- Is injection active in current buffer?
- Does buffer have styled-components import?
- Current injected language at cursor
- Full configuration
Plugin not loading / No completions:
-
Check cssls is installed:
:!which vscode-css-language-server
-
Check LSP is attached:
:LspInfo
Should show
csslsattached to.tsxfiles. -
Check injection is working:
:lua print(require("styled-components").is_injection_working())
-
Check you're in a styled-component: Place cursor in template literal and run:
:lua require("styled-components").print_status()
TreeSitter errors:
Install parsers:
:TSInstall typescript tsx javascript
:TSUpdatecssls not attaching:
Ensure you have nvim-lspconfig installed and loaded before this plugin.
LazyVim users: Error on startup about Snacks:
This is fixed in the latest version! The plugin now automatically loads queries on VimEnter to avoid timing issues.
If you're using an older version and have an init function in your config, you can remove it:
-- β OLD (not needed anymore)
init = function()
require("styled-components").load_queries_early()
end,
-- β
NEW (automatic)
-- Just use opts or config, no init needed!
opts = { debug = false }The plugin handles timing automatically to work with UI plugins like Snacks, lualine, etc.
| Metric | Value |
|---|---|
| Completion latency (in CSS) | ~5-15ms (LSP request) |
| Context detection (cached) | ~0.1ms (cache hit) |
| Context detection (uncached) | ~1-3ms (TreeSitter query) |
| Memory overhead | ~1KB (small cache) |
| CPU overhead | ~0% (efficient caching) |
| Startup time | ~5ms (query installation) |
Optimization Strategy:
- β Context Detection Caching: 100ms TTL cache prevents repeated TreeSitter queries
- β
Smart Trigger Characters: Only CSS symbols (
:,;,-), not a-z - β Fast Early Return: Exits immediately if not in styled-component template
- β Cache Cleanup: Automatic cleanup prevents memory leaks
Performance Impact:
Before optimization: 11 triggers Γ 5ms = ~55ms overhead per line
After optimization: 2 triggers Γ 0.1ms = ~0.2ms overhead per line
Improvement: 275x faster! π
Comparison with other approaches:
- Virtual Buffers: ~50ms + 500ms init + bugs
- Static Data: ~1ms but limited features
- TreeSitter Injection + Smart Caching: ~0.1-5ms with full LSP features β
| Feature | VS Code | styled-components.nvim |
|---|---|---|
| Syntax Highlighting | β TextMate | β TreeSitter (better!) |
| CSS Completions | β typescript-plugin | β Native LSP |
| Hover Docs | β Yes | β Yes |
| Diagnostics | β Yes | β Yes |
| Performance | ~1-5ms | ~1-5ms |
| Architecture | TypeScript plugin | TreeSitter injection |
Result: Feature parity or better! π
Contributions welcome! This plugin uses:
- TreeSitter injection queries (in
queries/directory) - Neovim's native LSP client
- No external dependencies (besides cssls)
MIT
- Inspired by vscode-styled-components
- Uses vscode-css-language-server
- Built with Neovim's native TreeSitter and LSP