@aspect-ops/exon-ui
v0.4.0
Published
Reusable Svelte UI components for web and Capacitor mobile apps
Maintainers
Readme
@aspect-ops/exon-ui
A modern, accessible UI component library for Svelte 5 with first-class support for Capacitor mobile apps.
Features
- Svelte 5 - Built with runes and modern Svelte patterns
- Mobile-first - Designed for Capacitor native app conversion
- Accessible - WCAG 2.1 compliant with proper ARIA attributes
- Themeable - CSS custom properties for light/dark themes
- Tree-shakeable - Import only what you need
- TypeScript - Full type definitions included
Installation
npm install @aspect-ops/exon-uiQuick Start
<script>
import { Button, Typography } from '@aspect-ops/exon-ui';
import '@aspect-ops/exon-ui/styles';
</script>
<Typography variant="h1">Hello World</Typography>
<Button variant="primary">Click Me</Button>Components
Core Components
| Component | Description |
| ------------ | --------------------------------------------- |
| Button | Buttons with variants, sizes, loading states |
| Typography | Semantic text with heading and body variants |
| Icon | SVG icon component with size options |
| Badge | Status badges with color variants |
| Link | Accessible links with external link detection |
Form Components
| Component | Description |
| --------------- | ------------------------------------------ |
| TextInput | Text input with validation states |
| Textarea | Multi-line input with auto-resize |
| Select | Dropdown select with keyboard navigation |
| Checkbox | Single checkbox with indeterminate state |
| CheckboxGroup | Group of checkboxes with shared state |
| Radio | Single radio button |
| RadioGroup | Radio button group with orientation |
| Switch | Toggle switch component |
| FormField | Label wrapper with helper/error text |
| SearchInput | Search with autocomplete suggestions |
| DatePicker | Date selection with calendar popup |
| TimePicker | Time selection with hour/minute picker |
| FileUpload | Drag-drop file upload with previews |
| OTPInput | One-time password input |
| NumberInput | Number input with +/- buttons and keyboard |
| ToggleGroup | Single/multi select button group |
Navigation Components
| Component | Description |
| ------------------------------------------------ | ---------------------------------- |
| Tabs, TabList, TabTrigger, TabContent | Accessible tabs with Bits UI |
| Menu, MenuTrigger, MenuContent, MenuItem | Dropdown menus with submenus |
| Breadcrumbs, BreadcrumbItem | Navigation breadcrumbs |
| BottomNav, BottomNavItem | Mobile bottom navigation |
| Navbar, NavItem | Responsive header with mobile menu |
| Sidebar, SidebarItem, SidebarGroup | Collapsible sidebar navigation |
| Stepper, StepperStep | Multi-step progress indicator |
| Pagination | Page navigation with ellipsis |
Data Display Components
| Component | Description |
| ---------------------------- | -------------------------------------------- |
| Accordion, AccordionItem | Collapsible content panels |
| Slider | Range slider with drag/keyboard support |
| Carousel, CarouselSlide | Image/content carousel with swipe |
| Image | Lazy loading image with placeholder/fallback |
| Rating | Star rating display/input |
| Chip, ChipGroup | Tag/filter chips with selection |
Mobile Components
| Component | Description |
| ----------------------------------------------------- | ------------------------------------------- |
| ActionSheet, ActionSheetItem | iOS/Android-style bottom action menu |
| BottomSheet, BottomSheetHeader, BottomSheetBody | Draggable bottom sheet with snap points |
| FAB | Floating action button with positions |
| FABGroup | Speed dial FAB with expandable actions |
| PullToRefresh | Pull-to-refresh gesture for lists |
| SwipeActions | Swipe-to-reveal actions on list items |
| SafeArea | Safe area inset wrapper for notched devices |
Usage Examples
Button
<script>
import { Button } from '@aspect-ops/exon-ui';
</script>
<!-- Variants -->
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<!-- Sizes -->
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<!-- States -->
<Button loading>Loading...</Button>
<Button disabled>Disabled</Button>
<Button fullWidth>Full Width</Button>Typography
<script>
import { Typography } from '@aspect-ops/exon-ui';
</script>
<Typography variant="h1">Heading 1</Typography>
<Typography variant="h2">Heading 2</Typography>
<Typography variant="body">Body text</Typography>
<Typography variant="body-sm">Small text</Typography>
<Typography variant="caption">Caption text</Typography>Form Components
<script>
import { FormField, TextInput, Select, Checkbox, Switch } from '@aspect-ops/exon-ui';
let email = $state('');
let country = $state('');
let newsletter = $state(false);
let darkMode = $state(false);
</script>
<FormField label="Email" required helperText="We'll never share your email">
<TextInput type="email" bind:value={email} placeholder="[email protected]" />
</FormField>
<FormField label="Country">
<Select
bind:value={country}
options={[
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'in', label: 'India' }
]}
/>
</FormField>
<Checkbox bind:checked={newsletter}>
{#snippet children()}
Subscribe to newsletter
{/snippet}
</Checkbox>
<Switch bind:checked={darkMode}>
{#snippet children()}
Dark Mode
{/snippet}
</Switch>Tabs
<script>
import { Tabs, TabList, TabTrigger, TabContent } from '@aspect-ops/exon-ui';
let activeTab = $state('tab1');
</script>
<Tabs bind:value={activeTab}>
<TabList>
<TabTrigger value="tab1">Account</TabTrigger>
<TabTrigger value="tab2">Security</TabTrigger>
<TabTrigger value="tab3">Notifications</TabTrigger>
</TabList>
<TabContent value="tab1">
<p>Account settings content</p>
</TabContent>
<TabContent value="tab2">
<p>Security settings content</p>
</TabContent>
<TabContent value="tab3">
<p>Notification preferences</p>
</TabContent>
</Tabs>
<!-- Scrollable tabs -->
<Tabs>
<TabList scrollable>
{#each Array(10) as _, i}
<TabTrigger value="tab-{i}">Tab {i + 1}</TabTrigger>
{/each}
</TabList>
</Tabs>
<!-- Vertical tabs -->
<Tabs orientation="vertical">
<TabList>
<TabTrigger value="v1">Item 1</TabTrigger>
<TabTrigger value="v2">Item 2</TabTrigger>
</TabList>
</Tabs>Menu / Dropdown
<script>
import {
Menu,
MenuTrigger,
MenuContent,
MenuItem,
MenuSeparator,
MenuSub,
MenuSubTrigger,
MenuSubContent
} from '@aspect-ops/exon-ui';
</script>
<Menu>
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>
<MenuItem icon="📝">Edit</MenuItem>
<MenuItem icon="📋">Copy</MenuItem>
<MenuSeparator />
<MenuSub>
<MenuSubTrigger>More Options</MenuSubTrigger>
<MenuSubContent>
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
</MenuSubContent>
</MenuSub>
<MenuSeparator />
<MenuItem icon="🗑️">Delete</MenuItem>
</MenuContent>
</Menu>Breadcrumbs
<script>
import { Breadcrumbs } from '@aspect-ops/exon-ui';
const items = [
{ label: 'Home', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'Category', href: '/products/category' },
{ label: 'Current Page' }
];
</script>
<Breadcrumbs {items} />
<!-- Custom separator -->
<Breadcrumbs {items}>
{#snippet separator()}
→
{/snippet}
</Breadcrumbs>Bottom Navigation (Mobile)
<script>
import { BottomNav } from '@aspect-ops/exon-ui';
let activeIndex = $state(0);
const items = [
{ label: 'Home', icon: '🏠' },
{ label: 'Search', icon: '🔍' },
{ label: 'Inbox', icon: '📬', badge: 5 },
{ label: 'Profile', icon: '👤' }
];
</script>
<BottomNav {items} {activeIndex} onchange={(index) => (activeIndex = index)} />Navbar
<script>
import { Navbar, NavItem, Button } from '@aspect-ops/exon-ui';
</script>
<Navbar>
{#snippet logo()}
<span>MyApp</span>
{/snippet}
<NavItem label="Home" href="/" active />
<NavItem label="Products" href="/products" />
<NavItem label="About" href="/about" />
{#snippet actions()}
<Button size="sm">Sign In</Button>
{/snippet}
</Navbar>Sidebar
<script>
import { Sidebar, SidebarItem, SidebarGroup } from '@aspect-ops/exon-ui';
let collapsed = $state(false);
</script>
<Sidebar bind:collapsed>
<SidebarGroup label="Main">
<SidebarItem icon="🏠" label="Dashboard" active />
<SidebarItem icon="📊" label="Analytics" badge={3} />
<SidebarItem icon="📁" label="Projects" />
</SidebarGroup>
<SidebarGroup label="Settings" collapsible>
<SidebarItem icon="👤" label="Profile" />
<SidebarItem icon="🔒" label="Security" />
</SidebarGroup>
</Sidebar>Advanced Form Components
SearchInput
Search input with autocomplete suggestions and keyboard navigation.
Props:
| Prop | Type | Default | Description |
| ---------------- | ----------- | ---------- | --------------------------------- |
| value | string | '' | Bindable search value |
| placeholder | string | 'Search' | Input placeholder text |
| suggestions | string[] | [] | Array of autocomplete suggestions |
| size | InputSize | 'md' | Input size: sm, md, lg |
| disabled | boolean | false | Disabled state |
| loading | boolean | false | Show loading spinner |
| maxSuggestions | number | 5 | Maximum suggestions to display |
| onselect | function | - | Callback when suggestion selected |
Usage:
<script>
import { SearchInput } from '@aspect-ops/exon-ui';
let query = $state('');
const suggestions = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
</script>
<SearchInput bind:value={query} {suggestions} onselect={(val) => console.log('Selected:', val)} />DatePicker
Date selection with calendar popup and keyboard navigation.
Props:
| Prop | Type | Default | Description |
| ------------- | ----------- | --------------- | ----------------------- |
| value | Date | null | Bindable selected date |
| placeholder | string | 'Select date' | Input placeholder |
| min | Date | null | Minimum selectable date |
| max | Date | null | Maximum selectable date |
| disabled | boolean | false | Disabled state |
| size | InputSize | 'md' | Input size: sm, md, lg |
| format | string | 'yyyy-MM-dd' | Date format string |
Usage:
<script>
import { DatePicker } from '@aspect-ops/exon-ui';
let selectedDate = $state(null);
</script>
<DatePicker bind:value={selectedDate} placeholder="Choose a date" />TimePicker
Time selection with scrollable hour/minute/period picker.
Props:
| Prop | Type | Default | Description |
| ------------ | ---------------- | ------- | --------------------------- |
| value | string | '' | Bindable time value (HH:mm) |
| format | '12h' \| '24h' | '24h' | Time format |
| minuteStep | number | 1 | Minute increment step |
| min | string | - | Minimum selectable time |
| max | string | - | Maximum selectable time |
| disabled | boolean | false | Disabled state |
| size | InputSize | 'md' | Input size: sm, md, lg |
Usage:
<script>
import { TimePicker } from '@aspect-ops/exon-ui';
let time = $state('');
</script>
<TimePicker bind:value={time} format="12h" minuteStep={15} />FileUpload
Drag-and-drop file upload with image previews and validation.
Props:
| Prop | Type | Default | Description |
| ------------- | --------- | ------- | -------------------------------- |
| files | File[] | [] | Bindable array of uploaded files |
| accept | string | '' | Accepted file types |
| multiple | boolean | false | Allow multiple file selection |
| maxSize | number | 10MB | Maximum file size in bytes |
| maxFiles | number | 10 | Maximum number of files |
| showPreview | boolean | true | Show image previews |
Usage:
<script>
import { FileUpload } from '@aspect-ops/exon-ui';
let files = $state([]);
</script>
<FileUpload bind:files multiple accept="image/*" maxSize={5242880} />OTPInput
One-time password input with auto-focus and paste support.
Props:
| Prop | Type | Default | Description |
| ------------ | ----------------------------- | ----------- | -------------------------------- |
| value | string | '' | Bindable OTP value |
| length | number | 6 | Number of OTP digits |
| type | 'numeric' \| 'alphanumeric' | 'numeric' | Input validation type |
| masked | boolean | false | Show dots instead of characters |
| disabled | boolean | false | Disabled state |
| size | Size | 'md' | Input size: sm, md, lg |
| oncomplete | function | - | Callback when all digits entered |
Usage:
<script>
import { OTPInput } from '@aspect-ops/exon-ui';
let otp = $state('');
</script>
<OTPInput bind:value={otp} length={6} oncomplete={(code) => console.log('OTP:', code)} />NumberInput
Number input with increment/decrement buttons and keyboard support.
Props:
| Prop | Type | Default | Description |
| --------------- | ------------------------- | ------- | --------------------------- |
| value | number \| null | null | Bindable number value |
| min | number | - | Minimum allowed value |
| max | number | - | Maximum allowed value |
| step | number | 1 | Increment/decrement step |
| disabled | boolean | false | Disabled state |
| placeholder | string | - | Input placeholder |
| error | boolean | false | Error state styling |
| onValueChange | (value: number) => void | - | Callback when value changes |
Usage:
<script>
import { NumberInput } from '@aspect-ops/exon-ui';
let quantity = $state(1);
</script>
<NumberInput bind:value={quantity} min={1} max={99} step={1} />
<!-- With error state -->
<NumberInput bind:value={quantity} min={0} max={10} error={quantity > 10} />ToggleGroup
Button group for single or multiple selection.
Props (ToggleGroup):
| Prop | Type | Default | Description |
| --------------- | ---------------------------- | -------------- | --------------------------- |
| type | 'single' \| 'multiple' | 'single' | Selection mode |
| value | string \| string[] | '' \| [] | Bindable selected value(s) |
| onValueChange | (value) => void | - | Callback when value changes |
| disabled | boolean | false | Disabled state |
| orientation | 'horizontal' \| 'vertical' | 'horizontal' | Layout direction |
Props (ToggleGroupItem):
| Prop | Type | Default | Description |
| ---------- | --------- | ------- | --------------------- |
| value | string | - | Item value (required) |
| disabled | boolean | false | Disabled state |
Usage:
<script>
import { ToggleGroup, ToggleGroupItem } from '@aspect-ops/exon-ui';
let alignment = $state('left');
let formats = $state([]);
</script>
<!-- Single selection (radio-like) -->
<ToggleGroup bind:value={alignment} type="single">
{#snippet children()}
<ToggleGroupItem value="left">Left</ToggleGroupItem>
<ToggleGroupItem value="center">Center</ToggleGroupItem>
<ToggleGroupItem value="right">Right</ToggleGroupItem>
{/snippet}
</ToggleGroup>
<!-- Multiple selection (checkbox-like) -->
<ToggleGroup bind:value={formats} type="multiple">
{#snippet children()}
<ToggleGroupItem value="bold">B</ToggleGroupItem>
<ToggleGroupItem value="italic">I</ToggleGroupItem>
<ToggleGroupItem value="underline">U</ToggleGroupItem>
{/snippet}
</ToggleGroup>Pagination
Page navigation with ellipsis for large page ranges.
Props:
| Prop | Type | Default | Description |
| -------------- | ------------------------ | ------- | --------------------------------- |
| currentPage | number | - | Current page number (required) |
| totalPages | number | - | Total number of pages (required) |
| siblingCount | number | 1 | Pages to show around current page |
| onPageChange | (page: number) => void | - | Callback when page changes |
Usage:
<script>
import { Pagination } from '@aspect-ops/exon-ui';
let currentPage = $state(1);
const totalPages = 20;
</script>
<Pagination {currentPage} {totalPages} onPageChange={(page) => (currentPage = page)} />
<!-- With more visible siblings -->
<Pagination
{currentPage}
{totalPages}
siblingCount={2}
onPageChange={(page) => (currentPage = page)}
/>Keyboard Navigation:
←/→: Previous / Next pageHome: First pageEnd: Last page
Data Display Components
Accordion
Collapsible content panels with single or multiple expansion.
Props:
| Prop | Type | Default | Description |
| ---------- | -------------------- | ------- | ------------------------------- |
| value | string \| string[] | [] | Bindable expanded item value(s) |
| multiple | boolean | false | Allow multiple panels open |
| disabled | boolean | false | Disabled state |
Usage:
<script>
import { Accordion, AccordionItem } from '@aspect-ops/exon-ui';
let expanded = $state([]);
</script>
<Accordion bind:value={expanded} multiple>
{#snippet children()}
<AccordionItem value="item1" title="Section 1">Content for section 1</AccordionItem>
<AccordionItem value="item2" title="Section 2">Content for section 2</AccordionItem>
{/snippet}
</Accordion>Slider
Range slider with drag, keyboard support, and optional value display.
Props:
| Prop | Type | Default | Description |
| ----------- | --------- | ------- | --------------------- |
| value | number | 0 | Bindable slider value |
| min | number | 0 | Minimum value |
| max | number | 100 | Maximum value |
| step | number | 1 | Value increment step |
| disabled | boolean | false | Disabled state |
| showValue | boolean | false | Show value tooltip |
| showTicks | boolean | false | Show tick marks |
Usage:
<script>
import { Slider } from '@aspect-ops/exon-ui';
let volume = $state(50);
</script>
<Slider bind:value={volume} min={0} max={100} showValue />Carousel
Image/content carousel with swipe gestures and autoplay.
Props:
| Prop | Type | Default | Description |
| ------------------ | --------- | ------- | --------------------------- |
| activeIndex | number | 0 | Bindable active slide index |
| autoplay | boolean | false | Enable autoplay |
| autoplayInterval | number | 5000 | Autoplay interval (ms) |
| loop | boolean | true | Loop slides |
| showIndicators | boolean | true | Show dot indicators |
| showArrows | boolean | true | Show navigation arrows |
Usage:
<script>
import { Carousel, CarouselSlide } from '@aspect-ops/exon-ui';
let activeSlide = $state(0);
</script>
<Carousel bind:activeIndex={activeSlide} autoplay loop>
{#snippet children()}
<CarouselSlide><img src="slide1.jpg" alt="Slide 1" /></CarouselSlide>
<CarouselSlide><img src="slide2.jpg" alt="Slide 2" /></CarouselSlide>
<CarouselSlide><img src="slide3.jpg" alt="Slide 3" /></CarouselSlide>
{/snippet}
</Carousel>Image
Lazy-loading image with placeholder and fallback support.
Props:
| Prop | Type | Default | Description |
| ------------- | ------------------- | --------- | ------------------------------- |
| src | string | - | Image source URL (required) |
| alt | string | - | Alt text (required) |
| loading | 'lazy' \| 'eager' | 'lazy' | Loading strategy |
| objectFit | string | 'cover' | CSS object-fit value |
| placeholder | string | - | Placeholder image or color |
| fallback | string | - | Fallback image on error |
| rounded | boolean \| string | false | Border radius: sm, md, lg, full |
| aspectRatio | string | - | CSS aspect ratio |
Usage:
<script>
import { Image } from '@aspect-ops/exon-ui';
</script>
<Image
src="profile.jpg"
alt="User profile"
rounded="full"
aspectRatio="1/1"
placeholder="#e5e7eb"
fallback="default-avatar.png"
/>Rating
Star rating display and input with half-star support.
Props:
| Prop | Type | Default | Description |
| ----------- | --------- | ------- | ----------------------- |
| value | number | 0 | Bindable rating value |
| max | number | 5 | Maximum rating |
| allowHalf | boolean | false | Allow half-star ratings |
| readonly | boolean | false | Read-only display mode |
| disabled | boolean | false | Disabled state |
| size | Size | 'md' | Star size: sm, md, lg |
| showValue | boolean | false | Show numeric value |
Usage:
<script>
import { Rating } from '@aspect-ops/exon-ui';
let rating = $state(4.5);
</script>
<Rating bind:value={rating} allowHalf showValue />Navigation & Feedback Components
Stepper
Multi-step progress indicator with linear or non-linear navigation.
Props:
| Prop | Type | Default | Description |
| ------------- | ---------------------------- | -------------- | ----------------------------- |
| activeStep | number | 0 | Bindable active step index |
| orientation | 'horizontal' \| 'vertical' | 'horizontal' | Layout orientation |
| linear | boolean | true | Enforce sequential navigation |
Usage:
<script>
import { Stepper, StepperStep } from '@aspect-ops/exon-ui';
let currentStep = $state(0);
</script>
<Stepper bind:activeStep={currentStep}>
{#snippet children()}
<StepperStep label="Personal Info" description="Enter your details" />
<StepperStep label="Address" description="Shipping information" />
<StepperStep label="Review" description="Confirm your order" />
{/snippet}
</Stepper>Chip
Compact tag/filter chips with selection and removal.
Props:
| Prop | Type | Default | Description |
| ----------- | ------------- | ----------- | ------------------------------------------------ |
| variant | ChipVariant | 'filled' | Chip style: filled, outlined, soft |
| color | ChipColor | 'default' | Color: default, primary, success, warning, error |
| size | ChipSize | 'md' | Size: sm, md, lg |
| removable | boolean | false | Show remove button |
| selected | boolean | false | Selected state |
| disabled | boolean | false | Disabled state |
Usage:
<script>
import { Chip, ChipGroup } from '@aspect-ops/exon-ui';
let selected = $state(['tag1']);
</script>
<ChipGroup>
<Chip onclick={() => console.log('clicked')}>JavaScript</Chip>
<Chip color="primary" removable onremove={() => console.log('removed')}>Svelte</Chip>
<Chip variant="outlined" color="success">TypeScript</Chip>
</ChipGroup>Mobile Components
Components optimized for Capacitor mobile apps with gesture support, haptic feedback, and safe area handling.
| Component | Description |
| --------------- | ------------------------------------------- |
| ActionSheet | iOS/Android-style bottom action menu |
| BottomSheet | Draggable bottom sheet with snap points |
| FAB | Floating action button with positions |
| FABGroup | Speed dial FAB with expandable actions |
| PullToRefresh | Pull-to-refresh gesture for lists |
| SwipeActions | Swipe-to-reveal actions on list items |
| SafeArea | Safe area inset wrapper for notched devices |
ActionSheet
<script>
import { ActionSheet, ActionSheetItem, Button } from '@aspect-ops/exon-ui';
let open = $state(false);
</script>
<Button onclick={() => (open = true)}>Show Actions</Button>
<ActionSheet bind:open title="Choose an action" description="Select what you want to do">
{#snippet actions()}
<ActionSheetItem onclick={() => console.log('Edit')}>Edit</ActionSheetItem>
<ActionSheetItem onclick={() => console.log('Share')}>Share</ActionSheetItem>
<ActionSheetItem destructive onclick={() => console.log('Delete')}>Delete</ActionSheetItem>
{/snippet}
</ActionSheet>Props:
open- Bindable open statetitle- Optional header titledescription- Optional header descriptioncancelLabel- Cancel button text (default: "Cancel")showCancel- Show cancel button (default: true)closeOnSelect- Close when action selected (default: true)
BottomSheet
<script>
import { BottomSheet, BottomSheetHeader, BottomSheetBody, Button } from '@aspect-ops/exon-ui';
let open = $state(false);
</script>
<Button onclick={() => (open = true)}>Open Sheet</Button>
<BottomSheet bind:open snapPoints={['half', 'full']} defaultSnapPoint="half">
<BottomSheetHeader>
<h3>Sheet Title</h3>
</BottomSheetHeader>
<BottomSheetBody>
<p>Drag the handle to resize or swipe down to close.</p>
</BottomSheetBody>
</BottomSheet>Props:
open- Bindable open statesnapPoints- Array of snap points:'min'|'half'|'full'| number (px)defaultSnapPoint- Initial snap point (default: 'half')showHandle- Show drag handle (default: true)closeOnBackdrop- Close on backdrop click (default: true)closeOnEscape- Close on Escape key (default: true)
FAB (Floating Action Button)
<script>
import { FAB } from '@aspect-ops/exon-ui';
</script>
<!-- Basic FAB -->
<FAB onclick={() => console.log('clicked')}>+</FAB>
<!-- Positioned FAB -->
<FAB position="bottom-left" size="lg">📝</FAB>
<!-- Extended FAB with label -->
<FAB extended position="bottom-right">
{#snippet children()}➕{/snippet}
{#snippet label()}Add Item{/snippet}
</FAB>Props:
size-'sm'|'md'|'lg'(44px, 56px, 72px)position-'bottom-right'|'bottom-left'|'bottom-center'|'top-right'|'top-left'extended- Extended FAB with labeldisabled- Disable the button
FABGroup (Speed Dial)
<script>
import { FABGroup } from '@aspect-ops/exon-ui';
const actions = [
{ icon: '📷', label: 'Camera', onAction: () => console.log('Camera') },
{ icon: '🖼️', label: 'Gallery', onAction: () => console.log('Gallery') },
{ icon: '📎', label: 'Attach', onAction: () => console.log('Attach') }
];
</script>
<FABGroup {actions} icon="+" closeIcon="×" position="bottom-right" direction="up" />Props:
actions- Array of{ icon, label, onAction }objectsicon- Main FAB icon (default: '+')closeIcon- Icon when open (default: '×')position- Same as FAB positionsdirection-'up'|'down'|'left'|'right'
PullToRefresh
<script>
import { PullToRefresh } from '@aspect-ops/exon-ui';
let refreshing = $state(false);
async function handleRefresh() {
// Fetch new data
await new Promise((r) => setTimeout(r, 2000));
refreshing = false;
}
</script>
<PullToRefresh bind:refreshing onrefresh={handleRefresh}>
<div class="content">
<p>Pull down to refresh...</p>
<!-- Your scrollable content -->
</div>
</PullToRefresh>Props:
refreshing- Bindable loading statethreshold- Pull distance to trigger (default: 80px)maxPull- Maximum pull distance (default: 150px)disabled- Disable pull-to-refreshonrefresh- Callback when threshold reached
SwipeActions
<script>
import { SwipeActions } from '@aspect-ops/exon-ui';
const leftActions = [
{ icon: '📌', label: 'Pin', color: '#3b82f6', onAction: () => console.log('Pin') }
];
const rightActions = [
{ icon: '🗑️', label: 'Delete', color: '#ef4444', onAction: () => console.log('Delete') }
];
</script>
<SwipeActions {leftActions} {rightActions}>
<div class="list-item">
<p>Swipe me left or right</p>
</div>
</SwipeActions>Props:
leftActions- Actions revealed on swipe rightrightActions- Actions revealed on swipe leftthreshold- Swipe distance to reveal (default: 60px)disabled- Disable swipe gestures
Action object:
icon- Icon string or emojilabel- Action label textcolor- Background coloronAction- Callback when tapped
SafeArea
<script>
import { SafeArea } from '@aspect-ops/exon-ui';
</script>
<!-- All edges (default) -->
<SafeArea>
<main>Content with safe area padding on all sides</main>
</SafeArea>
<!-- Specific edges -->
<SafeArea edges={['top', 'bottom']}>
<main>Only top and bottom safe area</main>
</SafeArea>Props:
edges- Array of edges:'top'|'right'|'bottom'|'left'(default: all)
Website Components
Chatbot
AI-ready chat widget with session management, typing indicators, and lead capture.
<script>
import { Chatbot } from '@aspect-ops/exon-ui';
async function handleMessage(message, sessionId) {
// Call your AI backend (OpenAI, Claude, etc.)
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message, sessionId })
});
return (await response.json()).reply;
}
async function handleLeadCapture(data, sessionId) {
// Send to your CRM
await fetch('/api/leads', {
method: 'POST',
body: JSON.stringify({ ...data, sessionId })
});
}
</script>
<Chatbot
title="Support Chat"
subtitle="We typically reply within minutes"
welcomeMessage="Hello! How can I help you today?"
onSendMessage={handleMessage}
onLeadCapture={handleLeadCapture}
position="bottom-right"
/>Props:
| Prop | Type | Default | Description |
| ------------------- | ---------- | ---------------- | ----------------------------------------- |
| title | string | 'Chat Support' | Chat header title |
| subtitle | string | - | Chat header subtitle |
| welcomeMessage | string | - | Initial welcome message |
| placeholder | string | 'Type...' | Input placeholder text |
| defaultOpen | boolean | false | Open chat on mount |
| position | string | 'bottom-right' | Position: bottom-right, bottom-left |
| inactivityTimeout | number | 40000 | Auto-open after inactivity (ms), 0=off |
| onSendMessage | function | - | Callback: (msg, sessionId) => reply |
| onLeadCapture | function | - | Callback: (data, sessionId) => void |
| onEscalate | function | - | Callback: (sessionId, messages) => void |
ContactForm
Customizable contact form with real-time validation and UTM tracking.
<script>
import { ContactForm } from '@aspect-ops/exon-ui';
async function handleSubmit(data) {
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data)
});
return { success: response.ok };
}
</script>
<ContactForm
submitText="Send Message"
successMessage="Thank you! We'll be in touch soon."
onSubmit={handleSubmit}
/>Props:
| Prop | Type | Default | Description |
| ------------------ | --------------- | -------------- | ------------------------------------- |
| fields | FieldConfig[] | Default fields | Array of field configurations |
| initialValues | object | {} | Initial form values |
| submitText | string | 'Submit' | Submit button text |
| successMessage | string | 'Thank you!' | Success message after submit |
| privacyNotice | string | - | Privacy policy text |
| extractUtmParams | boolean | true | Extract UTM params from URL |
| onSubmit | function | - | Callback: (data) => { success } |
| onValidate | function | - | Callback: (isValid, errors) => void |
Default fields: firstName, lastName, email, phone, company, message
ViewCounter
Track and display page views with session-based deduplication.
<script>
import { ViewCounter } from '@aspect-ops/exon-ui';
async function getCount(slug) {
const res = await fetch(`/api/views/${slug}`);
return (await res.json()).count;
}
async function trackView(slug) {
const res = await fetch(`/api/views/${slug}`, { method: 'POST' });
return (await res.json()).count;
}
</script>
<ViewCounter slug="blog-post-123" onGetCount={getCount} onTrackView={trackView} />Props:
| Prop | Type | Default | Description |
| ------------- | ---------- | ------- | ---------------------------- |
| slug | string | - | Unique content identifier |
| showLabel | boolean | true | Show "views" label |
| trackView | boolean | true | Track view (or just display) |
| onGetCount | function | - | Callback: (slug) => count |
| onTrackView | function | - | Callback: (slug) => count |
DoughnutChart
Pure SVG doughnut chart with interactive legend (no external dependencies).
<script>
import { DoughnutChart } from '@aspect-ops/exon-ui';
const data = [
{ label: 'Direct', value: 45000 },
{ label: 'Referral', value: 28000 },
{ label: 'Organic', value: 32000 },
{ label: 'Social', value: 15000 }
];
</script>
<DoughnutChart title="Traffic Sources" {data} size={200} showLegend showTotal />Props:
| Prop | Type | Default | Description |
| ------------ | ---------- | --------------- | ------------------------- |
| title | string | - | Chart title |
| data | array | [] | [{ label, value }, ...] |
| size | number | 200 | Chart size in pixels |
| thickness | number | 0.6 | Doughnut thickness (0-1) |
| colors | string[] | Default palette | Custom color array |
| showLegend | boolean | true | Show legend |
| showTotal | boolean | true | Show center total |
Theming & Customization
Quick Setup
<script>
// In your root layout (+layout.svelte)
import '@aspect-ops/exon-ui/styles';
</script>Custom Brand Colors
Override CSS variables in your app's global styles:
/* src/app.css or global styles */
:root {
/* Primary brand color */
--color-primary: #8b5cf6; /* Purple */
--color-primary-hover: #7c3aed;
--color-primary-active: #6d28d9;
--color-primary-bg: #ede9fe;
/* Secondary */
--color-secondary: #64748b;
--color-secondary-hover: #475569;
/* Destructive/Error */
--color-destructive: #dc2626;
--color-error: #dc2626;
/* Success */
--color-success: #16a34a;
/* Warning */
--color-warning: #d97706;
}Full Design Token Reference
:root {
/* ===== COLORS ===== */
/* Backgrounds */
--color-bg: #fcfcfc; /* Main background */
--color-bg-muted: #f9f9f9; /* Subtle background */
--color-bg-subtle: #f0f0f0; /* More contrast */
--color-bg-hover: #e8e8e8; /* Hover state */
--color-bg-active: #e0e0e0; /* Active/pressed */
--color-bg-elevated: #ffffff; /* Cards, modals */
--color-bg-card: #ffffff; /* Card backgrounds */
/* Text */
--color-text: #202020; /* Primary text */
--color-text-muted: #646464; /* Secondary text */
--color-text-subtle: #838383; /* Tertiary text */
--color-text-inverse: #ffffff; /* Text on dark backgrounds */
/* Borders */
--color-border: #d9d9d9;
--color-border-hover: #cecece;
--color-border-active: #bbbbbb;
/* ===== TYPOGRAPHY ===== */
--font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--font-family-mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
/* Fluid type scale (responsive) */
--text-xs: clamp(0.69rem, 0.66rem + 0.14vw, 0.78rem); /* ~11-12px */
--text-sm: clamp(0.83rem, 0.79rem + 0.21vw, 0.97rem); /* ~13-16px */
--text-base: clamp(1rem, 0.93rem + 0.33vw, 1.125rem); /* 16-18px */
--text-lg: clamp(1.13rem, 1.03rem + 0.47vw, 1.41rem); /* ~18-23px */
--text-xl: clamp(1.27rem, 1.14rem + 0.65vw, 1.76rem); /* ~20-28px */
--text-2xl: clamp(1.42rem, 1.25rem + 0.89vw, 2.2rem); /* ~23-35px */
--text-3xl: clamp(1.6rem, 1.37rem + 1.19vw, 2.75rem); /* ~26-44px */
--text-4xl: clamp(1.8rem, 1.49rem + 1.57vw, 3.43rem); /* ~29-55px */
/* Font weights */
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
/* ===== SPACING ===== */
--space-xs: clamp(0.25rem, 0.23rem + 0.11vw, 0.31rem); /* 4-5px */
--space-sm: clamp(0.5rem, 0.46rem + 0.22vw, 0.63rem); /* 8-10px */
--space-md: clamp(1rem, 0.93rem + 0.33vw, 1.125rem); /* 16-18px */
--space-lg: clamp(1.5rem, 1.39rem + 0.54vw, 1.75rem); /* 24-28px */
--space-xl: clamp(2rem, 1.85rem + 0.76vw, 2.5rem); /* 32-40px */
--space-2xl: clamp(3rem, 2.78rem + 1.09vw, 3.75rem); /* 48-60px */
/* ===== BORDER RADIUS ===== */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-xl: 0.75rem; /* 12px */
--radius-2xl: 1rem; /* 16px */
--radius-full: 9999px; /* Pill/circle */
/* ===== SHADOWS ===== */
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
/* ===== TRANSITIONS ===== */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
/* ===== MOBILE ===== */
--touch-target-min: 44px;
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
}Dark Mode
The library supports dark mode via data-theme attribute or system preference:
<script>
let isDark = $state(false);
function toggleTheme() {
isDark = !isDark;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
// Respect system preference on load
import { onMount } from 'svelte';
onMount(() => {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDark = true;
document.documentElement.setAttribute('data-theme', 'dark');
}
});
</script>
<Switch bind:checked={isDark} onchange={toggleTheme}>
{#snippet children()}Dark Mode{/snippet}
</Switch>Dark theme automatically adjusts all colors:
[data-theme='dark'] {
--color-bg: #111111;
--color-bg-muted: #191919;
--color-bg-elevated: #222222;
--color-text: #eeeeee;
--color-text-muted: #b4b4b4;
--color-border: #3a3a3a;
/* All other colors auto-adapt */
}Component-Level Customization
Override styles for specific components using CSS classes:
<Button class="my-custom-button" variant="primary">Custom Button</Button>
<style>
:global(.my-custom-button) {
--color-primary: #10b981;
border-radius: var(--radius-full);
text-transform: uppercase;
}
</style>Platform-Specific Styling
Components adapt to iOS/Android automatically. Set platform manually:
<script>
import { onMount } from 'svelte';
onMount(() => {
// Auto-detect or set manually
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
document.documentElement.setAttribute('data-platform', isIOS ? 'ios' : 'android');
});
</script>Mobile / Capacitor Support
Components are designed with mobile-first principles:
- 44px minimum touch targets for buttons and interactive elements
- Safe area inset handling for notched devices (iPhone, Android gesture bar)
- Responsive breakpoints for adaptive layouts
- Hardware-accelerated animations for smooth 60fps performance
- Haptic feedback integration (requires Capacitor Haptics plugin)
- Platform-adaptive styling for iOS and Android
Capacitor Integration
# Install Capacitor plugins for full mobile support
npm install @capacitor/haptics @capacitor/status-bar<script>
import { SafeArea, BottomNav, PullToRefresh } from '@aspect-ops/exon-ui';
</script>
<SafeArea edges={['top', 'bottom']}>
<PullToRefresh onrefresh={handleRefresh}>
<main>Your content here</main>
</PullToRefresh>
<BottomNav {items} />
</SafeArea>TypeScript
All components include TypeScript definitions:
import type {
ButtonProps,
ButtonVariant,
TypographyProps,
TabsProps,
MenuItemProps
} from '@aspect-ops/exon-ui';Browser Support
- Chrome/Edge 88+
- Firefox 78+
- Safari 14+
- iOS Safari 14+
- Chrome for Android 88+
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
License
MIT License - see LICENSE for details.
Built with Svelte 5 and Bits UI
