Skip to content
Blog

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 mode state 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

  1. Theme at the root — wrap everything with <Theme>
  2. No <div> — every layout concern uses an Astryx component
  3. State-based routing — no React Router needed for simple flows
  4. Validation via status prop — consistent error display across inputs
  5. Responsive via component propsStack direction, Collapsible, Grid
  6. Token-based values — no hardcoded colors, spacing, or radii