Directives
On this page 29
stx provides a powerful directive system for extending template functionality. This page covers all built-in directives and how to create custom directives in the stx ecosystem.
Built-in Directives
Conditional Directives
@if/v-if/:if/x-ifare interchangeable syntax for the same conditional. A condition over server data evaluates once at render time; a condition that reads a signal (on a page that uses signals) is reactive and re-evaluates on change — the keyword you pick is just style. See Conditionals.
<!-- Basic conditionals -->
@if(userisAuthenticated)
<dashboard />
@elseif(userisGuest)
<welcome-message />
<login-form />
<!-- Authentication helpers -->
<admin-panel />
<signup-banner />
<!-- Unless directive -->
@unless(userhasPermission(''))
<access-denied />
Loop Directives
<!-- Basic foreach -->
@foreach(itemsasitem)
<divclass="">{{ item.name }}</div>
<!-- With index -->
@foreach(usersasindex=>user)
<trclass="">
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
</tr>
<!-- With empty state -->
@forelse(postsaspost)
<article>{{ post.title }}</article>
<p>No posts found</p>
<!-- For range -->
@for(i1i<=10i++)
<div>Item {{ i }}</div>
<!-- While loop -->
@while(hasMore&&page<maxPages)
<load-more-button@click="loadMore" />
Template Directives
<!-- Include partials -->
@include(''currentUser })
<!-- Extend layouts -->
@extends('')
@section('', '')
@section('')
<h1>Welcome</h1>
<!-- Push to stacks -->
@push('')
<linkrel=""href="">
@push('')
<script></script>
<!-- Yield sections -->
@yield('', '')
('scripts')
<!-- Include markdown files -->
-file('content.md')
-file('docs/intro.md', {userName: 'Alice', role: 'Admin'})
Response Directives
<!-- The HTTP status this render should answer with -->
(404)
<!-- Any expression the template can evaluate -->
(feature ? 200 : 404)
@status runs after the conditionals, so it belongs inside the branch that
already knows the answer:
@if (!feature)
(404)
<h1>No feature by that name</h1>
<h1>{{ feature.title }}</h1>
A @status in the branch that lost never fires, so the page that found its
record answers 200 without having to say so. Without this, a dynamic route that
cannot resolve its parameter renders a not-found body under a 200 — which tells
a crawler, a cache and an uptime check that the URL is a real page.
An argument that is not an HTTP status (100–599) is ignored with a warning
rather than failing a page that has already rendered. The last @status wins.
@status is evaluated where the template is rendered, so it is skipped — with a
warning — when stx build precompiles a page, which happens once with no
request. On precompiled pages use notFound() or setResponseStatus() in
<script server>; those are re-run per request. See
<script server> for both spellings.
Markdown Directives
<!-- Include markdown files with frontmatter support -->
-file('content.md')
<!-- Relative paths -->
-file('./docs/getting-started.md')
-file('../shared/readme.md')
<!-- With additional context -->
-file('template.md', {userName: 'Alice', role: 'Admin'})
<!-- Inline markdown -->
# This is a heading
This is **bold** and this is *italic*.
\`\`\`javascript
const code = 'highlighted';
\`\`\`
Markdown File with Frontmatter:
---
title: "Getting Started"
author: "stx Team"
date: 2025-10-08
---
# {{ title }}
Written by {{ author }}
This guide will help you get started with stx.
Variables from frontmatter and template context are available!
Features:
- YAML frontmatter parsing
- Variable substitution with
{{ variable }} - Server-side syntax highlighting with Shiki
- Supports GitHub Flavored Markdown (GFM)
- Automatic caching for performance
See Markdown API for detailed documentation.
Form Directives
<!-- Two-way binding -->
<input@model="username"type="">
<input@model.number="age"type="">
<input@model.trim="message"type="">
<!-- Checkbox binding -->
<input@model="isChecked"type="">
<!-- Select binding -->
<select@model="selectedOption">
@foreach(optionsasoption)
<optionx-value="">{{ option.label }}</option>
</select>
<!-- Multiple select -->
<select@model="selectedItems"multiple>
@foreach(itemsasitem)
<optionx-value="">{{ item.name }}</option>
</select>
Event Directives
<!-- Basic event handling -->
<button@click="handleClick">Click me</button>
<input@input="handleInput"@blur="handleBlur">
<!-- Event modifiers -->
<form@submit.prevent="onSubmit">
<button@click.stop="stopPropagation">
<input@keyup.enter="search">
<div@click.once="initializeOnce">
<!-- Dynamic events -->
<buttoneventNamehandler">Dynamic Event</button>
<!-- Multiple events -->
<input
="handleInput"
="handleFocus"
="handleBlur"
>
Attribute Directives
<!-- Dynamic attributes -->
<imgx-src=""x-alt="">
<buttonx-disabled=""x-class="">
<!-- Dynamic attribute names -->
<inputattributeNameattributeValue">
<!-- Class binding -->
<divx-class="
'active': isActive,
'disabled': isDisabled,
'large': size === 'large'
}">
</div>
<!-- Style binding -->
<divx-style="
color: textColor,
fontSize: fontSize + 'px',
backgroundColor: bgColor
}">
</div>
Custom Directives
Simple Custom Directive
// directives/focus.ts
export const focus = {
mounted(el: HTMLElement, binding: any) {
if (binding.value) {
el.focus()
}
},
updated(el: HTMLElement, binding: any) {
if (binding.value) {
el.focus()
}
}
}
// Register directive
app.directive('focus', focus)
Usage:
<input@focus="shouldFocus"type="">
Advanced Custom Directive
// directives/lazy-load.ts
export const lazyLoad = {
mounted(el: HTMLElement, binding: any) {
const options = {
threshold: 0.1,
rootMargin: '50px',
...binding.value?.options
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
// Load the image
if (binding.value?.src) {
img.src = binding.value.src
}
// Add loaded class
img.classList.add('loaded')
// Execute callback
if (binding.value?.onLoad) {
binding.value.onLoad(img)
}
observer.unobserve(img)
}
})
}, options)
observer.observe(el)
// Store observer for cleanup
el._lazyObserver = observer
},
beforeUnmount(el: HTMLElement) {
if (el._lazyObserver) {
el._lazyObserver.disconnect()
}
}
}
Usage:
<img
-load="{
src: '/images/large-image.jpg',
onLoad: handleImageLoad,
options: { threshold: 0.2 }
}"
src="/images/placeholder.jpg"
alt="Lazy loaded image"
>
Directive with Arguments
// directives/click-outside.ts
export const clickOutside = {
mounted(el: HTMLElement, binding: any) {
const handler = (event: Event) => {
if (!el.contains(event.target as Node)) {
binding.value(event)
}
}
document.addEventListener('click', handler)
el._clickOutsideHandler = handler
},
beforeUnmount(el: HTMLElement) {
if (el._clickOutsideHandler) {
document.removeEventListener('click', el._clickOutsideHandler)
}
}
}
Usage:
<divclick-outside=""class="">
<!-- Dropdown content -->
</div>
Parameterized Directive
// directives/tooltip.ts
export const tooltip = {
mounted(el: HTMLElement, binding: any) {
const tooltip = document.createElement('div')
tooltip.className = 'tooltip'
tooltip.textContent = binding.value
tooltip.style.display = 'none'
document.body.appendChild(tooltip)
const showTooltip = (event: MouseEvent) => {
tooltip.style.display = 'block'
tooltip.style.left = event.pageX + 10 + 'px'
tooltip.style.top = event.pageY + 10 + 'px'
}
const hideTooltip = () => {
tooltip.style.display = 'none'
}
el.addEventListener('mouseenter', showTooltip)
el.addEventListener('mouseleave', hideTooltip)
el._tooltip = tooltip
el._showTooltip = showTooltip
el._hideTooltip = hideTooltip
},
updated(el: HTMLElement, binding: any) {
if (el._tooltip) {
el._tooltip.textContent = binding.value
}
},
beforeUnmount(el: HTMLElement) {
if (el._tooltip) {
document.body.removeChild(el._tooltip)
}
if (el._showTooltip) {
el.removeEventListener('mouseenter', el._showTooltip)
}
if (el._hideTooltip) {
el.removeEventListener('mouseleave', el._hideTooltip)
}
}
}
Usage:
<button@tooltip="''">
Hover me
</button>
Directive Composition
Multiple Directives
<input
="searchQuery"
="true"
-outside="closeSearchResults"
:300="performSearch"
type="text"
>
Directive Inheritance
// Base directive
const baseValidation = {
mounted(el: HTMLElement, binding: any) {
// Common validation logic
}
}
// Extended directive
const emailValidation = {
...baseValidation,
mounted(el: HTMLElement, binding: any) {
baseValidation.mounted(el, binding)
// Email-specific validation
el.addEventListener('input', validateEmail)
}
}
Built-in Directive Modifiers
Event Modifiers
<!-- Prevent default -->
<form@submit.prevent="onSubmit">
<!-- Stop propagation -->
<button@click.stop="handleClick">
<!-- Capture phase -->
<div@click.capture="handleCapture">
<!-- Once only -->
<button@click.once="initialize">
<!-- Passive listener -->
<div@scroll.passive="handleScroll">
<!-- Touch and wheel listeners are passive already; opt out to call preventDefault() -->
<div@touchstart="startSwipe">
<canvas@wheel.nonpassive="zoom">
<!-- Key modifiers -->
<input@keyup.enter="search">
<input@keyup.esc="clearSearch">
<input@keyup.ctrl.s="save">
@touchstart, @touchmove and @wheel listen passively by default, so a scroll that starts on the element never waits for the handler. .prevent keeps a blocking listener, since it calls preventDefault(); write .nonpassive when the handler calls it itself.
Model Modifiers
<!-- Trim whitespace -->
<input@model.trim="message">
<!-- Convert to number -->
<input@model.number="age">
<!-- Lazy sync (on change instead of input) -->
<input@model.lazy="value">
<!-- Debounced input -->
<inputmodeldebounce:500="">
Async Directives
Async Directive Example
// directives/async-load.ts
export const asyncLoad = {
async mounted(el: HTMLElement, binding: any) {
el.classList.add('loading')
try {
const data = await binding.value()
// Update element with loaded data
if (typeof data === 'string') {
el.textContent = data
} else if (data.html) {
el.innerHTML = data.html
}
el.classList.remove('loading')
el.classList.add('loaded')
} catch (error) {
el.classList.remove('loading')
el.classList.add('error')
console.error('Async load failed:', error)
}
}
}
Usage:
<divasync-load="">Loading</div>
Directive Testing
Testing Custom Directives
import { mount } from '@stacksjs/testing'
import { tooltip } from './directives/tooltip'
describe('Tooltip Directive', () => {
test('creates tooltip on hover', async () => {
const wrapper = mount({
template: '<button @tooltip="\'Test tooltip\'">Hover</button>',
directives: { tooltip }
})
const button = wrapper.find('button')
await button.trigger('mouseenter')
const tooltipEl = document.querySelector('.tooltip')
expect(tooltipEl).toBeTruthy()
expect(tooltipEl.textContent).toBe('Test tooltip')
await button.trigger('mouseleave')
expect(tooltipEl.style.display).toBe('none')
})
})
Integration Testing
describe('Form with Custom Directives', () => {
test('validation and focus work together', async () => {
const wrapper = mount({
template: `
<form>
<input
@model="email"
@focus="true"
@validate="validateEmail"
type="email"
>
</form>
`,
data: {
email: ''
}
})
const input = wrapper.find('input')
expect(document.activeElement).toBe(input.element)
await input.setValue('invalid-email')
// Test validation behavior
})
})
Performance Considerations
Efficient Directive Updates
export const optimizedDirective = {
mounted(el: HTMLElement, binding: any) {
// Initialize once
el._directiveData = {
previousValue: binding.value,
handler: createHandler(binding.value)
}
},
updated(el: HTMLElement, binding: any) {
// Only update if value changed
if (binding.value !== el._directiveData.previousValue) {
// Cleanup previous
cleanup(el._directiveData.handler)
// Setup new
el._directiveData = {
previousValue: binding.value,
handler: createHandler(binding.value)
}
}
}
}
Memory Management
export const memoryEfficientDirective = {
mounted(el: HTMLElement, binding: any) {
const controller = new AbortController()
el.addEventListener('click', handler, {
signal: controller.signal
})
// Store for cleanup
el._abortController = controller
},
beforeUnmount(el: HTMLElement) {
// Automatic cleanup of all listeners
el._abortController?.abort()
}
}
Related Resources
- Directive Guide - Comprehensive directive development guide
- Template System - Template syntax and directives
- Custom Directives - Advanced directive patterns
- Component System - Using directives with components