npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aspect-ops/exon-ui

v0.4.0

Published

Reusable Svelte UI components for web and Capacitor mobile apps

Readme

@aspect-ops/exon-ui

A modern, accessible UI component library for Svelte 5 with first-class support for Capacitor mobile apps.

npm version License: MIT

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-ui

Quick 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 page
  • Home: First page
  • End: 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 state
  • title - Optional header title
  • description - Optional header description
  • cancelLabel - 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 state
  • snapPoints - 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 label
  • disabled - 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 } objects
  • icon - Main FAB icon (default: '+')
  • closeIcon - Icon when open (default: '×')
  • position - Same as FAB positions
  • direction - '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 state
  • threshold - Pull distance to trigger (default: 80px)
  • maxPull - Maximum pull distance (default: 150px)
  • disabled - Disable pull-to-refresh
  • onrefresh - 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 right
  • rightActions - Actions revealed on swipe left
  • threshold - Swipe distance to reveal (default: 60px)
  • disabled - Disable swipe gestures

Action object:

  • icon - Icon string or emoji
  • label - Action label text
  • color - Background color
  • onAction - 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