Builtin Components Reference
On this page 47
stx ships with seven builtin components and a handful of runtime globals. Builtins are always available without import — they render to HTML at build time with no client-side component overhead. Runtime globals (toast, modal, drawer, stxAlert, stxConfirm) are attached to window by the signals runtime and can be called from any <script client> block.
| Builtin | Purpose |
|---|---|
<StxLink> | SPA-aware anchor |
<StxImage> | Optimized <img> with srcset / lazy / blur |
<Icon> | Iconify icon renderer |
<StxLoadingIndicator> | Top-of-page progress bar |
<StxToast> | Toast notification container |
<StxModal> | Imperative modal dialog |
<StxDrawer> | Imperative slide-in panel |
| Runtime API | Where it lives |
|---|---|
toast.success / error / info / warning / dismiss | window.toast |
modal.open(id) / close(id) / toggle(id) | window.modal |
drawer.open(id) / close(id) / toggle(id) | window.drawer |
stxAlert(message, opts?) | Promise-returning replacement for window.alert |
stxConfirm(message, opts?) | Promise-returning replacement for window.confirm |
x-tooltip="text" directive | Event-delegated tooltip with auto-flip positioning |
Presentational components (Button, Card, Switch, Tabs, etc.) live in @stacksjs/components, not here.
StxLink
Navigation-aware anchor element for SPA routing. Renders as a plain <a> tag with data-stx-link — the router intercepts clicks on these elements for client-side navigation without page reloads.
<StxLinkto="">About</StxLink>
<StxLinkto=""class=""activeClass="">Dashboard</StxLink>
Renders as:
<ahref=""data-stx-linkdata-stx-active-class=""data-stx-exact-active-class="">About</a>
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
to | string | '#' | Yes | Destination URL |
class | string | — | No | CSS classes |
activeClass | string | 'active' | No | Class applied when route matches (prefix match). Supports space-separated class lists |
exactActiveClass | string | 'exact-active' | No | Class applied when route exactly matches |
prefetch | boolean | false | No | Enable route prefetching on hover |
Active Class Handling
Both activeClass and exactActiveClass support space-separated class strings. Each class is added/removed individually via classList:
<StxLinkto=""activeClass="">
Settings
</StxLink>
When the route matches /settings, both bg-indigo-500/10 and text-indigo-400 are added.
Reactive href
The to prop supports client-reactive bindings:
<StxLinkto="">View Profile</StxLink>
When using a :to binding, the rendered anchor gets :href instead of href, which the signals runtime evaluates at runtime.
Event Forwarding
Event handlers on <StxLink> are forwarded to the rendered <a>:
<StxLinkto=""click="">Page</StxLink>
StxLink vs <a href>
<StxLink to> | <a href> | |
|---|---|---|
| Navigation | SPA (no reload) | Full page reload |
| Active class | Automatic | Manual |
| Prefetch | Supported | No |
| Router integration | Yes | No |
Aliases: <stx-link>
Extra Attributes
Any static attribute not consumed by StxLink is forwarded to the <a> element:
<StxLinkto=""id=""aria-label="">Page</StxLink>
StxImage
Full-featured image component with responsive images, format negotiation, lazy loading, placeholder support, and CDN provider integration.
<StxImagesrc=""alt="" />
<StxImagesrc=""alt=""width=""height=""sizes="" />
<StxImagesrc=""alt=""format=""placeholder=""preload />
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
src | string | — | Yes | Image source URL |
alt | string | '' | Yes | Alt text (accessibility) |
width | string | number | — | No | Image width in pixels |
height | string | number | — | No | Image height in pixels |
sizes | string | — | No | Responsive sizes (Tailwind-style breakpoints or raw media queries) |
srcset | string | — | No | Custom srcset (overrides auto-generation) |
densities | string | — | No | Density descriptors, e.g. "1x 2x 3x" |
format | string | — | No | Image format: 'webp', 'avif', 'auto' |
placeholder | string | — | No | Placeholder mode: 'blur', 'color', 'empty', or a custom URL |
placeholderColor | string | '#e5e7eb' | No | Background color for blur/color placeholders |
provider | string | — | No | CDN provider: 'cloudinary', 'imgix', 'bunny' |
lazy | boolean | true | No | Enable loading="lazy" |
preload | boolean | false | No | Inject <link rel="preload"> in head |
picture | boolean | false | No | Force <picture> wrapper (auto-enabled when format is set) |
quality | string | number | — | No | Image quality hint |
class | string | — | No | CSS classes |
style | string | — | No | Inline styles |
Responsive Sizes
The sizes prop accepts Tailwind-style breakpoint prefixes:
<StxImagesrc=""alt=""sizes="" />
This generates:
<imgsizes=""srcset="" />
Default breakpoints:
| Prefix | Width |
|---|---|
sm | 640px |
md | 768px |
lg | 1024px |
xl | 1280px |
2xl | 1536px |
Numeric breakpoints also work: sizes="800:50vw 1200:33vw".
Raw media queries pass through unchanged: sizes="(max-width: 768px) 100vw, 50vw".
When sizes is provided, a responsive srcset is auto-generated with widths: 320, 640, 768, 1024, 1280, 1536, 1920.
Density Descriptors
<StxImagesrc=""alt=""width=""densities="" />
Generates: srcset="/logo.png 1x, /logo.png 2x, /logo.png 3x". When a width is provided, each density multiplies the width for CDN providers.
Format Negotiation
Setting format wraps the image in a <picture> element with <source> elements for modern formats:
<StxImagesrc=""alt=""format="" />
Renders:
<picture>
<sourcetype=""srcset="" />
<sourcetype=""srcset="" />
<imgsrc=""alt=""loading=""decoding="" />
</picture>
| Format value | Sources generated |
|---|---|
'webp' | WebP only |
'avif' | AVIF only |
'auto' | AVIF + WebP (with original as fallback) |
Placeholders
<!-- Blur placeholder (SVG-based) -->
<StxImagesrc=""alt=""placeholder=""width=""height="" />
<!-- Custom color -->
<StxImagesrc=""alt=""placeholder=""placeholderColor="" />
<!-- No placeholder -->
<StxImagesrc=""alt=""placeholder="" />
<!-- Custom placeholder image URL -->
<StxImagesrc=""alt=""placeholder="" />
The blur placeholder generates a tiny SVG rectangle with the specified color, set as a background-image. It shows through while the real image loads.
The startup image pass
When a template uses <StxImage> or @image, the server derives placeholders and encodes the responsive variants for every raster under public/ when it starts. In production it holds its bind until that pass is done, so a zero-downtime deploy keeps sending visitors to the previous release instead of one that cannot serve them the optimized images yet.
That wait is capped. A cold variant cache (a first deploy, a cleared STX_IMAGE_CACHE_DIR, an encoder upgrade that renames variants) re-encodes everything, which on a photo-heavy site can take minutes, and a deploy tool gives up on a release that never binds. After the budget the server binds anyway and finishes the pass in the background. Until it finishes, <StxImage> falls back to a flat colour and the delivery lookup to the original file.
The decoding and encoding run on a worker thread (at most two, and one on a two-core machine, so serving keeps a core), and the finished catalog is handed to the serving thread in one piece. Requests that arrive mid-pass are answered at normal speed rather than queueing behind the encoders. prepareImageDelivery(publicDir, outDir, { offThread: true }) and warmImagePlaceholders(publicDir, { offThread: true }) expose the same thing to other long-lived servers; without a worker available they run in-thread.
Variants of sources that can carry transparency are named with the WebP and AVIF encoder versions in their hash, so an encoder upgrade re-encodes those few (logos, icons, cut-outs) under new URLs instead of reusing what an older encoder wrote into a shared STX_IMAGE_CACHE_DIR. Opaque photos keep their names across encoder upgrades.
With 'auto', a component that wraps <StxImage> (Stacks vendors one as Image.stx) turns the pass on only when some page, layout or partial renders it.
| Setting | Where | Default | What it does |
|---|---|---|---|
imageWarmupBindBudgetMs | serve() option or stx.config.ts | 45000 | How long a production start holds its bind for the pass. 0 binds at once. Keep it under your deploy tool's own limit (ts-cloud waits 180s). |
imageWarmupGraceMs | serve() option | 1000 | How long a request that arrives mid-pass waits before rendering with the fallbacks. |
imageWarmup | serve() option or stx.config.ts | 'auto' | Run the pass only when a template uses <StxImage> or @image ('auto'), always (true), or never (false). |
STX_IMAGE_CACHE_DIR | environment | unset | Keep the variants outside the release directory, so the next deploy starts warm. |
// stx.config.ts
export default {
imageWarmupBindBudgetMs: 30_000,
}
CLS Prevention
When both width and height are provided, an aspect-ratio style is added to prevent Cumulative Layout Shift:
<StxImagesrc=""alt=""width=""height="" />
<!-- Adds: style="aspect-ratio:800/600" -->
CDN Providers
<!-- Cloudinary -->
<StxImagesrc=""
alt="Sample" provider="cloudinary" width="800" format="webp" />
<!-- Transforms: /upload/w_800,f_webp,q_auto/sample.jpg -->
<!-- imgix -->
<StxImagesrc=""
alt="Image" provider="imgix" width="800" format="webp" />
<!-- Transforms: ?w=800&fm=webp&auto=format -->
<!-- Bunny CDN -->
<StxImagesrc=""
alt="Image" provider="bunny" width="800" format="webp" />
<!-- Transforms: ?width=800&format=webp -->
Preloading
<StxImagesrc=""alt=""preloadformat="" />
Injects a <link> tag before the image:
<linkrel=""as=""href=""type="" />
When srcset and sizes are present, imagesrcset and imagesizes attributes are included.
Default Behavior
Every <StxImage> automatically gets:
loading="lazy"(disable withlazy={false})decoding="async"
Aliases: <stx-image>, <stx-img>
Icon
Renders inline SVG from the Iconify icon library at build time. Zero client-side JavaScript.
<Iconname="" />
<Iconname=""size=""color="" />
<Iconname=""size=""class="" />
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | Yes | Icon name. Format: name (uses default collection) or collection:name |
icon | string | — | No | Alias for name (Iconify web-component convention) |
size | string | number | 24 | No | Icon dimensions in pixels (width and height) |
color | string | 'currentColor' | No | Icon color. Replaces currentColor in SVG paths |
class | string | — | No | CSS classes |
style | string | — | No | Inline styles |
Default Collection
The default icon collection is Lucide. When no collection prefix is specified, the icon is looked up in the Lucide set:
<Iconname="" /> <!-- lucide:house -->
<Iconname="" /> <!-- lucide:search -->
<Iconname="" /> <!-- lucide:settings -->
Using Other Collections
Prefix the icon name with the collection ID:
<Iconname="" />
<Iconname="" />
<Iconname="" />
<Iconname="" />
How It Works
- Icon collections are loaded from
@iconify/jsonat build time - The SVG
bodyis extracted from the collection JSON - An inline
<svg>element is rendered directly in the HTML - No JavaScript is shipped to the client
Renders as:
<svgxmlns=""width=""height=""viewBox=""fill="">
<!-- SVG path data from the icon -->
</svg>
Collection Pre-loading
Collections are cached after first load. If a collection is not yet cached when the component renders, a comment placeholder is inserted and the collection is loaded asynchronously for subsequent renders. To ensure icons render on first pass, pre-load collections in your build setup:
import { preloadIconCollection } from '@stacksjs/stx'
await preloadIconCollection('lucide')
await preloadIconCollection('heroicons')
Warning: If
@iconify/jsonis not installed, icons render as HTML comments:<!-- Icon: collection "lucide" not loaded -->. Install withbun add @iconify/json.
Aliases: <icon>, <stx-icon>
StxLoadingIndicator
Fixed-position progress bar at the top of the viewport that activates during navigation. Includes a shimmer animation and automatic link-click interception.
<StxLoadingIndicator />
<StxLoadingIndicatorcolor=""height="" />
<StxLoadingIndicatorcolor=""initialColor="" />
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
color | string | '#6366f1' | No | Bar color (CSS color value) |
initialColor | string | — | No | Start color for gradient (creates a left-to-right gradient with color) |
height | string | '3px' | No | Bar height (CSS length) |
duration | number | 2000 | No | Animation duration in ms (reserved for future use) |
throttle | number | 200 | No | Progress update interval in ms |
zIndex | number | 999999 | No | CSS z-index |
Prop initial-color is also accepted (kebab-case alias for initialColor). Prop z-index is also accepted.
How It Works
The component renders:
- A fixed-position
<div>at the top of the viewport withscaleX(0)transform - A shimmer overlay with a CSS animation
- An inline
<script>that sets upwindow.stxLoading
Automatic Interception
The loading indicator automatically starts when:
- A same-origin
<a>link is clicked (excludes external URLs, hash links,mailto:,tel:, andtarget="_blank") - A
popstateevent fires (browser back/forward)
It automatically finishes on the load event.
JavaScript API
The component exposes window.stxLoading for programmatic control:
// Start the progress bar
window.stxLoading.start()
// Complete the progress bar (animates to 100%, then fades out)
window.stxLoading.finish()
// Set progress to a specific percentage (0-100)
window.stxLoading.set(50)
// Clear immediately (no animation)
window.stxLoading.clear()
stxLoading API:
| Method | Description |
|---|---|
start() | Begin progress animation. Increments from 10% toward 90% |
finish() | Jump to 100%, then fade out after 200ms |
set(value) | Set progress to value (0-100) |
clear() | Reset to 0% immediately with no animation |
Integration with SPA Router
When using the stx SPA router, call stxLoading.start() before navigation and stxLoading.finish() after the new page loads. The router's stx:navigate event or the @async component loading states pair well with this indicator.
Aliases: <stx-loading-indicator>
StxToast
Renders a fixed-position container that the runtime toast() API populates with notification cards. Drop one <StxToast /> into your layout — typically once per app — and trigger toasts from anywhere with toast.success(...), toast.error(...), etc.
<StxToast />
<StxToastposition=""max="" />
<StxToastposition="" />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
position | 'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center' | 'top-right' | Where the stack appears |
max | number | 5 | Visible toast cap. New toasts past the cap evict the oldest. |
Runtime API — window.toast
The signals runtime registers toast on window so any <script client> block can call it without imports.
<scriptclient
toast.success('Saved!')
toast.error('Something went wrong', { duration: 5000 })
toast.info('FYI', { title: 'Heads up' })
toast.warning('This is risky')
toast.dismiss() // dismiss all
toast.dismiss(toastId) // dismiss a specific toast (return value of toast.*)
</script>
Each call returns the new toast's id (a number).
Options
| Option | Type | Default | Description |
|---|---|---|---|
duration | number | 3000 | Auto-dismiss delay in ms. 0 keeps it until dismissed |
title | string | — | Heading above the message, in bold |
id | string | number | — | Semantic key — see below |
dark | boolean | follows the document | Force the dark or light palette |
Left alone, dark follows the dark class, then data-theme / data-color-mode, then the OS setting.
Replace by id
A second toast with the same id replaces the first in place rather than
stacking, and toast.dismiss(id) ends it. That is what lets a long-running
action own one toast without threading the numeric handle between the call that
starts it and the call that finishes it:
<scriptclient
async function publish() {
toast.info('Publishing…', { id: 'publish', duration: 0 })
await savePost()
toast.success('Published.', { id: 'publish', title: 'Done' })
}
</script>
toast.dismiss() takes either kind of id — the semantic key or the numeric
handle — so a caller does not have to know which it is holding.
Other defaults: slide-in/out animation matched to the container's position,
role="alert", close button.
Aliases: <stx-toast>
StxModal
Renders a hidden, animated modal dialog with the slot as content. Open and close imperatively via modal.open(id) / modal.close(id) / modal.toggle(id) from a <script client> block.
<buttonclick="">Open</button>
<StxModalid="">
<h2>Settings</h2>
<p>Modal content here</p>
<buttonclick="">Done</button>
</StxModal>
<StxModalid=""size=""closeOnBackdrop="">
<h3>Delete this item</h3>
<buttonclick="">Cancel</button>
</StxModal>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — (required) | Unique identifier; passed to modal.open(id) |
size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | Max-width preset |
closeOnBackdrop | boolean | true | Click outside the panel closes the modal |
closeOnEscape | boolean | true | Esc closes the modal |
Runtime API — window.modal
modal.open(id: string): void
modal.close(id: string): void
modal.toggle(id: string): void
Open behavior: locks document.body scroll, sets data-stx-modal-open for the CSS transition, registers an Escape handler (if enabled). Close reverses everything; body scroll restores once no other modals are still open.
Both backdrop click and Escape can be opted out of via the closeOnBackdrop / closeOnEscape props.
Aliases: <stx-modal>
StxDrawer
Slide-in panel from the left or right edge. Same imperative model as <StxModal> — call drawer.open(id) to slide in, drawer.close(id) to dismiss.
<buttonclick="">Menu</button>
<StxDrawerid=""side=""size="">
<nav>
<StxLinkto="">Home</StxLink>
<StxLinkto="">About</StxLink>
</nav>
</StxDrawer>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — (required) | Unique identifier |
side | 'left' | 'right' | 'right' | Edge to slide in from |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Panel width preset (16/24/32/40 rem) |
closeOnBackdrop | boolean | true | Click outside closes |
closeOnEscape | boolean | true | Esc closes |
overlay | boolean | true | Show a dimmed backdrop. Set false for a non-modal drawer (clicks outside still hit the page). |
Runtime API — window.drawer
drawer.open(id: string): void
drawer.close(id: string): void
drawer.toggle(id: string): void
Same locking/restore semantics as modal — body scroll is locked while any drawer or modal is open.
Aliases: <stx-drawer>
stxAlert / stxConfirm
Styled, Promise-returning replacements for the synchronous window.alert and window.confirm. They render the same dialog primitive used for <StxModal> but build the markup imperatively, so they don't require a builtin in the page.
<scriptclient
await stxAlert('Saved successfully', { type: 'success', title: 'Done' })
const confirmed = await stxConfirm('Delete this draft?', {
type: 'warning',
confirmText: 'Delete',
cancelText: 'Keep',
})
if (confirmed) deleteDraft()
</script>
Signatures
stxAlert(message: string, options?: AlertOptions): Promise<void>
stxConfirm(message: string, options?: ConfirmOptions): Promise<boolean>
interface AlertOptions {
title?: string
type?: 'info' | 'warning' | 'error' | 'success' | 'question' // default: 'info'
confirmText?: string // default: 'OK'
}
interface ConfirmOptions extends AlertOptions {
cancelText?: string // default: 'Cancel'
type?: 'info' | 'warning' | 'error' | 'success' | 'question' // default: 'question'
}
Behavior:
- Animates in with a backdrop blur and panel scale.
- Auto-focuses the primary button.
- Esc resolves with
false(confirm) orundefined(alert). - Each
typeswaps the icon and primary button color. - Supports dark mode via
prefers-color-scheme.
x-tooltip Directive
A single, shared, event-delegated tooltip element handles every element with an x-tooltip attribute on the page. There's no per-element JS, no per-call DOM creation — the runtime listens once on document and shows/hides one shared tooltip node on hover or focus.
<buttonx-tooltip=""></button>
<ahref=""x-tooltip=""x-tooltip-position="">Pricing</a>
<inputx-tooltip="" />
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
x-tooltip | string | — | Tooltip text. Required to activate. |
x-tooltip-position | 'top' | 'bottom' | 'left' | 'right' | 'top' | Preferred position. The runtime auto-flips to fit the viewport. |
Triggers
The runtime activates on:
mouseover/mouseoutfocusin/focusout(so keyboard navigation gets tooltips too)
Notes
- Multiline text: use
\nin the attribute, the tooltip preserves whitespace viawhite-space: pre-wrap. - Tooltips don't capture pointer events (
pointer-events: none), so they never block clicks on neighbors. - The shared element lives at
document.bodywithz-index: 999999; if you nest your app inside atransformorcontain: paintancestor, the tooltip still positions correctly because it usesposition: absolutewith viewport coordinates derived fromgetBoundingClientRect.