Stacks UI
Design & anti-slop skills
For premium, non-templated UI (layout, typography, color, motion) built on stx + Crosswind, reach for the design-taste skill family:
stacks-design-taste - flagship anti-slop frontend skill (brief inference, the three dials, layout/type/color discipline, strict pre-flight check)
- Aesthetic presets:
stacks-design-soft, stacks-design-minimalist, stacks-design-brutalist
stacks-redesign - audit-first upgrade of an existing UI; stacks-design-output - full-output enforcement (no placeholder or truncated components)
- Image-first:
stacks-image-to-code, plus reference-image generators stacks-imagegen-web, stacks-imagegen-mobile, stacks-brandkit
Key Paths
- Core package:
storage/framework/core/ui/src/
- Components:
storage/framework/core/ui/src/components/
- UI config:
config/ui.ts (Crosswind)
- STX config:
config/ui.ts
- STX engine:
node_modules/@stacksjs/stx/
- Crosswind:
node_modules/@cwcss/crosswind/
- Editor metadata:
storage/framework/core/web-types.json, storage/framework/core/custom-elements.json
Source Files
ui/src/
├── index.ts # Re-exports from @stacksjs/stx
├── components.ts # Component re-exports
└── components/
├── autocomplete.ts # Combobox, ComboboxInput, ComboboxOption, ComboboxOptions
├── disclosure.ts # Disclosure, DisclosureButton, DisclosurePanel
├── menu.ts # Menu, MenuButton, MenuItem, MenuItems
├── modal.ts # Dialog, DialogDescription, DialogPanel, DialogTitle
├── popover.ts # Popover, PopoverButton, PopoverPanel
├── radio-group.ts # RadioGroup, RadioGroupLabel, RadioGroupOption
├── select.ts # Combobox-based select
├── tabs.ts # Tab, TabGroup, TabList, TabPanel, TabPanels
├── toggle.ts # Switch
└── transition.ts # TransitionChild, TransitionRoot
Headless Components
import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from '@stacksjs/ui'
import { Dialog, DialogDescription, DialogPanel, DialogTitle } from '@stacksjs/ui'
import { Menu, MenuButton, MenuItem, MenuItems } from '@stacksjs/ui'
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@stacksjs/ui'
import { Switch } from '@stacksjs/ui'
import { TransitionChild, TransitionRoot } from '@stacksjs/ui'
Craft Native Components
Built-in components with native HTML fallbacks:
| Component |
Fallback |
Key Props |
craft-button |
<button> |
variant (primary/secondary/outline) |
craft-text-input |
<input> |
placeholder, value, type, disabled |
craft-textarea |
<textarea> |
placeholder, value, rows |
craft-checkbox |
<input type="checkbox"> |
checked, disabled, label |
craft-select |
<select> |
value, options, placeholder |
craft-modal |
<dialog> |
open, title, closable, size |
craft-tabs |
<div> |
activeTab, tabs |
craft-table |
<table> |
columns, rows, sortable, selectable |
craft-card |
<div> |
title, subtitle, variant |
craft-alert |
<div> |
variant, title, dismissible |
craft-toast |
<div> |
variant, duration, position |
craft-tooltip |
<span> |
content, position |
craft-pagination |
<nav> |
total, page, pageSize |
craft-code-editor |
<textarea> |
value, language, theme, lineNumbers |
craft-date-picker |
<input type="date"> |
value, min, max, format |
craft-color-picker |
<input type="color"> |
value, format |
craft-badge |
<span> |
variant, size |
craft-avatar |
<div> |
src, alt, size, fallback |
craft-progress |
<div> |
value, max, variant |
craft-spinner |
<div> |
size |
craft-accordion |
<details> |
open, title |
craft-divider |
<hr> |
orientation, variant |
craft-breadcrumb |
<nav> |
items, separator |
craft-menu |
<nav> |
items, orientation |
craft-tree |
<div> |
nodes, expandable, selectable |
craft-list |
<ul> |
items, selectable |
craft-slider |
<input type="range"> |
value, min, max, step |
craft-radio |
<input type="radio"> |
checked, name, value, label |
craft-file-browser |
<div> |
path, showHidden, selectable |
Reactivity System
import { ref, namedRef, computed, watch } from '@stacksjs/stx'
const count = ref(0)
count.value = 5
const doubled = computed(() => count.value * 2)
const stop = watch(
() => count.value,
(newVal, oldVal) => console.log(`${oldVal} → ${newVal}`),
{ immediate: false }
)
stop() // cleanup
Types
interface Ref<T> { value: T | null, readonly current: T | null }
interface ComponentInstance {
id: string, element: Element | null
mountHooks: LifecycleHook[], destroyHooks: CleanupFn[], updateHooks: LifecycleHook[]
refs: Map<string, Ref<any>>, watchers: Array<{ stop: () => void }>
isMounted: boolean
}
Lifecycle Hooks
import { onMount, onDestroy, onUpdate } from '@stacksjs/stx'
// Aliases: onMounted, onUnmounted, onUpdated
onMount(() => {
console.log('mounted')
return () => console.log('cleanup') // optional
})
onDestroy(() => console.log('destroyed'))
onUpdate(() => console.log('updated'))
Dependency Injection
import { provide, inject, createInjectionKey, withInjectionScope } from '@stacksjs/stx'
const ThemeKey = createInjectionKey<string>('theme')
provide(ThemeKey, 'dark')
const theme = inject(ThemeKey) // 'dark'
const theme = inject(ThemeKey, 'light') // with default
Browser Composables
import {
useLocalStorage, useSessionStorage, useEventListener,
useClickOutside, useWindowSize, useMediaQuery,
usePrefersDark, useOnline
} from '@stacksjs/stx'
const { value, remove } = useLocalStorage('key', defaultValue)
const { width, height } = useWindowSize()
const isDark = usePrefersDark()
const isOnline = useOnline()
const cleanup = useClickOutside(elementRef, handler)
Crosswind Configuration (config/ui.ts)
export default {
content: [
'./resources/**/*.{html,js,ts,jsx,tsx,stx}',
'./storage/framework/defaults/**/*.{html,js,ts,jsx,tsx,stx}',
'./storage/framework/views/**/*.{html,js,ts,jsx,tsx,stx}',
],
output: './storage/framework/assets/headwind.css',
minify: false,
} satisfies CrosswindOptions
STX Configuration (config/ui.ts)
export default {
componentsDir: 'components',
layoutsDir: 'layouts',
partialsDir: 'partials',
} satisfies StxOptions
Full StxConfig
interface StxConfig {
enabled: boolean, debug: boolean
templatesDir?, componentsDir, partialsDir, layoutsDir?, defaultLayout?
ssr?: boolean, cache?: boolean, cachePath: string
i18n?: Partial<I18nConfig>
webComponents?: Partial<WebComponentConfig>
streaming?: Partial<StreamingConfig>
hydration?: Partial<HydrationConfig>
a11y?: Partial<A11yConfig>
seo?: Partial<SeoFeatureConfig>
animation?: Partial<AnimationConfig>
markdown?: Partial<MarkdownConfig>
pwa?: Partial<PwaConfig>
strict?: boolean | StrictModeConfig
}
Accessibility
import { checkA11y, autoFixA11y, scanA11yIssues } from '@stacksjs/stx'
const violations = await checkA11y(html, filePath)
const result = autoFixA11y(html, config)
const issues = await scanA11yIssues('./resources', { recursive: true })
interface A11yConfig {
enabled: boolean, addSrOnlyStyles: boolean
level: 'AA' | 'AAA', ignoreChecks?: string[], autoFix: boolean
}
Crosswind CSS Framework
Utility-first CSS (like Tailwind), built into Stacks:
import { buildCrosswindCSS, extractClassNames, generateCrosswindCSS } from '@stacksjs/stx'
const css = await buildCrosswindCSS(cwd)
const classNames = extractClassNames(htmlContent)
Features: theme config, 40+ variant modifiers, custom rules, shortcuts, attributify mode, bracket syntax, presets.
Gotchas
- @stacksjs/ui re-exports from @stacksjs/stx — the UI package is thin, the engine is in STX
- Craft components use native fallbacks —
preferNative: true renders plain HTML
- Refs are not Vue refs — similar API but custom reactive implementation
- Lifecycle hooks require component context — must be called within
setupComponent()
- Crosswind is not Tailwind — Stacks' own CSS utility implementation
- Crosswind is the utility engine — handles class extraction, CSS generation, purging
- STX is the templating engine — handles
.stx files, SSR, streaming, hydration
- Two CSS systems coexist — Crosswind (config) and Crosswind (engine)
- 150+ globally registered Vue components — no imports needed
1---2name: stacks-ui3description: Use when working with UI in a Stacks application - components, composables, reactivity (refs/watch/computed), Craft native components, Crosswind CSS, Crosswind utility framework, accessibility, or the STX templating engine. Covers @stacksjs/ui, @stacksjs/stx, and related UI tooling.4license: MIT5---67# Stacks UI89## Design & anti-slop skills1011For premium, non-templated UI (layout, typography, color, motion) built on stx + Crosswind, reach for the design-taste skill family:12- `stacks-design-taste` - flagship anti-slop frontend skill (brief inference, the three dials, layout/type/color discipline, strict pre-flight check)13- Aesthetic presets: `stacks-design-soft`, `stacks-design-minimalist`, `stacks-design-brutalist`14- `stacks-redesign` - audit-first upgrade of an existing UI; `stacks-design-output` - full-output enforcement (no placeholder or truncated components)15- Image-first: `stacks-image-to-code`, plus reference-image generators `stacks-imagegen-web`, `stacks-imagegen-mobile`, `stacks-brandkit`1617## Key Paths18- Core package: `storage/framework/core/ui/src/`19- Components: `storage/framework/core/ui/src/components/`20- UI config: `config/ui.ts` (Crosswind)21- STX config: `config/ui.ts`22- STX engine: `node_modules/@stacksjs/stx/`23- Crosswind: `node_modules/@cwcss/crosswind/`24- Editor metadata: `storage/framework/core/web-types.json`, `storage/framework/core/custom-elements.json`2526## Source Files27```28ui/src/29├── index.ts # Re-exports from @stacksjs/stx30├── components.ts # Component re-exports31└── components/32 ├── autocomplete.ts # Combobox, ComboboxInput, ComboboxOption, ComboboxOptions33 ├── disclosure.ts # Disclosure, DisclosureButton, DisclosurePanel34 ├── menu.ts # Menu, MenuButton, MenuItem, MenuItems35 ├── modal.ts # Dialog, DialogDescription, DialogPanel, DialogTitle36 ├── popover.ts # Popover, PopoverButton, PopoverPanel37 ├── radio-group.ts # RadioGroup, RadioGroupLabel, RadioGroupOption38 ├── select.ts # Combobox-based select39 ├── tabs.ts # Tab, TabGroup, TabList, TabPanel, TabPanels40 ├── toggle.ts # Switch41 └── transition.ts # TransitionChild, TransitionRoot42```4344## Headless Components4546```typescript47import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from '@stacksjs/ui'48import { Dialog, DialogDescription, DialogPanel, DialogTitle } from '@stacksjs/ui'49import { Menu, MenuButton, MenuItem, MenuItems } from '@stacksjs/ui'50import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@stacksjs/ui'51import { Switch } from '@stacksjs/ui'52import { TransitionChild, TransitionRoot } from '@stacksjs/ui'53```5455## Craft Native Components5657Built-in components with native HTML fallbacks:5859| Component | Fallback | Key Props |60|-----------|----------|-----------|61| `craft-button` | `<button>` | variant (primary/secondary/outline) |62| `craft-text-input` | `<input>` | placeholder, value, type, disabled |63| `craft-textarea` | `<textarea>` | placeholder, value, rows |64| `craft-checkbox` | `<input type="checkbox">` | checked, disabled, label |65| `craft-select` | `<select>` | value, options, placeholder |66| `craft-modal` | `<dialog>` | open, title, closable, size |67| `craft-tabs` | `<div>` | activeTab, tabs |68| `craft-table` | `<table>` | columns, rows, sortable, selectable |69| `craft-card` | `<div>` | title, subtitle, variant |70| `craft-alert` | `<div>` | variant, title, dismissible |71| `craft-toast` | `<div>` | variant, duration, position |72| `craft-tooltip` | `<span>` | content, position |73| `craft-pagination` | `<nav>` | total, page, pageSize |74| `craft-code-editor` | `<textarea>` | value, language, theme, lineNumbers |75| `craft-date-picker` | `<input type="date">` | value, min, max, format |76| `craft-color-picker` | `<input type="color">` | value, format |77| `craft-badge` | `<span>` | variant, size |78| `craft-avatar` | `<div>` | src, alt, size, fallback |79| `craft-progress` | `<div>` | value, max, variant |80| `craft-spinner` | `<div>` | size |81| `craft-accordion` | `<details>` | open, title |82| `craft-divider` | `<hr>` | orientation, variant |83| `craft-breadcrumb` | `<nav>` | items, separator |84| `craft-menu` | `<nav>` | items, orientation |85| `craft-tree` | `<div>` | nodes, expandable, selectable |86| `craft-list` | `<ul>` | items, selectable |87| `craft-slider` | `<input type="range">` | value, min, max, step |88| `craft-radio` | `<input type="radio">` | checked, name, value, label |89| `craft-file-browser` | `<div>` | path, showHidden, selectable |9091## Reactivity System9293```typescript94import { ref, namedRef, computed, watch } from '@stacksjs/stx'9596const count = ref(0)97count.value = 59899const doubled = computed(() => count.value * 2)100101const stop = watch(102 () => count.value,103 (newVal, oldVal) => console.log(`${oldVal} → ${newVal}`),104 { immediate: false }105)106stop() // cleanup107```108109### Types110```typescript111interface Ref<T> { value: T | null, readonly current: T | null }112113interface ComponentInstance {114 id: string, element: Element | null115 mountHooks: LifecycleHook[], destroyHooks: CleanupFn[], updateHooks: LifecycleHook[]116 refs: Map<string, Ref<any>>, watchers: Array<{ stop: () => void }>117 isMounted: boolean118}119```120121## Lifecycle Hooks122123```typescript124import { onMount, onDestroy, onUpdate } from '@stacksjs/stx'125// Aliases: onMounted, onUnmounted, onUpdated126127onMount(() => {128 console.log('mounted')129 return () => console.log('cleanup') // optional130})131onDestroy(() => console.log('destroyed'))132onUpdate(() => console.log('updated'))133```134135## Dependency Injection136137```typescript138import { provide, inject, createInjectionKey, withInjectionScope } from '@stacksjs/stx'139140const ThemeKey = createInjectionKey<string>('theme')141provide(ThemeKey, 'dark')142const theme = inject(ThemeKey) // 'dark'143const theme = inject(ThemeKey, 'light') // with default144```145146## Browser Composables147148```typescript149import {150 useLocalStorage, useSessionStorage, useEventListener,151 useClickOutside, useWindowSize, useMediaQuery,152 usePrefersDark, useOnline153} from '@stacksjs/stx'154155const { value, remove } = useLocalStorage('key', defaultValue)156const { width, height } = useWindowSize()157const isDark = usePrefersDark()158const isOnline = useOnline()159const cleanup = useClickOutside(elementRef, handler)160```161162## Crosswind Configuration (config/ui.ts)163164```typescript165export default {166 content: [167 './resources/**/*.{html,js,ts,jsx,tsx,stx}',168 './storage/framework/defaults/**/*.{html,js,ts,jsx,tsx,stx}',169 './storage/framework/views/**/*.{html,js,ts,jsx,tsx,stx}',170 ],171 output: './storage/framework/assets/headwind.css',172 minify: false,173} satisfies CrosswindOptions174```175176## STX Configuration (config/ui.ts)177178```typescript179export default {180 componentsDir: 'components',181 layoutsDir: 'layouts',182 partialsDir: 'partials',183} satisfies StxOptions184```185186### Full StxConfig187```typescript188interface StxConfig {189 enabled: boolean, debug: boolean190 templatesDir?, componentsDir, partialsDir, layoutsDir?, defaultLayout?191 ssr?: boolean, cache?: boolean, cachePath: string192 i18n?: Partial<I18nConfig>193 webComponents?: Partial<WebComponentConfig>194 streaming?: Partial<StreamingConfig>195 hydration?: Partial<HydrationConfig>196 a11y?: Partial<A11yConfig>197 seo?: Partial<SeoFeatureConfig>198 animation?: Partial<AnimationConfig>199 markdown?: Partial<MarkdownConfig>200 pwa?: Partial<PwaConfig>201 strict?: boolean | StrictModeConfig202}203```204205## Accessibility206207```typescript208import { checkA11y, autoFixA11y, scanA11yIssues } from '@stacksjs/stx'209210const violations = await checkA11y(html, filePath)211const result = autoFixA11y(html, config)212const issues = await scanA11yIssues('./resources', { recursive: true })213```214215```typescript216interface A11yConfig {217 enabled: boolean, addSrOnlyStyles: boolean218 level: 'AA' | 'AAA', ignoreChecks?: string[], autoFix: boolean219}220```221222## Crosswind CSS Framework223224Utility-first CSS (like Tailwind), built into Stacks:225226```typescript227import { buildCrosswindCSS, extractClassNames, generateCrosswindCSS } from '@stacksjs/stx'228229const css = await buildCrosswindCSS(cwd)230const classNames = extractClassNames(htmlContent)231```232233Features: theme config, 40+ variant modifiers, custom rules, shortcuts, attributify mode, bracket syntax, presets.234235## Gotchas236- **@stacksjs/ui re-exports from @stacksjs/stx** — the UI package is thin, the engine is in STX237- **Craft components use native fallbacks** — `preferNative: true` renders plain HTML238- **Refs are not Vue refs** — similar API but custom reactive implementation239- **Lifecycle hooks require component context** — must be called within `setupComponent()`240- **Crosswind is not Tailwind** — Stacks' own CSS utility implementation241- **Crosswind is the utility engine** — handles class extraction, CSS generation, purging242- **STX is the templating engine** — handles `.stx` files, SSR, streaming, hydration243- **Two CSS systems coexist** — Crosswind (config) and Crosswind (engine)244- **150+ globally registered Vue components** — no imports needed