Render Alpine JS data with {curly braces} instead of x-text π
Alpine JS Tash (Template Hash) brings the interpolation syntax you already know
from React, Vue, Svelte and Angular to Alpine. Write the value where it belongs β
in the middle of a sentence β instead of splitting the sentence into <span>s.
<p x-tash>Hello, I am {name} and I am {age} years old!</p>Against the x-text equivalent:
<p>Hello, I am <span x-text="name"></span> and I am <span x-text="age"></span> years old!</p>- π Keys are detected from the template β no need to list them
- π Works inside
x-forandx-if - π·οΈ Works in text and in attributes
- π¨ Configurable delimiters, so it won't fight Blade, Twig or Jinja
- π Fully reactive, and updates without rebuilding your DOM
- π‘οΈ Values render as text, so data can't inject markup
- πͺΆ ~1.5KB gzipped, zero dependencies
<script defer src="https://unpkg.com/alpinejs-tash@latest/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@latest/dist/cdn.min.js"></script>Tash must load before Alpine, as with every Alpine plugin.
pnpm add -D alpinejs-tash
yarn add -D alpinejs-tash
npm install -D alpinejs-tashimport Alpine from 'alpinejs'
import tash from 'alpinejs-tash'
Alpine.plugin(tash)
window.Alpine = Alpine
Alpine.start()Add x-tash to an element and use {key} anywhere inside it:
<div x-data="{ name: 'Walter White', age: 50, company: 'Gray Matter Technologies' }">
<p x-tash>
Hello, I am {name}! I am {age} years old and I currently work at {company}!
</p>
<!-- Hello, I am Walter White! I am 50 years old and I currently work at Gray Matter Technologies! -->
</div>Anything that works in an Alpine expression works as a key, including nested paths, array indexes and getters:
<p x-tash>{user.name} has {user.orders.length} orders, worth {orderTotal}</p>Passing a comma-separated list restricts rendering to those keys, and leaves
every other {...} in the template untouched. This is the v1 syntax, and it
still works:
<p x-tash="name, age">Hi {name}, you are {age}. This stays literal: {company}</p>Use it when your markup contains literal braces you don't want touched.
Otherwise leave x-tash empty and let it find the keys.
Placeholders in attributes are interpolated too:
<div x-data="{ label: 'Close dialog', theme: 'dark' }">
<button x-tash title="{label}" data-theme="{theme}">Γ</button>
<!-- <button title="Close dialog" data-theme="dark">Γ</button> -->
</div>Alpine's own attributes β anything starting with x-, @ or : β are skipped,
because Alpine already evaluates those as expressions. Use :title="label" for
those, not title="{label}".
Placeholders inside x-for and x-if work, and resolve against the scope they
sit in:
<ul x-tash x-data="{ rows: [{ label: 'a', n: 1 }] }">
<template x-for="row in rows">
<li title="row {row.label}">{row.label} = {row.n}</li>
</template>
</ul>Content inserted after init is picked up as it appears, so rows added later
render too. Putting x-tash on the element inside the loop works equally well
if you prefer to scope it tightly:
<template x-for="item in items">
<li x-tash>Item: {item}</li>
</template>The default pair is { and }. Change it globally when it collides with
server-side templating β Blade, Twig, Jinja and Handlebars all use {{ }}:
import Alpine from 'alpinejs'
import tash from 'alpinejs-tash'
Alpine.plugin(tash({ delimiters: ['[[', ']]'] }))<p x-tash>Hello, [[name]]! Blade's {{ name }} is left alone.</p>From a CDN, set the config before the plugin script runs:
<script>
window.tashConfig = { delimiters: ['[[', ']]'] }
</script>
<script defer src="https://unpkg.com/alpinejs-tash@latest/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/alpinejs@latest/dist/cdn.min.js"></script>Whitespace inside the delimiters is always optional, so [[name]] and
[[ name ]] are equivalent. To use Vue or Angular style, set the pair to
['{{', '}}'].
| Value | Renders as |
|---|---|
'Walter' |
Walter |
50 |
50 |
null / undefined |
(empty string) |
{ a: 1 } |
{"a":1} |
'<b>bold</b>' |
<b>bold</b> as visible text |
Values are written as text, never parsed as HTML. A value containing markup
shows up as characters on the page, so user-supplied data can't inject elements.
If you genuinely want to render HTML, use Alpine's x-html β and only on
content you trust.
Each node is bound once. Tash captures a node's original text the first time
it sees it, then writes rendered output back into that same node. Editing the
DOM by hand to add new {key} text won't be picked up, but anything Alpine
inserts β x-for rows, x-if branches β is.
Updates preserve your DOM. Because only matched nodes are rewritten, child
elements keep their identity across updates β a form input keeps its value,
focus isn't lost, event listeners survive, and nested x-data components aren't
torn down and rebuilt.
An unresolvable key only affects its own placeholder. Keys are evaluated
independently, so {config} with no config in scope is left on the page as
literal text while every other placeholder still renders. Alpine logs its usual
expression error for it. If a literal {...} in your copy is triggering that
noise, name your real keys explicitly to opt the rest out.
Nested x-tash elements own their own subtree, and are skipped by the
parent, so a placeholder is never rendered twice.
<script> and <style> contents are never interpolated.
- Values render as text, not HTML. v1 replaced into
el.innerHTML, so a value containing markup became real elements. If you relied on that, switch that binding tox-html. - The key list is now optional, and
x-tashwith no expression auto-detects keys. Existingx-tash="a, b"markup keeps working unchanged. - The
.vueand.angularmodifiers are gone, replaced by thedelimitersoption. Both were fixed to{{ }}, which is exactly the pair that collides with server-side templating; configuring it globally covers those two styles and every other pair. Replacex-tash.vue/x-tash.angularwithx-tashandAlpine.plugin(tash({ delimiters: ['{{', '}}'] })). nullandundefinedrender as an empty string rather than the textnull/undefined, and objects render as JSON rather than[object Object].- Placeholders in attributes now render. Previously only element content was
processed. If you had a literal
{...}in an attribute on anx-tashelement, it will now be interpolated. dist/esm.min.jsis gone, replaced bydist/module.mjsanddist/module.cjsbehind anexportsmap β 1.2.1 declared onlymodule, so neitherrequire()nor Node ESMimportresolved. Package-name imports are unaffected; update any deep imports of the old path.dist/cdn.min.jsis unchanged, so CDN users need no changes.