alpinejs.nvim
Alpine.js Highlighting and Completion for Neovim
alpinejs.nvim is a free, open source Neovim plugin that brings Alpine.js developer support to Neovim: directive and magic-property highlighting, completion for every major engine, hover documentation, unknown-directive diagnostics, go to definition, and VS Code-format snippets. It is at feature parity with Alpine.js Tools, the VS Code extension this plugin is based on, and covers the same twelve languages: HTML, EJS, PHP, Twig, Nunjucks, Blade, Liquid, Jinja2, Astro, and JSX/TSX.
All dependencies beyond Neovim itself are optional. The plugin degrades gracefully when nvim-treesitter, a completion engine, or LuaSnip are absent.
Features
-
Directive and magic-property highlighting
Highlighting for Alpine directives (
x-data,x-on:click,:class,@click), shorthand forms (:attr,@event),.modifier.chains, and the inline JS expressions inside directive values, including magic properties ($el,$refs,$store, ...). -
Two highlighting paths, chosen automatically per buffer
Tree-sitter (primary): query additions in
queries/html/andqueries/javascript/layer onto whateverhtml/javascriptparse tree is already active — including.ejsbuffers whose HTML content is injected via ejs.nvim'sembedded_template -> htmlinjection, with no.ejs-specific code. Legacy:syntaxregex (fallback): used on buffers where Tree-sitter highlighting isn't active, so users without nvim-treesitter still get highlighting. -
Astro and JSX/TSX
.astrofiles are treated as markup, since an Astro template is markup and Astro passes@clickand:classthrough to the HTML — and Astro's ownclient:load,transition:animate, andset:htmlare untouched, because a:shorthand only counts when the colon starts the attribute name. JSX and TSX get their own Tree-sitter queries written against(jsx_attribute (property_identifier))and(jsx_attribute (jsx_namespace_name)), since neither shares the html grammar's node types. Enable or opt out via thejsx_filetypesoption. -
Long forms only in JSX, verified against the TypeScript compiler
@clickand:classare TS1003 syntax errors in JSX. Runningtscagainst modifier chains found the same forx-on:click.preventandx-model.lazy, which the VS Code extension had not documented. Completion therefore offers no shorthands and no modifiers in JSX, and a diagnostic explains each — tsserver reports only "Identifier expected" pointing at the@, which says nothing about Alpine or about what to write instead. -
Configurable filetype list
Defaults to the nine markup languages Alpine.js Tools supports (HTML, EJS, PHP, Twig, Nunjucks, Blade, Liquid, Jinja2, Astro), mapped to 10 filetype strings, plus a separate
jsx_filetypeslist forjavascript,javascriptreact, andtypescriptreact. Both are correctable viasetup(). -
Completion for every major engine
One engine-agnostic core feeds adapters for nvim-cmp, blink.cmp (registered automatically via its runtime API on v1.6+, appending to a per-filetype list so it cannot displace sources you configured yourself), and a complete-function covering Neovim 0.12's built-in completion, mini.completion, and coq_nvim. Directive names, modifier chains after a
., magic properties,$refs.from the document'sx-refattributes,$store.andx-data="component"from a lazy workspace index, and directive-value properties from the nearestx-dataobject literal. Each adapter is skipped silently when its engine is absent. -
Hover, diagnostics, and go to definition
Kshows the type signature and docs link for the directive, shorthand, modifier, or magic property under the cursor, falling through tovim.lsp.buf.hover()orkeywordprgeverywhere else so it never takes the key from your language server. Unknownx-*attributes get a warning with a "did you mean" suggestion, applied by:AlpineFix.:AlpineDefinitionjumps to theAlpine.data()orAlpine.store()registration, backed by a lazy workspace index that skipsnode_modules,dist, andvendorand never parses a file that does not contain the stringAlpine. -
Front matter is skipped
A
---fenced block at the top of a file is front matter in every ecosystem that uses one, and it is never markup. The rule is keyed on the fence rather than the language, so Astro's TypeScript frontmatter and YAML front matter in Jekyll, Hugo, and Eleventy.html/.liquidtemplates are all skipped. The closing fence must be a line containing exactly---, so a stray---cannot blank out a file. -
Snippets
46 snippets shipped in the same
package.json+snippets/*.jsonlayout asrafamadriz/friendly-snippets, loaded by LuaSnip's own VS Code loader, with no LuaSnip dependency in this plugin's code at all. The same forms are also served by the completion source, so they work in every engine regardless of snippet plugin. -
Health checks and a test suite
:checkhealth alpinejs(or:AlpineHealth) diagnoses your setup. 86 tests run withnvim -l tests/run.lua, needing no test framework or plugin dependencies.
Prerequisites
All dependencies are optional. The plugin degrades gracefully when any of them are absent.
| Dependency | Purpose | Required |
|---|---|---|
| Neovim >= 0.9 | vim.treesitter.query.add_directive() | Yes |
nvim-treesitter/nvim-treesitter | html/javascript parsers, for the primary highlighting path (plus astro, tsx for those languages) | Recommended |
hrsh7th/nvim-cmp | Completion | Optional |
saghen/blink.cmp | Completion (v1.6+ registers automatically) | Optional |
L3MON4D3/LuaSnip | Snippet engine | Optional |
Installation
lazy.nvim
{
"connorontheweb/alpinejs.nvim",
ft = {
"html", "ejs", "php", "twig", "blade", "liquid",
"jinja", "htmldjango", "nunjucks", "astro",
"javascript", "javascriptreact", "typescriptreact",
},
dependencies = {
"nvim-treesitter/nvim-treesitter", -- optional, recommended
"hrsh7th/nvim-cmp", -- optional
"L3MON4D3/LuaSnip", -- optional
},
opts = {},
}
With opts = {}, lazy.nvim calls setup() for you. If the default filetype list already matches your setup, you can omit opts entirely — plugin/alpinejs.lua bootstraps setup() with defaults the first time any default filetype is opened.
No plugin manager (built-in packages)
mkdir -p ~/.local/share/nvim/site/pack/plugins/start
git clone https://github.com/connorontheweb/alpinejs.nvim \
~/.local/share/nvim/site/pack/plugins/start/alpinejs.nvim
vim-plug
Plug 'connorontheweb/alpinejs.nvim'
packer.nvim
use 'connorontheweb/alpinejs.nvim'
mini.deps
MiniDeps.add('connorontheweb/alpinejs.nvim')
require('alpinejs').setup()
Post-install: install the Tree-sitter parsers (recommended)
:TSInstall html javascript astro tsx
Without these, the plugin falls back to the legacy :syntax regex highlighter automatically, so nothing needs to be configured differently either way.
Configuration
All options default as shown. Pass overrides to require('alpinejs').setup(), or via opts if using lazy.nvim:
require('alpinejs').setup({
-- markup buffers to activate Alpine support on; see config.lua for the
-- language -> filetype-string mapping this list assumes
filetypes = {
'html', 'ejs', 'php', 'twig', 'blade', 'liquid',
'jinja', 'htmldjango', 'nunjucks', 'astro',
},
-- JavaScript buffers that may contain JSX; set to {} to opt out
jsx_filetypes = { 'javascript', 'javascriptreact', 'typescriptreact' },
highlight = true, -- enable the :syntax regex fallback path
diagnostics = true, -- warn on unknown x-* directives (:AlpineFix applies the suggestion)
hover = true, -- K shows docs, falling through to LSP elsewhere
workspace = true, -- lazy index of Alpine.data()/Alpine.store() registrations
completion = {
cmp = true, -- register the nvim-cmp source
blink = true, -- register the blink.cmp source (v1.6+)
omni = false, -- complete-function for built-in/mini.completion/coq_nvim
snippets = true, -- insert x-data="{ | }" rather than the bare label
},
})
Each completion adapter is skipped silently when its engine is not installed. The pre-1.1.0 top-level cmp spelling is still honoured.
Commands
:AlpineHealth, :AlpineFix (apply the suggested directive under the cursor), :AlpineDefinition (jump to the Alpine.data() / Alpine.store() registration), and :AlpineRescan (rebuild the workspace index).
Health Checks
Run :checkhealth alpinejs (or :AlpineHealth) to diagnose your setup: Neovim version, active filetype list, whether the html Tree-sitter parser is available, whether your completion engines and LuaSnip are installed, and, for every configured filetype other than HTML, whether highlighting will actually activate for it. Unlike the extension's original silent findFiles truncation, reaching the workspace file cap is reported here instead of quietly returning partial results.
How It Works
The Tree-sitter path is purely declarative: query files Neovim's loader merges across the whole runtimepath whenever a buffer already has Tree-sitter highlighting active for html or javascript. Every styled capture (@alpinejs.directive, @alpinejs.modifier, @alpinejs.shorthand, @alpinejs.magic) sets an explicit priority of 110 via (#set! @capture "priority" 110), and the unstyled anchor captures set 90. This isn't decorative: when multiple captures tie on Tree-sitter's default priority of 100 at the exact same range, Neovim renders whichever capture comes last in the merged query text — an order that depends on runtimepath plugin load order and can't be controlled or predicted. Without the explicit priorities, unstyled anchor captures could end up ordered after the styled ones and silently blank them out instead of layering underneath, which is exactly what happened before the 1.0.1 fix.
Because .ejs buffers parsed via ejs.nvim's embedded_template parser inject a genuine, separate html language tree for their HTML content, these same query files apply there too automatically, with no .ejs-specific code anywhere in this plugin.
The legacy :syntax fallback layers :syntax match/region rules into the existing htmlTag region via containedin=, reusing the real syntax/javascript.vim grammar for directive values via :syntax include. It's only applied to a buffer if Tree-sitter highlighting isn't (and doesn't become) active for that buffer. containedin=htmlTag is a group-name hook, not a filetype-specific one, so it activates for any filetype whose syntax happens to define htmlTag, with no per-language code in this plugin — verified directly for PHP, htmldjango, and liquid.
Three classes of bug the VS Code extension had to fix cannot arise here, and the test suite asserts each rather than assuming it. Livewire's wire:model being read as the : shorthand, TextMate grammar scope targeting, and unknown-directive warnings on ordinary English are all consequences of matching Alpine syntax with regexes across an entire document. Both highlighting paths here are anchored to attribute positions — Tree-sitter matches (attribute (attribute_name)) nodes and the regex fallback is containedin=htmlTag — so wire:model fails the ^[:@] test on its first character, and prose can never reach an attribute node at all. Diagnostics are built on the same guarantee: candidates come from the document tag scan rather than a document-wide regex, so hyphenated prose, translate-x-1/2, const diff = x-y > 0, and typos inside <!-- --> are all out of scope by construction.
Completion dispatches on language family, because JSX needs its own scanner: < is also the less-than operator and a generic-argument delimiter, so the markup scan would report "inside a tag" across large stretches of ordinary TypeScript. The JSX scan accepts a < only when an element name follows and everything up to the cursor could appear in an opening tag, rejecting new Map<string, number>() on the , and i.n < 5 && i.n > 1 on the &. Tree-sitter is deliberately not used for JSX completion despite being available: an unterminated <div x- collapses the whole statement to a bare ERROR node, indistinguishable from const diff = x-. It is used for JSX diagnostics, which run over a complete document where the tree is exact.