dist/SKILL.md
# AgentUI — Build Skill Guide > **Practical recipes for building web apps with AgentUI.** > For framework overview and API concepts, see [AGENTS.md](./AGENTS.md). > For full API reference, see [AGENTS_REFERENCE.md](./AGENTS_REFERENCE.md). > **AI-friendly web components framework.** 50 components, zero Shadow DOM. --- ## 📑 Index | Section | Description | |---------|-------------| | [🔥 Schema Form Showcase](#-schema-form-showcase) | Auto-generate entire forms from JSON Schema | | [🤖 Ready-
npx skillsauth add ghosthunter8/agentui distInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Practical recipes for building web apps with AgentUI. For framework overview and API concepts, see AGENTS.md. For full API reference, see AGENTS_REFERENCE.md.
AI-friendly web components framework. 50 components, zero Shadow DOM.
| Section | Description | |---------|-------------| | 🔥 Schema Form Showcase | Auto-generate entire forms from JSON Schema | | 🤖 Ready-to-Use Agent Patterns | Form data, input values, theme control, discovery | | 🏗️ PWA App Shell Components | Templates: dashboard, e-commerce, admin, full-bleed | | 🚀 Modern App Shell Pattern | Lazy loading, performance, 100/100 Lighthouse | | 📋 Common Patterns | Forms, modals, tabs, toast, drag & drop |
au-schema-formis AgentUI's killer feature for AI agents. Define a JSON Schema → get a complete, validated, accessible form with zero boilerplate.
<au-schema-form id="my-form"></au-schema-form>
<script type="module">
const form = document.getElementById('my-form');
form.schema = {
title: "User Registration",
required: ["email", "password", "name"],
properties: {
name: { type: "string", title: "Full Name", minLength: 2, maxLength: 50 },
email: { type: "string", title: "Email", format: "email" },
password: { type: "string", title: "Password", minLength: 8, placeholder: "Min 8 characters" },
age: { type: "integer", title: "Age", minimum: 18, maximum: 120 },
bio: { type: "string", title: "Bio", multiline: true, maxLength: 500 },
role: { type: "string", title: "Role", enum: ["user", "admin", "editor"], enumLabels: ["User", "Administrator", "Editor"] },
newsletter: { type: "boolean", title: "Subscribe to newsletter" }
}
};
form.addEventListener('au-submit', (e) => {
console.log('Form data:', e.detail);
// { name: "John", email: "[email protected]", password: "...", age: 25, bio: "...", role: "admin", newsletter: true }
});
</script>
What you get automatically:
string → au-input (with type detection: email, url, password)string + multiline: true → au-textareainteger/number → au-input type="number" with min/maxboolean → au-switchenum → au-dropdown with au-optionsrequired, minLength, maxLength, pattern, minimum, maximum, format (email/url)| Attribute | Description | Default |
|-----------|-------------|---------|
| submit-label | Submit button text | "Submit" |
| reset-label | Reset button text | "Reset" |
| inline | Horizontal layout | false |
| readonly | All fields read-only | false |
| disabled | All fields disabled | false |
form.schema = { ... }; // Set/update schema (triggers re-render)
form.getValues(); // Get current values as object
form.setValues({ name: "..." }); // Set values programmatically
form.validate(); // Returns boolean, shows errors
form.getErrors(); // Get error object { field: ["error"] }
form.reset(); // Reset to defaults
form.submit(); // Trigger submit programmatically
form.schema = {
title: "App Settings",
required: ["appName"],
properties: {
appName: { type: "string", title: "App Name", minLength: 1 },
darkMode: { type: "boolean", title: "Dark Mode" },
language: { type: "string", title: "Language", enum: ["en", "it", "es", "de"], enumLabels: ["English", "Italiano", "Español", "Deutsch"] },
maxItems: { type: "integer", title: "Max Items Per Page", minimum: 10, maximum: 100 },
apiUrl: { type: "string", title: "API Endpoint", format: "url", placeholder: "https://..." }
}
};
form.schema = {
required: ["name", "email", "message"],
properties: {
name: { type: "string", title: "Name", minLength: 2 },
email: { type: "string", title: "Email", format: "email" },
phone: { type: "string", title: "Phone", pattern: "^\\+?[0-9]{8,15}$", patternError: "Enter a valid phone number" },
subject: { type: "string", title: "Subject", enum: ["general", "support", "sales"], enumLabels: ["General Inquiry", "Technical Support", "Sales"] },
message: { type: "string", title: "Message", multiline: true, minLength: 10, maxLength: 1000 }
}
};
Copy-paste these patterns directly. Tested and verified for AI agent workflows.
// Get all form values at once (PREFERRED)
const form = document.querySelector('au-form');
const data = form.getValues(); // { email: '...', password: '...' }
// Validate before submission
if (form.validate()) {
// All required fields are filled
console.log('Form data:', data);
}
// Collect all au-input values by name
const inputs = document.querySelectorAll('au-input[name]');
const data = {};
inputs.forEach(input => {
data[input.getAttribute('name')] = input.value;
});
// Set value - works like native input
const input = document.querySelector('au-input');
input.value = 'new value';
console.log(input.value); // 'new value'
// Trigger validation
input.setAttribute('value', 'another value');
// Get component metadata at runtime
const ButtonClass = customElements.get('au-button');
const schema = ButtonClass.describe();
// Returns: {
// name: 'au-button',
// description: 'Material Design 3 button',
// props: { variant: { type: 'string', values: [...] }, ... },
// events: ['click'],
// examples: ['<au-button variant="filled">Click</au-button>']
// }
// Supported on: au-button, au-input, au-card, au-checkbox, au-switch,
// au-dropdown, au-textarea, au-radio-group, au-alert, au-toast,
// au-modal, au-spinner, au-progress
// Get all clickable/focusable AgentUI components
const interactive = document.querySelectorAll(
'au-button, au-input, au-checkbox, au-switch, au-chip, au-dropdown'
);
// Get component state
interactive.forEach(el => {
console.log(el.tagName, {
disabled: el.hasAttribute('disabled'),
value: el.value,
checked: el.checked
});
});
// Toggle dark/light mode
import { Theme } from 'agentui-wc';
Theme.toggle();
// Set specific theme
Theme.set('dark');
Theme.set('light');
// Get current theme
const current = Theme.get(); // 'dark' | 'light'
AgentUI includes a complete, responsive App Shell system. You don't need to build it — just compose these components.
AgentUI provides 5 components that together form a full PWA App Shell with responsive behavior baked in:
| Component | Role | Key Features |
|-----------|------|--------------|
| au-layout | Shell container — orchestrates header, drawer, content, footer, bottom nav. Use full-bleed for zero-padding layouts (kanban, maps). | 5 named slots: header, drawer, main (default), footer, bottom. Attr: full-bleed |
| au-drawer | Side navigation — responsive sidebar | mode: auto (recommended), permanent, temporary, rail. Supports expand-on-hover |
| au-drawer-item | Nav item inside drawer | icon, label, href, active, data-page |
| au-navbar | Top app bar | sticky, variant (surface, primary) |
| au-bottom-nav | Mobile bottom nav — auto-shows on compact screens | Hidden on desktop, visible on mobile |
┌──────────────────────────────────────────────────────┐
│ slot="header" → au-navbar (sticky top bar) │
├──────────┬───────────────────────────────────────────┤
│ │ │
│ slot= │ default slot (main content area) │
│ "drawer" │ ← Your page content goes here │
│ │ │
│ au-drawer│ │
│ │ │
├──────────┴───────────────────────────────────────────┤
│ slot="footer" → Optional footer │
├──────────────────────────────────────────────────────┤
│ slot="bottom" → au-bottom-nav (mobile only) │
└──────────────────────────────────────────────────────┘
mode="auto")| Screen Size | Drawer Behavior | Bottom Nav | |-------------|----------------|------------| | Desktop (≥ 840px) | Expanded sidebar, always visible | Hidden | | Tablet (600-839px) | Rail mode (icons only), expand on hover | Hidden | | Mobile (< 600px) | Hidden (opens as overlay on tap) | Visible |
This is 100% automatic with
mode="auto". No media queries, no JavaScript — the components handle all responsive transitions internally.
<au-layout>
<!-- Top bar with branding and actions -->
<au-navbar slot="header" sticky>
<au-navbar-brand>My Dashboard</au-navbar-brand>
<au-navbar-actions>
<au-theme-toggle></au-theme-toggle>
<au-icon-button icon="notifications"></au-icon-button>
<au-avatar src="user.jpg"></au-avatar>
</au-navbar-actions>
</au-navbar>
<!-- Sidebar: auto-responsive (expanded → rail → overlay) -->
<au-drawer slot="drawer" mode="auto" expand-on-hover>
<au-drawer-item icon="dashboard" href="#dashboard" active>Dashboard</au-drawer-item>
<au-drawer-item icon="people" href="#users">Users</au-drawer-item>
<au-drawer-item icon="analytics" href="#analytics">Analytics</au-drawer-item>
<au-drawer-item icon="settings" href="#settings">Settings</au-drawer-item>
</au-drawer>
<!-- Main content: changes on navigation -->
<au-container>
<main id="content">
<!-- Page content loads here -->
</main>
</au-container>
<!-- Mobile bottom nav: auto-visible on compact screens only -->
<au-bottom-nav slot="bottom">
<au-bottom-nav-item icon="dashboard" label="Dashboard" active></au-bottom-nav-item>
<au-bottom-nav-item icon="people" label="Users"></au-bottom-nav-item>
<au-bottom-nav-item icon="analytics" label="Analytics"></au-bottom-nav-item>
<au-bottom-nav-item icon="settings" label="Settings"></au-bottom-nav-item>
</au-bottom-nav>
</au-layout>
This gives you:
<au-layout>
<au-navbar slot="header" sticky>
<au-navbar-brand>ShopName</au-navbar-brand>
<au-navbar-actions>
<au-icon-button icon="search"></au-icon-button>
<au-icon-button icon="shopping_cart"></au-icon-button>
</au-navbar-actions>
</au-navbar>
<au-container>
<main id="content"><!-- Products grid --></main>
</au-container>
<au-bottom-nav slot="bottom">
<au-bottom-nav-item icon="home" label="Home" active></au-bottom-nav-item>
<au-bottom-nav-item icon="category" label="Categories"></au-bottom-nav-item>
<au-bottom-nav-item icon="shopping_cart" label="Cart"></au-bottom-nav-item>
<au-bottom-nav-item icon="person" label="Account"></au-bottom-nav-item>
</au-bottom-nav>
</au-layout>
<au-layout>
<au-navbar slot="header" sticky variant="primary">
<au-navbar-brand>Admin Panel</au-navbar-brand>
</au-navbar>
<!-- Permanent drawer: always visible, never collapses -->
<au-drawer slot="drawer" mode="permanent">
<au-drawer-item icon="dashboard" href="#overview" active>Overview</au-drawer-item>
<au-drawer-item icon="group" href="#users">Users</au-drawer-item>
<au-drawer-item icon="assessment" href="#reports">Reports</au-drawer-item>
<au-drawer-item icon="admin_panel_settings" href="#config">Config</au-drawer-item>
</au-drawer>
<main id="content"></main>
</au-layout>
💡 Key insight for agents:
au-layoutis NOT just a CSS grid wrapper — it's a responsive orchestrator. When you putau-drawerinslot="drawer"andau-bottom-navinslot="bottom", the layout automatically coordinates their visibility across breakpoints. You don't write any responsive CSS or JavaScript — the components talk to each other internally.
[!CAUTION] NEVER override
paddingon.au-layout-content— it silently defeats bottom-nav compensation. For zero-padding layouts (kanban, maps, dashboards), use<au-layout full-bleed>. The framework emits aconsole.warnat runtime if it detects the override.
<au-layout full-bleed>
<au-navbar slot="header" sticky>
<au-navbar-brand>Kanban Board</au-navbar-brand>
</au-navbar>
<!-- Content fills edge-to-edge, zero padding -->
<div id="kanban-board" style="display: flex; gap: 16px; height: 100%; padding: 16px;">
<!-- Your columns -->
</div>
<au-bottom-nav slot="bottom">
<au-bottom-nav-item icon="view_kanban" label="Board" active></au-bottom-nav-item>
<au-bottom-nav-item icon="list" label="List"></au-bottom-nav-item>
</au-bottom-nav>
</au-layout>
For production apps, use this pattern instead of Minimal Page Setup. This is how
demo/index.htmlachieves 100/100 Lighthouse with all 50 components.
Why App Shell? | Approach | Initial Load | Lighthouse | DX for Agents | |----------|--------------|------------|---------------| | Minimal (all-in-one) | ~60KB + content | 70-85 | Simple but suboptimal | | App Shell (lazy) | ~20KB shell → routes lazy | 100/100 | Optimal performance |
┌─────────────────────────────────────────────────────────────┐
│ INITIAL LOAD (~20KB) │
│ index.html + agentui.css + shell-critical.js │
│ → Renders navbar, drawer, footer instantly │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ ON NAVIGATION (lazy) │
│ dist/routes/{page}.js → Component bundle for that page │
│ content/{page}.html → HTML content fragment │
└─────────────────────────────────────────────────────────────┘
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My AgentUI App</title>
<!-- Critical CSS: async load prevents render blocking -->
<link rel="preload" as="style" href="dist/agentui.css">
<link rel="stylesheet" href="dist/agentui.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="dist/agentui.css"></noscript>
<!-- Modern 2026: Speculation Rules for prefetch -->
<script type="speculationrules">
{
"prefetch": [{ "urls": ["/dist/routes/home.js", "/dist/routes/dashboard.js"], "eagerness": "moderate" }]
}
</script>
<style>
/* Font + critical inline CSS */
@font-face { font-family: 'Roboto'; font-display: swap; src: url('https://fonts.gstatic.com/s/roboto/v47/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiA8.woff2') format('woff2'); }
body { font-family: var(--md-sys-typescale-font); background: var(--md-sys-color-background); margin: 0; }
:root { --md-sys-color-background: #FFFBFE; }
[data-theme="dark"] { --md-sys-color-background: #141218; }
/* Modern: Lazy render off-screen content */
au-example { content-visibility: auto; contain-intrinsic-size: auto 300px; }
</style>
</head>
<body>
<!-- App Shell: renders instantly -->
<au-layout>
<header slot="header">
<au-theme-toggle></au-theme-toggle>
</header>
<au-drawer slot="drawer" mode="auto">
<au-drawer-item icon="home" href="#home" data-page="home" active>Home</au-drawer-item>
<au-drawer-item icon="dashboard" href="#dashboard" data-page="dashboard">Dashboard</au-drawer-item>
</au-drawer>
<!-- Dynamic content area -->
<main id="content"></main>
</au-layout>
<script type="module">
// ====================================
// LAZY LOADING ENGINE (copy this!)
// ====================================
const loadedRoutes = new Set();
const contentArea = document.getElementById('content');
// Load route bundle on demand
async function loadRoute(name) {
if (loadedRoutes.has(name)) return;
loadedRoutes.add(name);
await import(`./dist/routes/${name}.js`);
}
// Load HTML content fragment
async function loadContent(pageId) {
const response = await fetch(`./content/${pageId}.html`);
return response.ok ? await response.text() : null;
}
// Navigate with View Transitions (smooth)
async function showPage(pageId) {
await loadRoute(pageId);
const content = await loadContent(pageId);
const updateDOM = () => {
contentArea.innerHTML = content || `<h1>${pageId}</h1>`;
// Execute inline scripts
contentArea.querySelectorAll('script').forEach(s => {
const newScript = document.createElement('script');
newScript.textContent = s.textContent;
s.replaceWith(newScript);
});
};
// Modern: View Transitions API for smooth navigation
if (document.startViewTransition) {
await document.startViewTransition(updateDOM).finished;
} else {
updateDOM();
}
}
// Prefetch on hover (anticipate navigation)
document.querySelectorAll('au-drawer-item').forEach(item => {
item.addEventListener('mouseenter', () => loadRoute(item.dataset.page));
item.addEventListener('click', e => {
e.preventDefault();
window.location.hash = item.dataset.page;
});
});
// Hash-based routing
window.addEventListener('hashchange', () => showPage(location.hash.slice(1) || 'home'));
showPage(location.hash.slice(1) || 'home');
</script>
</body>
</html>
| Technique | Code | Why It Matters |
|-----------|------|----------------|
| Async CSS | media="print" onload="this.media='all'" | Prevents render-blocking |
| Speculation Rules | <script type="speculationrules"> | Browser prefetches likely routes |
| content-visibility | content-visibility: auto | Skips rendering off-screen content |
| View Transitions | document.startViewTransition() | Smooth page transitions |
| Hover prefetch | mouseenter → loadRoute() | Loads before user clicks |
| Route caching | loadedRoutes.has(name) | Never re-download same route |
my-app/
├── index.html # App shell (copy template above)
├── content/ # HTML fragments (lazy loaded)
│ ├── home.html
│ ├── dashboard.html
│ └── settings.html
├── dist/
│ ├── agentui.css
│ ├── agentui.esm.js
│ └── routes/ # Route bundles (auto-generated by build)
│ ├── home.js
│ ├── dashboard.js
│ └── settings.js
└── app/
└── pages/ # Source pages (build input)
├── home.html
├── dashboard.html
└── settings.html
bun run build # Generates dist/routes/*.js from app/pages/*.html
<!-- Form with validation -->
<au-form>
<au-stack gap="md">
<au-input label="Email" type="email" required></au-input>
<au-input label="Password" type="password" required></au-input>
<au-button variant="filled">Submit</au-button>
</au-stack>
</au-form>
<!-- Card layout -->
<au-grid cols="3" gap="md">
<au-card variant="elevated">
<h3>Title</h3>
<p>Content</p>
<au-button variant="text">Action</au-button>
</au-card>
</au-grid>
<!-- Navigation tabs -->
<au-tabs active="0">
<au-tab>Tab 1</au-tab>
<au-tab>Tab 2</au-tab>
</au-tabs>
<!-- Modal dialog -->
<au-modal id="my-modal">
<h2>Modal Title</h2>
<p>Content</p>
<au-button onclick="this.closest('au-modal').close()">Close</au-button>
</au-modal>
<au-button onclick="document.getElementById('my-modal').open()">Open Modal</au-button>
<!-- Toast notification -->
<script type="module">
import { showToast } from './dist/agentui.esm.js';
showToast('Success!', { severity: 'success', duration: 3000 });
</script>
<!-- Drag & Drop (native HTML5, works with any component) -->
<au-card draggable="true" data-id="123"
ondragstart="event.dataTransfer.setData('text/plain', this.dataset.id)"
ondragend="this.style.opacity = '1'">
Drag me
</au-card>
<au-card ondragover="event.preventDefault(); this.classList.add('drag-over')"
ondragleave="this.classList.remove('drag-over')"
ondrop="handleDrop(event, this)">
Drop here
</au-card>
development
# AgentUI — Build Skill Guide > **Practical recipes for building web apps with AgentUI.** > For framework overview and API concepts, see [AGENTS.md](./AGENTS.md). > For full API reference, see [AGENTS_REFERENCE.md](./AGENTS_REFERENCE.md). > **AI-friendly web components framework.** 57 components, zero Shadow DOM. --- ## 📑 Index | Section | Description | |---------|-------------| | [🔥 Schema Form Showcase](#-schema-form-showcase) | Auto-generate entire forms from JSON Schema | | [🤖 Ready-
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.