Directive Reference
On this page 81
Complete reference for all stx template directives. stx uses Laravel Blade-inspired syntax with @directive patterns.
Table of Contents
- Expressions
- Layout & Inheritance
- Conditionals
- Loops
- Includes & Components
- Authentication & Authorization
- Forms & Validation
- Internationalization
- SEO & Meta
- Security
- Accessibility
- Animations
- Pre-paint Appearance
- Code Execution
- Utility
- Comments & Escaping
Expressions
Output with Escaping
{{ variable }}
{{ user.name }}
{{ formatDate(date) }}
Output is automatically HTML-escaped to prevent XSS attacks.
Raw Output (Unescaped)
{!! rawHtml !!}
{!! markdown(content) !!}
Warning: Only use with trusted content. Does not escape HTML.
Filters
{{ name | uppercase }}
{{ price | currency }}
{{ text | truncate(100) }}
{{ items | join(', ') }}
Available Filters: uppercase, lowercase, capitalize, escape, json, number, currency, date, truncate, join, first, last, length, reverse, slice, replace, stripTags, urlencode, abs, round, default, pluralize
Layout & Inheritance
@extends
Extend a parent layout template.
@extends('layouts/main')
@section('content')
<h1>Page Content</h1>
@endsection
@section / @endsection
Define a section that can be yielded in layouts.
@section('sidebar')
<nav>Sidebar content</nav>
@endsection
@yield
Output a section in a layout.
<main>
@yield('content')
@yield('sidebar', '<p>Default sidebar</p>')
</main>
@parent
Include parent section content when overriding.
@section('scripts')
@parent
<scriptsrc=""script
@endsection
@push / @prepend
Add content to a stack.
@push('scripts')
<scriptsrc=""script
@endpush
@prepend('styles')
<linkrel=""href="">
@endprepend
@stack
Output all pushed content.
<head>
@stack('styles')
</head>
<body>
@stack('scripts')
</body>
Conditionals
@if / @elseif / @else / @endif
@if(user.isAdmin)
<span>Admin</span>
@elseif(user.isModerator)
<span>Moderator</span>
@else
<span>User</span>
@endif
@unless / @endunless
Inverse of @if.
@unless(user.isGuest)
<ahref="">Dashboard</a>
@endunless
@switch / @case / @default / @endswitch
@switch(status)
@case('pending')
<spanclass="">Pending</span>
@case('approved')
<spanclass="">Approved</span>
@default
<spanclass="">Unknown</span>
@endswitch
@isset / @endisset
Check if variable exists and is not null.
@isset(user.avatar)
<imgsrc=""alt="">
@endisset
@empty / @endempty
Check if variable is empty.
@empty(notifications)
<p>No notifications</p>
@endempty
@env / @endenv
Conditional by environment.
@env('local')
<divclass="">Debug Info</div>
@endenv
@production / @development / @staging / @testing
Environment-specific shortcuts.
@production
<scriptsrc=""script
@endproduction
@development
<scriptsrc=""script
@enddevelopment
Loops
@foreach / @endforeach
@foreach(users as user)
<li>{{ user.name }}</li>
@endforeach
Loop Variables available inside @foreach:
| Variable | Description |
|---|---|
loop.index | Zero-based index |
loop.iteration | One-based count |
loop.first | Is first iteration |
loop.last | Is last iteration |
loop.count | Total items |
$loop | Alias (avoids conflicts) |
@foreach(items as item)
<divclass="">
{{ loop.iteration }}. {{ item.name }}
</div>
@endforeach
@forelse / @empty / @endforelse
Loop with empty state fallback.
@forelse(posts as post)
<article>{{ post.title }}</article>
@empty
<p>No posts found</p>
@endforelse
@for / @endfor
Traditional for loop.
@for(let i = 0; i < 5; i++)
<span>{{ i }}</span>
@endfor
@while / @endwhile
@while(items.length > 0)
{{ items.pop() }}
@endwhile
@break / @continue
Control loop flow.
@foreach(users as user)
@if(user.hidden)
@continue
@endif
@if(count > 10)
@break
@endif
<li>{{ user.name }}</li>
@endforeach
Conditional versions:
@foreach(users as user)
@continue(user.hidden)
@break(loop.index > 10)
<li>{{ user.name }}</li>
@endforeach
Includes & Components
@include
Include a partial template.
@include('partials/header')
@include('partials/card', { title: 'Hello', content: 'World' })
@includeIf
Include only if file exists.
@includeIf('partials/optional-widget')
@includeWhen / @includeUnless
Conditional includes.
@includeWhen(user.isPremium, 'partials/premium-features')
@includeUnless(user.isGuest, 'partials/user-menu')
@includeFirst
Include first existing template from array.
@includeFirst(['custom/header', 'default/header'])
@includeFirst(['custom/widget', 'partials/widget'], {}, '<p>Fallback</p>')
@component
Include a component with props.
@component('components/alert', { type: 'warning', message: 'Be careful!' })
Component Tags
PascalCase or kebab-case component syntax.
<Alerttype=""message="" />
<user-carduser="" />
@slot / @endslot
Define slot content for components.
@component('components/modal')
@slot('header')
<h2>Modal Title</h2>
@endslot
<p>Modal body content</p>
@endcomponent
/
Render content only once per request (deduplication).
@once
<linkrel=""href="">
@endonce
Authentication & Authorization
Show content to authenticated users.
@auth
<ahref="">Logout</a>
@endauth
Required Context:
{ auth: { check: true, user: { ... } } }
/
Show content to unauthenticated users.
@guest
<ahref="">Login</a>
@endguest
@can / @endcan
Check permission/ability.
@can('edit', post)
<button>Edit Post</button>
@endcan
Required Context:
{
permissions: {
check: (ability: string, type?: string, id?: any) => boolean
}
}
@cannot / @endcannot
Inverse permission check.
@cannot('delete', post)
<span>You cannot delete this post</span>
@endcannot
@elsecan / @elsecannot
@can('admin')
<button>Admin Panel</button>
@elsecan('moderator')
<button>Mod Tools</button>
@endcan
Forms & Validation
Insert CSRF token.
<formmethod="">
@csrf
<!--<inputtype=""name=""value=""> -->
</form>
@method
HTTP method spoofing.
<formmethod=""action="">
@method('PUT')
@csrf
</form>
@form /
Form wrapper with automatic CSRF.
@form('POST', '/submit', { class: 'my-form' })
<inputname=""type="">
<buttontype="">Submit</button>
@endform
@input
Form input with validation state.
@input('email', user.email, { type: 'email', placeholder: 'Email' })
@textarea
@textarea('bio', user.bio, { rows: 5 })
@select
@select('country', countries, user.country, { class: 'form-select' })
@checkbox / @radio
@checkbox('newsletter', true, { label: 'Subscribe' })
@radio('plan', 'premium', { label: 'Premium Plan' })
@error / @enderror
Display validation errors.
@error('email')
<spanclass="">{{ message }}</span>
@enderror
Internationalization (i18n)
@translate / @t
Translate text with optional parameters.
@translate('welcome')
@t('greeting', { name: user.name })
With pluralization:
@t('items_count', { count: items.length })
Translation file format:
{
"items_count": "One item|{count} items"
}
SEO & Meta
Every SEO directive takes expressions, evaluated against the template's context
(<script server> variables, the render context, and a partial's own
<script server> when the directive sits in a partial). A directive that cannot
produce its tag says so: an HTML comment where the tag would have been, such as
<!-- [SEO Error [1101]]: @seo(seo): "seo" is not defined here... -->, and a
[stx] console warning naming the file. It never renders nothing without a word.
: meta directive requires at least the meta name -->: meta directive requires at least the meta name -->
Generate a meta tag. The first argument is the name, the second the content.
@meta('description', 'Page description here') <!-- quoted: literal text -->
@meta('author', author) <!-- a variable -->
@meta('og:title', post.title) <!-- any expression -->
@meta('keywords', tags) <!-- an array joins with ", " -->
@meta('og:site_name', `${site.name} Docs`)
@meta('og:title') <!-- one argument: reads `title`, then `openGraph.title` -->
- A quoted value is always literal text. It is never looked up in the context, so
@meta('description', 'title')writes the word "title". - An unquoted value is an expression.
null,undefined,falseand''render no tag; a name the template does not have is reported (quote it if you meant the text). - The name may be quoted, a bare name (
@meta(og:image, '/og.png')), or an expression (@meta(tag.name, tag.value)). og:,article:,profile:and the other Open Graph namespaces are written asproperty; everything else, includingtwitter:, asname.
In a page, @meta tags are placed in <head> wherever the directive sits. In a
partial they are written in place, which suits a head partial.
@metaTag
A meta tag with full control over its attributes. The argument is any
expression that evaluates to { name?, property?, httpEquiv?, content }.
@metaTag({ httpEquiv: 'refresh', content: '30' })
@metaTag(themeColorTag)
@seo
Comprehensive SEO generation. The argument is any expression that evaluates to an object:
@seo({
title: 'Page Title',
description: 'Page description',
keywords: ['keyword1', 'keyword2'],
canonical: 'https://example.com/page',
openGraph: {
type: 'article',
image: '/images/og.jpg'
},
twitter: {
card: 'summary_large_image',
site: '@username'
},
structuredData: { '@type': 'Article', headline: 'Page Title' }
})
@seo(seo) <!--<scriptserver
@seo({ ...seo, title: post.title }) <!-- spread and override -->
@seo(buildSeo(post)) <!-- a function call -->
@seo(page.seo ?? {}) <!-- a variable only some pages define -->
@seo reads title, description, keywords, robots, canonical,
openGraph, twitter and structuredData. Any other key is ignored with a
warning that says where it belongs (image is openGraph.image, url is
canonical or openGraph.url, type is openGraph.type). structuredData
gets "@context": "https://schema.org" when it has none.
A variable that exists but holds null or undefined renders nothing. A
variable that does not exist, or a value that is not an object, is reported.
@seo writes its tags where it sits, so put it inside <head>: in the layout,
or in a head partial. At the top of a page body the tags land in <body>, where
crawlers ignore them; use useSeoMeta() in <script server> there instead.
@structuredData
JSON-LD structured data. The argument is any expression that evaluates to an
object, or an array of objects; @context defaults to https://schema.org.
@structuredData({
'@type': 'Article',
headline: article.title,
author: {
'@type': 'Person',
name: article.author
}
})
@structuredData(product)
@structuredData(buildSchema(post))
</script> inside a value is escaped, so data cannot close the block.
Security
Insert Content Security Policy meta tag.
<head>
@csp
</head>
Requires CSP configuration in stx.config.ts:
export default {
csp: {
enabled: true,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", "'strict-dynamic'"]
}
}
}
Output nonce for inline scripts/styles.
<scriptnonce=""
console.log('Safe inline script');
</script>
Or with expression:
<scriptnonce=""
// ...
</script>
Accessibility
Add accessibility hints as HTML comments.
@a11y('landmark', 'This section needs a heading')
/
Content visible only to screen readers.
@screenReader
<span>Opens in new window</span>
@endscreenReader
@ariaDescribe
Connect elements with descriptions.
@ariaDescribe('password-help', 'Password must be at least 8 characters')
<inputtype=""aria-describedby="">
Animations
@transition directive requires at least a transition type/
Add transition effects.
@transition('fade', 300)
<div>Fading content</div>
@endtransition
@transition('slide', 500, 'ease-out')
<div>Sliding content</div>
@endtransition
Transition Types: fade, slide, scale, flip, rotate
@animationGroup directive requires a group name and at least one element selector / @endanimationGroup
Group animations together.
@animationGroup('stagger')
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
@endanimationGroup
@scroll / @endscroll
Scroll-triggered animations.
@scroll
<sectionclass="">
Content revealed on scroll
</section>
@endscroll
Code Execution
Execute JavaScript server-side.
@js
const greeting = `Hello, ${name}!`;
@endjs
<p>{{ greeting }}</p>
Execute TypeScript server-side.
@ts
interface User { name: string; age: number }
const user: User = { name: 'John', age: 30 };
@endts
Utility
@json
Output data as JSON.
<script
const config = @json(appConfig);
const formatted = @json(data, true);
</script>
@route
Generate named route URLs.
<ahref="">View Profile</a>
Comments & Escaping
Comments
{{-- This comment will not appear in HTML output --}}
Escape @ Symbol
@@if <!-- Outputs: @if -->
Escape Expressions
@{{ variable }} <!-- Outputs: {{ variable }} -->
Processing Order
Directives are processed in a specific order. Understanding this helps debug complex templates:
- Comments removal
- Escaped directives/expressions
- Stack directives (
@push,@prepend) - Layout resolution (
@extends,@section,@yield) - Code execution (
@js,@ts) - Custom directives
- Components
- Animations
- Authentication/Authorization
- Forms
- Includes
- Loops
- Conditionals
- i18n
- Accessibility
- SEO
- Security (CSP)
- Expressions (
{{ }}) - Auto-injections (SEO tags, CSP meta)
Custom Directives
Register custom directives in configuration:
// stx.config.ts
export default {
customDirectives: [
{
name: 'uppercase',
handler: (content) => content.toUpperCase(),
hasEndTag: true,
description: 'Convert content to uppercase'
},
{
name: 'datetime',
handler: (_, params) => new Date(params[0]).toLocaleString(),
hasEndTag: false
}
]
}
Usage:
@uppercase
this will be uppercase
@enduppercase
@datetime('2024-01-15')
See Also
Pre-paint appearance
Use @appearanceBootstrap({...}) when persisted appearance must be applied to
the root element before the browser parses the application shell. The compiler
emits a synchronous, CSP-aware script at the directive location. This avoids a
light or layout flash without placing raw browser JavaScript in an stx template.
({
storageKey: 'app-appearance',
appearance: {
key: 'sidebarStyle',
attribute: 'appearance',
allowed: ['macos', 'arc'],
default: 'macos',
},
colorMode: {
key: 'colorMode',
attribute: 'color-mode',
default: 'system',
},
})
The storage value is a JSON object. The example reads sidebarStyle and
colorMode, writes data-appearance and data-color-mode, toggles the root
dark class, and writes data-theme. Stored values are accepted only when
they match the configured appearance allowlist or one of light, dark, and
system. Invalid or unavailable storage falls back to the declared defaults.