Building Real-World Applications with Astryx: 6 Workshop Patterns
Six production-ready application patterns built with Astryx — Chat UI, Kanban Board, Analytics Dashboard, Settings Page, Checkout Form, and Authentication Flow.
Published on • July 31, 2026
AI Assistant

This tutorial extracts the architectural patterns from six real-world workshops. Each pattern demonstrates how Astryx components come together to build production-grade interfaces.
Pattern 1: Chat UI
A complete chat interface with dark/light theme toggle, artifact panel, and responsive split-pane layout.
import { Theme } from '@astryxdesign/core/theme';
import { neutralTheme } from '@astryxdesign/theme-neutral/built';
export default function App() {
const [mode, setMode] = useState<'light' | 'dark'>('light');
return (
<Theme theme={neutralTheme} mode={mode}>
<VStack height="100dvh">
<TopNav
label="Main navigation"
heading={<TopNavHeading heading="Chat UI"
logo={<NavIcon icon={<Icon icon={ChatBubbleIcon} size="sm" />} />}
/>}
endContent={
<Button label={mode === 'light' ? 'Dark mode' : 'Light mode'}
variant="ghost" isIconOnly
icon={<Icon icon={mode === 'light' ? MoonIcon : SunIcon} size="sm" />}
onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}
/>
}
/>
<ChatPage />
</VStack>
</Theme>
);
}
Chat Interface
<ChatLayout density="spacious"
composer={
<ChatComposer onSubmit={handleSend} placeholder="Ask anything..."
input={<ChatComposerInput />}
/>
}
>
<ChatMessageList>
<ChatSystemMessage variant="divider">Today</ChatSystemMessage>
<ChatMessage sender="assistant" avatar={<Avatar name="AI" size="md" />}>
<ChatMessageBubble variant="ghost">How can I help?</ChatMessageBubble>
<ChatMessageMetadata timestamp={<Timestamp value="10:15" format="time" />} />
</ChatMessage>
<ChatMessage sender="user">
<ChatMessageBubble>Review the auth files</ChatMessageBubble>
</ChatMessage>
<ChatMessage sender="assistant">
<ChatMessageBubble variant="ghost">
<ChatTokenizedText tokens={MENTION_TOKENS}>@agent Here are the auth files</ChatTokenizedText>
</ChatMessageBubble>
<ChatToolCalls calls={[
{ name: 'read', target: 'auth-service.ts', status: 'complete', duration: '45ms' },
]} />
</ChatMessage>
</ChatMessageList>
</ChatLayout>
Key Patterns
- Theme toggle via
modestate on<Theme> - ChatLayout wraps composer + message list
- ChatToolCalls shows AI agent tool call status
- ChatTokenizedText handles inline mentions and tokens
Pattern 2: Kanban Board
A drag-and-drop Kanban board with color-coded columns, priority tags, and localStorage persistence — using Pointer Events, no external DnD library.
<Card padding={3} variant="muted">
<HStack gap={2} vAlign="center">
<StatusDot variant="accent" />
<Heading level={4}>In Progress</Heading>
<Badge label={count} variant="neutral" />
</HStack>
<VStack gap={2}>
{items.map(item => (
<Card key={item.id} padding={3} onPointerDown={e => onPointerDown(e, item.id)}>
<VStack gap={2}>
<HStack hAlign="between" vAlign="start">
<Badge label={item.ref} variant="neutral" />
<Badge label={priority.label} variant={priority.variant} />
</HStack>
<Heading level={4}>{item.title}</Heading>
<Text type="supporting" color="secondary" maxLines={2}>{item.description}</Text>
</VStack>
</Card>
))}
</VStack>
</Card>
Task Creation Dialog
<Dialog open={!!editMode} onOpenChange={() => setEditMode(null)} purpose="form" width={480}>
<DialogHeader title="Add Task" />
<VStack gap={3} padding={4}>
<TextInput label="Task ref" value={formRef} onChange={setFormRef} />
<Selector label="Priority" value={formPriority} onChange={setFormPriority}
options={priorityOptions} />
<TextInput label="Title" value={formTitle} onChange={setFormTitle} />
<TextArea label="Description" value={formDescription} onChange={setFormDescription} />
</VStack>
<HStack hAlign="end" gap={2} padding={4}>
<Button label="Cancel" variant="secondary" onClick={() => setEditMode(null)} />
<Button label="Create" variant="primary" onClick={handleFormSubmit} />
</HStack>
</Dialog>
Pattern 3: Analytics Dashboard
An analytics dashboard with KPI cards, line charts, stacked bars, and engagement tables using Recharts + Astryx.
<AppShell contentPadding={0}
sideNav={
<SideNav collapsible
header={<SideNavHeading icon={<NavIcon icon={<CubeIcon />} />}
heading="Analytics" headingHref="/" />}
>
<SideNavSection title="Navigation" isHeaderHidden>
<SideNavItem label="Dashboard" icon={HomeIcon} isSelected href="/" />
<SideNavItem label="Analytics" icon={ChartBarIcon} href="/analytics" />
</SideNavSection>
</SideNav>
}
>
<DashboardContent />
</AppShell>
KPI Metric Cards
<Card>
<VStack gap={2}>
<Heading level={4}>Revenue</Heading>
<HStack gap={2} vAlign="center">
<Heading level={2}>$42,500</Heading>
<HStack gap={1} vAlign="center">
{positive
? <Icon icon={ArrowUpIcon} size="xsm" color="success" />
: <Icon icon={ArrowDownIcon} size="xsm" color="error" />}
<Text color="secondary">+12.5%</Text>
</HStack>
</HStack>
</VStack>
</Card>
Engagement Table
<Table<PageRow>
data={data}
columns={[
{ key: 'page', header: 'Page', width: pixel(160) },
{ key: 'views', header: 'Views', width: proportional(1),
renderCell: (item) => (
<VStack gap={1}>
<ProgressBar value={item.views} max={maxViews} isLabelHidden />
<Text type="supporting">{item.views.toLocaleString()} views</Text>
</VStack>
)},
]}
idKey="id" density="compact" dividers="rows" hasHover
/>
Pattern 4: Settings Page
A settings page with sidebar navigation for 5 categories, editable inline fields, and toggle switches.
State-Based Routing (No React Router)
const SECTIONS = [
{ key: 'personal', label: 'Personal Information', icon: UserIcon },
{ key: 'login', label: 'Login & Security', icon: ShieldIcon },
{ key: 'privacy', label: 'Privacy', icon: EyeIcon },
{ key: 'notifications', label: 'Notifications', icon: BellIcon },
{ key: 'language', label: 'Language & Currency', icon: GlobeIcon },
];
const SECTION_COMPONENTS: Record<string, () => ReactElement> = {
personal: PersonalInfoSection,
login: LoginAndSecuritySection,
};
export default function SettingsPage() {
const [activeSection, setActiveSection] = useState('personal');
const ActiveComponent = SECTION_COMPONENTS[activeSection];
return (
<AppShell contentPadding={4}
sideNav={
<SideNav header={<SideNavHeading heading="Settings" />}>
<SideNavSection title="Sections" isHeaderHidden>
{SECTIONS.map(({ key, label, icon: Icon }) => (
<SideNavItem key={key} label={label}
icon={<Icon size={16} />}
isSelected={activeSection === key}
onClick={() => setActiveSection(key)} />
))}
</SideNavSection>
</SideNav>
}
>
<ActiveComponent />
</AppShell>
);
}
Pattern 5: Checkout / Payment Form
A complete checkout page with shipping info, delivery method, payment form, promo code, and order summary sidebar.
const isMobile = useMediaQuery('(max-width: 767px)');
<Stack direction={isMobile ? 'vertical' : 'horizontal'} gap={8} vAlign="start">
<StackItem size="fill">
<FormLayout>
<Text type="large" weight="bold">Shipping Information</Text>
<FormLayout direction="horizontal">
<TextInput size="lg" label="First Name" value={firstName} onChange={setFirstName} />
<TextInput size="lg" label="Last Name" value={lastName} onChange={setLastName} />
</FormLayout>
<TextInput size="lg" label="Address" value={address} onChange={setAddress} />
</FormLayout>
<RadioList label="Delivery method" value={deliveryMethod} onChange={setDeliveryMethod}>
<RadioListItem value="standard" label="Standard (3-7 days)"
endContent={<Text weight="medium">$4.95</Text>} />
<RadioListItem value="expedited" label="Expedited (1-2 days)"
endContent={<Text weight="medium">$9.95</Text>} />
</RadioList>
</StackItem>
<StackItem size="fill" style={isMobile ? { order: -1 } : { position: 'sticky', top: 'var(--spacing-4)' }}>
<Card padding={5}>
<Collapsible trigger="Order Summary" defaultIsOpen={!isMobile}>
<VStack gap={4}>
{ORDER_ITEMS.map(item => (
<HStack key={item.id} gap={3} vAlign="start">
<Thumbnail src={item.image} alt={item.name} />
<Text weight="medium">{item.name}</Text>
</HStack>
))}
<Divider />
<HStack hAlign="between">
<Text type="large" weight="bold">Total</Text>
<Text type="large" weight="bold">{fmt(total)}</Text>
</HStack>
</VStack>
</Collapsible>
</Card>
</StackItem>
</Stack>
Pattern 6: Authentication Flow
A three-view auth flow (Login, Sign Up, Forgot Password) with form validation and state-based routing.
type View = 'login' | 'signup' | 'forgot-password';
export default function App() {
const [view, setView] = useState<View>('login');
return (
<Theme theme={stoneTheme}>
<Center axis="both" width="100%" style={{ minHeight: '100dvh' }}>
{view === 'login' && <LoginView onSignUp={() => setView('signup')}
onForgotPassword={() => setView('forgot-password')} />}
{view === 'signup' && <SignUpView onLogin={() => setView('login')} />}
{view === 'forgot-password' && <ForgotPasswordView onBackToLogin={() => setView('login')} />}
</Center>
</Theme>
);
}
Login View (No <div>)
<Card maxWidth={400} width="100%">
<VStack gap={4}>
<Heading level={3}>Sign In</Heading>
<TextInput label="Email" type="email" value={email}
onChange={setEmail} placeholder="you@example.com" isRequired
status={submitted && errors.email
? { type: 'error', message: errors.email } : undefined} />
<TextInput label="Password" type="password" value={password}
onChange={setPassword} placeholder="Enter your password" isRequired
status={submitted && errors.password
? { type: 'error', message: errors.password } : undefined} />
<CheckboxInput label="Remember me" value={remember} onChange={setRemember} />
<Button label="Sign In" variant="primary" width="100%" onClick={handleSubmit} />
<VStack gap={1} hAlign="center">
<Text color="secondary">
Don't have an account?{' '}
<Link href="#" onClick={(e) => { e.preventDefault(); onSignUp(); }}>Sign Up</Link>
</Text>
</VStack>
</VStack>
</Card>
Common Patterns Across All Workshops
- Theme at the root — wrap everything with
<Theme> - No
<div>— every layout concern uses an Astryx component - State-based routing — no React Router needed for simple flows
- Validation via
statusprop — consistent error display across inputs - Responsive via component props —
Stack direction,Collapsible,Grid - Token-based values — no hardcoded colors, spacing, or radii