emily-state
Defines a component boundary and its starting state. The value must be valid JSON. All other directives inside this element read from and write to this state object.
<!-- Single key -->
<div emily-state='{"open": false}'>
…
</div>
<!-- Multiple keys -->
<div emily-state='{"tab": 1, "count": 0, "name": ""}'>
…
</div>emily-click
Runs an expression when the element is clicked. Supported expressions: toggle a boolean, assign a value, increment or decrement a number. If you put this on a div or span instead of a button, EmilyJS automatically adds role="button" and tabindex="0" so keyboard users can reach it.
<!-- Toggle a boolean -->
<button emily-click="open = !open">Toggle</button>
<!-- Set to a specific value -->
<button emily-click="tab = 2">Go to tab 2</button>
<!-- Increment / decrement -->
<button emily-click="count++">Add</button>
<button emily-click="count--">Remove</button>
<!-- Set to a string -->
<button emily-click="mode = 'dark'">Dark mode</button>
emily-show
Shows or hides an element based on a state expression. When hidden, EmilyJS sets the hidden attribute — the element stays in the DOM and remains accessible to screen readers who explicitly navigate hidden content.
<!-- Show when key is true -->
<div emily-show="open">Visible when open is true</div>
<!-- Show when key is false -->
<div emily-show="!open">Visible when open is false</div>
<!-- Show when key equals a value -->
<div emily-show="tab === 2">Only visible on tab 2</div>
emily-text
Sets the text content of an element to the value of a state key. Updates automatically whenever the state changes.
<div emily-state='{"count": 0}'>
<p>
Items selected: <span emily-text="count"></span>
</p>
<button emily-click="count++">Add item</button>
</div>emily-bind
Sets one or more HTML attributes from state values. Pass a JSON object where each key is the attribute name and each value is the state key to read from. Useful for dynamic aria-label, data-*, or href values.
<div emily-state='{"label": "Close menu", "href": "/home"}'>
<!-- Dynamic aria-label -->
<button emily-bind='{"aria-label": "label"}'>×</button>
<!-- Dynamic href -->
<a emily-bind='{"href": "href"}'>Go home</a>
</div>emily-modelemily-model.lazy
Two-way binding for inputs, selects, and textareas. The state key updates as the user types. Use emily-model.lazy to defer the update until the field loses focus (on blur) — useful for validation patterns that should not fire on every keystroke. Checkboxes bind to a boolean. Form reset restores all bound fields to their original mount values.
<div emily-state='{"email": "", "subscribed": false, "country": ""}'>
<!-- Text input — updates state on every keystroke -->
<input type="email" emily-model="email" />
<!-- Text input — updates state only when the field loses focus -->
<input type="text" emily-model.lazy="email" />
<!-- Checkbox — binds to a boolean -->
<input type="checkbox" emily-model="subscribed" />
<!-- Select -->
<select emily-model="country">
<option value="gb">United Kingdom</option>
<option value="us">United States</option>
</select>
<!-- Form reset restores all fields to their original values -->
<button type="reset">Clear form</button>
</div>emily-html
Sets the innerHTML of an element to the value of a state key. Use this when you need to inject HTML — for example a rich text value returned from an API. Only use it with content you trust; unlike emily-text, the value is not escaped.
<div emily-state='{"bio": "<strong>Hello</strong> world"}'>
<!-- Renders innerHTML — only use with trusted content -->
<div emily-html="bio"></div>
</div>emily-disabledAccessibility
Syncs both the disabled attribute and aria-disabled from a state expression. When the expression is truthy, the element is disabled for both native browser behaviour and assistive technology — no need to manage them separately.
<div emily-state='{"saving": false}'>
<button emily-click="saving = true" emily-disabled="saving">
Save
</button>
<!-- When saving is true:
disabled attribute is set (native browser behaviour)
aria-disabled="true" is set (assistive technology) -->
</div>emily-on:event
Binds any DOM event to an expression. More flexible than emily-click, which is a shorthand for emily-on:click. Use emily-on: when you need to respond to events other than clicks — focus, blur, change, input, keydown, and so on.
<div emily-state='{"dirty": false, "focused": false}'>
<!-- Respond to any DOM event -->
<input
emily-on:focus="focused = true"
emily-on:blur="focused = false"
emily-on:input="dirty = true"
/>
<p emily-show="dirty">You have unsaved changes.</p>
</div>emily-submit
Binds a form submit event and prevents the browser default. Put it on the <form> element along with a state expression to run when the form is submitted — useful for toggling a loading state or marking a form as submitted without a page reload.
<div emily-state='{"submitted": false}'>
<!-- Prevents page reload, runs expression on submit -->
<form emily-submit="submitted = true">
<input type="email" name="email" />
<button type="submit" emily-disabled="submitted">
Subscribe
</button>
</form>
<p emily-show="submitted">Thanks — you're on the list.</p>
</div>emily-class
Toggles CSS classes on or off based on state. Pass a JSON object where each key is the class name to add and each value is the state expression that controls it. Static classes already on the element are not touched.
<div emily-state='{"open": false, "active": true}'>
<!-- Adds 'menu-open' class when open is true.
The existing 'menu base' classes are not removed. -->
<nav class="menu base" emily-class='{"menu-open": "open"}'>
…
</nav>
</div>emily-style
Sets inline styles or CSS custom properties from state values. Useful when a value needs to drive a visual property that has no fixed utility class — for example a dynamic width, a runtime theme colour, or a CSS variable used by emilyCSS component patterns.
<div emily-state='{"progress": 40, "themeColour": "#DB2777"}'>
<!-- Sets a CSS custom property from state -->
<div
class="progress-bar"
emily-style='{"--progress-width": "progress"}'
></div>
</div>
<!-- The CSS variable can then drive a width or other property:
.progress-bar::after { width: var(--progress-width); } -->emily-trapAccessibility
Traps keyboard focus inside a container while the trap expression is truthy. When the trap activates, focus moves to the first focusable element inside the container and Tab cycles through them in order. When the trap deactivates, focus returns automatically to the element that triggered it — typically the button that opened the dialog.
Use this for modals, drawers, and any overlay pattern where a keyboard user should not be able to Tab outside the open panel.
<div emily-state='{"dialogOpen": false}'>
<!-- Button opens the dialog -->
<button emily-click="dialogOpen = !dialogOpen">
Open dialog
</button>
<!-- Focus is trapped inside this container while dialogOpen is true.
When dialogOpen becomes false, focus returns to the button above. -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
emily-show="dialogOpen"
emily-trap="dialogOpen"
>
<h2 id="dialog-title">Confirm action</h2>
<p>Are you sure you want to continue?</p>
<button emily-click="dialogOpen = false">Confirm</button>
<button emily-click="dialogOpen = false">Cancel</button>
</div>
</div>emily-announceAccessibility
Announces a state change to screen readers without moving focus. When the named state key changes, EmilyJS injects the new value into a visually hidden aria-live region. Screen readers pick this up and read it aloud. Use it for status messages, search result counts, form validation feedback, and any change a sighted user would notice but a screen reader user might miss.
<div emily-state='{"status": "Ready", "count": 0}'>
<button emily-click="count++">
Add item
</button>
<!-- Announces the value of 'count' to screen readers whenever it changes.
The element itself is visually present but the announcement
goes into a hidden aria-live region. -->
<span emily-announce="count" aria-hidden="true">
<span emily-text="count"></span> items in basket
</span>
</div>