Alpine.js Highlighting and Completion for Neovim
alpinejs.nvim is a free, open source Neovim plugin that adds Alpine.js directive highlighting, magic-property highlighting, nvim-cmp completion, and snippets to Neovim. It is available now on GitHub and dotfyle.
The State of Alpine.js Tooling in Neovim
Alpine.js has become a widely used framework for adding interactivity to server-rendered HTML, but dedicated Neovim support for it is essentially nonexistent. There are no maintained plugins covering Alpine v3, no Tree-sitter queries targeting directive attribute values, and no completion sources for Alpine magic properties. Developers working in Neovim with Alpine have had to rely on generic HTML tooling that treats x-data="{ open: false }" and @click="toggle()" as plain attribute strings with no tokenization inside them.
alpinejs.nvim was built to close that gap with full Alpine 3 coverage and support for the same template languages the Alpine.js Tools VS Code extension targets: HTML, EJS, PHP, Twig, Nunjucks, Blade, Liquid, and Jinja2.
Features
Two Highlighting Paths, Chosen Automatically
The most significant design decision in alpinejs.nvim is that it maintains two separate highlighting paths and selects between them automatically per buffer.
The Tree-sitter path is the primary path and activates on any buffer where Tree-sitter highlighting is already active for html or javascript. It is implemented entirely as declarative query files with no runtime Lua required. Neovim’s query loader merges these files across the runtimepath whenever a matching parse tree is available.
The legacy :syntax regex path is the fallback and activates on buffers where Tree-sitter highlighting is not active. It layers :syntax match and region rules into the existing htmlTag region via containedin=, giving users without nvim-treesitter (or without the html parser installed) full directive highlighting without any additional configuration.
Both paths are active simultaneously in a given Neovim install, each covering the buffers the other cannot.
Tree-sitter Highlighting
Three query files drive the Tree-sitter path, all using the ;; extends convention so they layer onto whatever HTML and JavaScript parse trees are already active.
queries/html/highlights.scm captures attribute names matching ^x- as @alpinejs.directive and names matching ^[:@] as @alpinejs.shorthand. It also captures the corresponding attribute value as @alpinejs.expression, an anchor capture left intentionally unstyled so the nested JavaScript tokens provide all the coloring.
queries/html/injections.scm injects javascript into those same attribute value ranges, giving directive values full JavaScript grammar highlighting for operators, strings, identifiers, and calls.
queries/javascript/highlights.scm captures bare identifiers exactly matching the ten magic property names ($el, $refs, $store, $watch, $dispatch, $nextTick, $data, $root, $id, $event) as @alpinejs.magic. This is a global addition to the javascript query, so it applies inside directive value injections and in standalone .js files alike.
Custom Query Directives for Modifier Chains
Alpine directive names like x-on:click.prevent.stop are single opaque leaf tokens in the HTML grammar with no child nodes, so Tree-sitter cannot structurally separate the base event name from the modifier chain within one attribute name node. alpinejs.nvim registers two custom query directives, #alpinejs-split-base! and #alpinejs-split-modifier!, that compute the byte offset of the first . in the leaf’s text and override each capture’s highlighted range via metadata. This is the same mechanism Neovim’s built-in #offset! directive uses. Both directives are registered unconditionally at plugin load time, before any query referencing them gets compiled.
Capture Priority
Every styled capture in both HTML query files sets an explicit priority of 110 via (#set! @capture "priority" 110). Unstyled anchor captures set 90. Without explicit priorities, when multiple captures tie at Tree-sitter’s default priority of 100 over the same byte range, Neovim renders whichever capture appears last in the merged query text. That merge order depends on runtimepath plugin load order, which alpinejs.nvim cannot control. Before the 1.0.1 fix, unstyled anchor captures were silently overwriting the styled ones in some load orders rather than layering underneath them. The explicit priority spread resolves this regardless of load order.
Legacy :syntax Regex Highlighting
The legacy path reuses syntax/javascript.vim for directive values via :syntax include, giving genuine JavaScript highlighting rather than custom regex patterns. One compatibility issue arises from the fact that javascript.vim‘s bare-quote string regions (javaScriptStringD and javaScriptStringS) start on the same " or ' character as this plugin’s own attribute-value region. Left unaddressed, both patterns compete for the same opening character and the attribute value collapses into a flat string token. The fix removes those two groups from the shared cluster and adds small dedicated regions for a string of the opposite quote type nested inside an attribute value (for cases like :class="x ? 'a' : 'b'"), which is the only nesting scenario that can actually occur inside an HTML attribute.
Unlike the Tree-sitter path, the regex path can fully separate x-on:click from .prevent.stop and color each . separator independently from the modifier name, since regex matching has no leaf-node restriction.
The containedin=htmlTag hook is not filetype-specific. It activates for any filetype whose syntax defines htmlTag, which covers PHP, htmldjango, and liquid as bundled Neovim filetypes. Whether a third-party Twig, Blade, Nunjucks, or Jinja2 plugin follows the same convention varies by plugin; :checkhealth alpinejs reports this definitively for the actual setup.
nvim-cmp Completion
The completion module is wrapped in pcall around require('cmp') and skips silently if nvim-cmp is not installed. Context detection works from line text alone with no Tree-sitter dependency: the module finds the start of the current tag, tracks quote parity to determine whether the cursor sits inside an open attribute value, and inspects the attribute name before the =. Inside an Alpine attribute value, it suggests magic properties. In bare attribute-name position, it suggests directive names. All candidates and documentation strings come from a centralized data table. The current implementation covers single-line tags.
Snippets
alpinejs.nvim ships snippets in the same package.json plus snippets/*.json layout used by rafamadriz/friendly-snippets. Any configuration that calls require('luasnip.loaders.from_vscode').lazy_load() picks them up automatically. There is no require('luasnip')anywhere in this plugin’s code, so there is no hard LuaSnip dependency and no version coupling.
Filetype Coverage
alpinejs.nvim activates for nine filetype strings covering eight template languages.
| Filetype string | Language |
|---|---|
html | HTML |
ejs | EJS |
php | PHP |
twig | Twig |
blade | Blade |
liquid | Liquid |
jinja, htmldjango | Jinja2 |
nunjucks | Nunjucks |
Jinja2 maps to both jinja and htmldjango because community plugins use both names. The full list is configurable via setup({ filetypes = {...} }) for setups where a third-party plugin registers a different filetype string for Twig, Blade, or Nunjucks.
Health Checks
:checkhealth alpinejs (also available as :AlpineHealth) reports the Neovim version, the active filetype list, whether the html Tree-sitter parser is installed, whether nvim-cmp and LuaSnip are present, and for each configured filetype other than HTML, whether highlighting will actually activate. That last check matters most for Twig, Blade, Liquid, Jinja2, and Nunjucks: it probes a scratch buffer to verify whether that filetype’s syntax exposes htmlTag (required for the regex fallback) and whether a matching compiled Tree-sitter parser is installed.
Composition with ejs.nvim
Because ejs.nvim injects a genuine, separate html language tree into .ejs buffers via the embedded_template parser, alpinejs.nvim’s Tree-sitter query files apply to those buffers automatically. There is no .ejs-specific code in alpinejs.nvim. The same composition applies to any other filetype whose Tree-sitter parser injects html for its surrounding markup, such as a PHP setup that injects html around <?php ?> regions.
Installation
lazy.nvim
{
"connorontheweb/alpinejs.nvim",
ft = { "html", "ejs", "php", "twig", "blade", "liquid", "jinja", "htmldjango", "nunjucks" },
dependencies = {
"nvim-treesitter/nvim-treesitter", -- optional, recommended
"hrsh7th/nvim-cmp", -- optional
"L3MON4D3/LuaSnip", -- optional
},
opts = {},
}
After installation, run :TSInstall html javascript inside Neovim to install the Tree-sitter parsers used by the primary highlighting path. Without them, the plugin falls back to the legacy regex highlighter automatically.
Other plugin managers are also supported. See the full installation documentation for vim-plug, packer.nvim, mini.deps, and the built-in packages approach.
Source code is available at github.com/ConnorOnTheWeb/alpinejs.nvim.