@ulam/ube
v0.4.0
Published
Framework-agnostic UI components (vanilla, React, Vue, Angular, Remix), theming, and design tokens.
Maintainers
Readme
@ulam/ube
Framework-agnostic UI components built on vanilla web components. Adapters for React, Vue, Angular, and Remix. The sweet layer of the ulam framework.
Accessible out of the box. Purple-first design. Zero external dependencies beyond your chosen framework.
Purpose & Scope
What ube does:
- Vanilla web components that work in any JavaScript environment
- Framework adapters for React, Vue, Angular, and Remix (same component API)
- Design tokens for colors, spacing, typography, animations
- CSS theming system (light, dark, fiesta)
- Component CSS automatically imported on use (tree-shakeable)
- Focus styling and user preference support (reduced motion, high contrast)
- Light DOM pattern (no shadow DOM encapsulation) so tokens cascade from your app
What ube doesn't do:
- Focus management or overlays (use @ulam/sili for Dialog/Sheet/Drawer)
- State management (you manage component state)
- i18n or translations (use @ulam/calamansi)
- Routing or navigation (use @ulam/sili for routing)
- Announcements or live regions (use @ulam/taho)
Who should use ube:
- React, Vue, Angular, or Remix apps needing accessible, themable UI components
- Projects building custom component libraries on top of solid foundations
- Teams prioritizing accessibility without sacrificing design control
- SPAs wanting tree-shakeable, zero-dependency component libraries
- Multi-framework monorepos that want to reuse the same component library
The ulam Framework
Ube is one of six independent packages in the ulam framework. See docs/ARCHITECTURE.md for the complete framework structure and dependency graph.
Use ube independently, or with the full ulam stack. No cross-package dependencies: ube does not import from taho, sili, calamansi, or sawsawan.
Install
npm install @ulam/ubeNo peer dependencies for vanilla web components. Optional framework peers (React 18+, Vue 3, Angular 17+) only when using framework adapters.
Optional: import aliases
The package names are Filipino food terms: meaningful to the framework but unfamiliar to most developers. If you prefer shorter or more descriptive import names, npm's built-in alias syntax lets you rename packages at install time without any changes to ube itself.
Install with aliases:
npm install ube@npm:@ulam/ube
npm install calamansi@npm:@ulam/calamansi
npm install sawsawan@npm:@ulam/sawsawanYour package.json will show:
{
"dependencies": {
"ube": "npm:@ulam/ube",
"calamansi": "npm:@ulam/calamansi",
"sawsawan": "npm:@ulam/sawsawan"
}
}Then import using whichever name you installed under:
import { Button, FormControlToggle } from 'ube'
import { useT, usePref } from 'calamansi'Or use any name you like: the alias is yours to choose:
npm install ui@npm:@ulam/ube
npm install i18n@npm:@ulam/calamansiThe canonical package names (@ulam/ube etc.) are always the stable reference. Aliases are a local convenience and do not affect the published package.
Quick start
Import stylesheets at app root
import '@ulam/ube/base-tokens.css' // Design token primitives
import '@ulam/ube/base-typography.css' // Structural typography baseline
import '@ulam/ube/ui.css' // Reset, utility styles, print, user preferencesThese foundational imports must come first. Component-specific CSS is automatically imported by each component.
For framework-specific adapters, use the subpath exports:
// React
import { Button } from '@ulam/ube/react'
// Vue
import { Button } from '@ulam/ube/vue'
// Angular
import { UbeModule } from '@ulam/ube/angular'
// Remix (same as React)
import { Button } from '@ulam/ube/remix'Set up your app
import { Button, FormControlToggle, FormInputText } from '@ulam/ube/react'
import { Announcer } from '@ulam/taho/react'
import { Router } from '@ulam/sili/react'
export default function App() {
return (
<Router>
<Announcer />
<AppShell />
</Router>
)
}All components are tree-shakeable: only imported components bundle their CSS.
Where ube stops (sili starts)
Ube provides reusable UI components and tokens. Sili provides focus management and overlay orchestration. Here's the boundary:
| Aspect | Ube (UI layer) | Sili (Focus/Overlay layer) | | ------ | -------------- | -------------------------- | | Button, Input, Toggle, etc. | ✓ Components | — | | Panel (side navigation container) | ✓ Structure + styling | — | | Dialog, Sheet, Drawer (overlays) | — | ✓ Full implementations | | Focus trap, ARIA hide, escape | — | ✓ Handled automatically | | Overlay transitions & stacking | — | ✓ OverlayManager orchestrates | | Page heading focus management | — | ✓ Router focus plugins | | Component content/behavior | ✓ Your app's responsibility | — |
Panel vs Dialog/Sheet/Drawer: Panel is a structural container for organizing content (settings, details, etc.). Dialog/Sheet/Drawer are full overlay components with built-in focus management. If you need a side container without sili's focus handling, use Panel. If you need an overlay with automatic focus management, use sili's overlays.
Components
Button
Unified button component for text, text with icon, or icon-only layouts. Automatically detects icon-only mode based on whether children are provided.
import { Button } from '@ulam/ube/react'
// Text button
<Button variant="primary" onClick={handleClick}>Save changes</Button>
// Text with icon (icon on left by default)
<Button variant="primary" icon={<Star size={16} />}>Save</Button>
// Icon on right
<Button icon={<ChevronRight size={16} />} iconPosition="end">Next</Button>
// Icon-only button (no children)
<Button icon={<X size={20} />} label="Close" variant="accent" />
// With state transitions
<Button icon={<Star size={16} />} active={saved} activeLabel="Saved" activeIcon={<StarFilled size={16} />}>
Save
</Button>
// Full-width variants and sizing
<Button variant="secondary" fullWidth>Delete</Button>
<Button size="large" align="left">Options</Button>
<Button size="compact">Small</Button>| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| variant | 'primary' \| 'secondary' \| 'tertiary' \| 'accent' | 'primary' | Visual style |
| size | 'compact' \| 'default' \| 'large' | 'default' | Button size |
| onClick | function | - | Click handler |
| disabled | boolean | false | Disabled state (aria-disabled, stays in tab order) |
| busy | boolean | false | Loading/processing state |
| fullWidth | boolean | false | Stretches to container width |
| error | boolean | false | Error/danger state |
| icon | ReactNode | - | Icon element |
| iconPosition | 'start' \| 'end' | 'start' | Icon placement relative to text |
| active | boolean | false | Active/toggled state |
| activeIcon | ReactNode | - | Icon shown when active |
| label | string | - | aria-label (used in icon-only mode) |
| activeLabel | string | - | aria-label when active |
| title | string | - | Tooltip text |
| className | string | - | Additional CSS classes |
Icon-only detection: When no children are provided and an icon is present, Button automatically applies icon-only styling. The label prop is required in this case for accessibility.
LinkBtnStyled
Anchor element styled with button classes, for external links or hash navigation.
import { LinkBtnStyled } from '@ulam/ube'
<LinkBtnStyled href="https://example.com" className="btn--primary">Visit site</LinkBtnStyled>
<LinkBtnStyled href="#/settings" className="btn--secondary">Settings</LinkBtnStyled>| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| href | string | - | Link destination (required) |
| className | string | - | CSS classes for button styling |
| children | ReactNode | - | Link label |
FormControlToggle
Binary on/off switch. Always pair with a visible label.
import { FormControlToggle } from '@ulam/ube'
<label htmlFor="live-search">
<FormControlToggle id="live-search" checked={liveSearch} onChange={setLiveSearch} />
Live search
</label>| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| id | string | - | Input id, must match label's htmlFor |
| checked | boolean | - | Controlled value (required) |
| onChange | function | - | (checked: boolean) => void |
| disabled | boolean | false | Disabled state |
FormControlRadioChip
Radio input styled as a selectable chip. Group under a shared name.
import { FormControlRadioChip } from '@ulam/ube'
{['A', 'AA', 'AAA'].map(level => (
<FormControlRadioChip
key={level}
name="wcag-level"
value={level.toLowerCase()}
label={level}
current={selectedLevel}
onChange={setSelectedLevel}
/>
))}| Prop | Type | Description |
| ---- | ---- | ----------- |
| name | string | Radio group name (required) |
| value | string | This chip's value (required) |
| label | string | Visible label (required) |
| current | string | Currently selected value |
| onChange | function | (value: string) => void |
FormControlRadio
Plain accessible radio input with label for use in control layouts.
import { FormControlRadio } from '@ulam/ube'
<FormControlRadio name="theme" value="dark" label="Dark" checked={theme === 'dark'} onChange={() => setTheme('dark')} />FormControlCheckbox
Plain accessible checkbox input with label for use in control layouts.
import { FormControlCheckbox } from '@ulam/ube'
<FormControlCheckbox label="Accept terms" checked={accepted} onChange={setAccepted} />FormControlSelect
Native-enhanced dropdown with keyboard support and token-driven sizing.
import { FormControlSelect } from '@ulam/ube'
<FormControlSelect id="framework" value={value} onChange={e => setValue(e.target.value)}>
<option value="react">React</option>
<option value="vue">Vue</option>
</FormControlSelect>| Prop | Type | Description |
| ---- | ---- | ----------- |
| id | string | Input id |
| value | string | Controlled value |
| onChange | function | Change handler |
| disabled | boolean | Disabled state |
FormInputText
Unified text input supporting three modes: plain, search, and clearable. Automatically applies appropriate wrapper and styling based on mode.
import { FormInputText } from '@ulam/ube/react'
// Plain text input
<FormInputText
id="username"
type="text"
value={username}
onChange={setUsername}
placeholder="Username"
/>
// Search mode: form[role="search"] wrapper, live or submit mode
<FormInputText
id="site-search"
value={query}
onChange={setQuery}
search
liveSearch
placeholder="Search the site"
clearAriaLabel="Clear search"
/>
// Search with submit button
<FormInputText
id="filter-search"
value={query}
onChange={setQuery}
search
showSubmit
onSubmit={handleSearch}
submitAriaLabel="Search"
/>
// Clearable mode: shows clear button when input has value
<FormInputText
id="filter"
value={filter}
onChange={setFilter}
clearable
placeholder="Filter items"
clearAriaLabel="Clear filter"
clearIcon={<X size={16} />}
/>Pair with usePref from @ulam/calamansi to persist the live/submit preference:
import { usePref } from '@ulam/calamansi'
const [liveSearch, setLiveSearch] = usePref('liveSearch', true)| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| id | string | - | Input id |
| type | string | 'text' | Input type |
| value | string | - | Controlled value (required) |
| onChange | function | - | (value: string) => void (required) |
| placeholder | string | - | Input placeholder |
| disabled | boolean | false | Disabled state (aria-disabled) |
| label | string | - | aria-label override |
| width | string | - | Custom width |
| height | string | - | Custom height |
| Search mode | | | |
| search | boolean | false | Enable search mode (form[role="search"] wrapper) |
| liveSearch | boolean | false | Fire onChange on every keystroke, hide submit button |
| showSubmit | boolean | true | Show submit icon button (when not liveSearch) |
| onSubmit | function | - | Called on Enter or submit button click |
| submitAriaLabel | string | 'Search' | aria-label for submit button |
| Clearable mode | | | |
| clearable | boolean | false | Show clear button when value is not empty |
| onClear | function | - | Called when clear button clicked |
| clearAriaLabel | string | 'Clear' | aria-label for clear button |
| clearIcon | ReactNode | - | Custom clear button icon |
| Styling | | | |
| wrapClassName | string | - | CSS classes for wrapper div |
| inputClassName | string | - | CSS classes for input element |
| clearButtonClassName | string | - | CSS classes for clear button |
| inputRef | ref | - | Forward ref to input element |
| clearAriaLabel | string | - | aria-label for clear button (required) |
| clearIcon | ReactNode | '↺' | Clear button icon |
| wrapClassName | string | '' | Class on the wrapper div |
| inputClassName | string | '' | Class on the input |
| clearButtonClassName | string | '' | Class on the clear button |
| inputRef | ref | - | Forward ref to the input element |
Badge
Semantic badge for severity levels, status, and counts. Supports click for interactive use.
import { Badge } from '@ulam/ube'
<Badge variant="critical">Critical</Badge>
<Badge variant="high" prefix="SC">2.4.3</Badge>
<Badge variant="success" onClick={handleClick}>Clickable</Badge>| Prop | Type | Description |
| ---- | ---- | ----------- |
| variant | 'critical' \| 'high' \| 'medium' \| 'best-practice' \| 'info' \| 'success' \| 'warning' \| 'neutral' | Color variant |
| prefix | string | Small prefix label inside the badge |
| onClick | function | Makes the badge a button |
| bg | string | Custom background color |
| color | string | Custom text color |
InfoBox
Informational callout for tips, warnings, or supplemental content.
import { InfoBox } from '@ulam/ube'
<InfoBox>This setting affects all platforms.</InfoBox>Panel
Detail panel with useFocusOnMount and usePageTitle built in. Use for routed detail panels.
import { Panel } from '@ulam/ube'
<Panel heading="Finding Detail" onClose={onClose} closeAriaLabel="Close panel">
{children}
</Panel>| Prop | Type | Description |
| ---- | ---- | ----------- |
| heading | string | Panel title |
| onClose | function | Close handler |
| closeAriaLabel | string | aria-label for close button |
| children | ReactNode | Panel body content |
ButtonBack
RTL-aware back chevron button. Flips direction when html[dir="rtl"].
import { ButtonBack } from '@ulam/ube'
<ButtonBack onClick={onBack} label="Back to results" />| Prop | Type | Description |
| ---- | ---- | ----------- |
| onClick | function | Click handler |
| label | string | aria-label |
Screen
Generic screen state component for displaying page-level information, errors, or empty states.
import { Screen } from '@ulam/ube'
// No results
<Screen
variant="no-results"
heading="No results found"
body="Try adjusting your search terms."
activeFilters={filters}
onOpenSettings={handleSettings}
/>
// Error state
<Screen
variant="error"
heading="Unable to load data"
body="An error occurred while loading."
action={retryData}
actionLabel="Try again"
/>| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| variant | 'no-results' \| 'error' | 'no-results' | Screen type |
| heading | string | Varies by variant | Main heading |
| body | string | Varies by variant | Description text |
| icon | ReactNode | Varies by variant | Custom icon |
| action | function | - | Action button click handler |
| actionLabel | string | Varies by variant | Action button label |
| actionIcon | ReactNode | - | Custom action button icon |
| activeFilters | string[] | [] | Applied filter tags to display |
| onOpenSettings | function | - | Settings link click handler |
| onMount | function | - | Called when component mounts |
Plugins
Ube works with two companion packages for live region announcements and routing. Both are part of the ulam framework and listed as optional peer dependencies.
Announce
ARIA live region system from @ulam/taho. Call announce() from anywhere: no prop drilling, no context wiring.
import { Announcer, announce, useAnnounce } from '@ulam/taho/react'
// Mount once at app root
<Announcer />
// Direct call from any module
announce('Settings: Saved')
announce('Error: Invalid key', { priority: 'assertive' })
// Hook style
const announce = useAnnounce()
announce('Copy: Copied to clipboard')Message format: prefix with context: "Settings: Saved" not "Saved". Bare messages are ambiguous to screen reader users who may miss where the message came from.
Priority:
'polite'(default): waits for a natural pause. Use for confirmations, results, background changes.'assertive': interrupts immediately. Use only for errors and urgent alerts.
Do not announce: focus-managed transitions (modals opening, page navigation). Screen readers announce focus targets automatically.
Implementation: two always-in-DOM live regions with auto-clearing after ~1 second. Duplicate messages re-announce reliably via clear-then-set cycle.
Router
Hash-based routing with built-in focus management, RTL support, Escape handling, and page title updates. Comes from @ulam/sili/react.
import { Dialog, Drawer, Sheet } from '@ulam/sili/react'
import {
useFocusOnMount, useReturnFocus, useFocusTrap,
usePaginationFocus, useAriaHide, useDir,
useMediaQuery, usePageTitle, useEscapeKey,
} from '@ulam/sili/react'
// In components
const dir = useDir() // 'ltr' | 'rtl', reactive to html[dir]
const headingRef = useFocusOnMount() // focus this element on mount
usePageTitle('Page Name') // sets document.title = "AppName | Page Name"
<Dialog open={isOpen} onClose={onClose} heading="Title">
Content here
</Dialog>Overlay components:
Dialog: centered modal dialog, focus trap, Escape-to-dismiss, stacks at z-index 301Drawer: slide-in panel from left, focus management built inSheet: slide-up sheet from bottom, focus management, desktop collapse
Focus management hooks:
| Hook | Description |
| ---- | ----------- |
| useFocusOnMount(ref?) | Move focus to element on mount (page headings, modal open) |
| useReturnFocus() | Restore focus to trigger element on unmount |
| useFocusTrap(containerRef, active) | Restrict Tab to container while active |
| usePaginationFocus(headingRef, pageIndex) | Re-focus heading on page change within a modal or sheet |
| useAriaHide(panelRef, active) | Set inert on background content while overlay is open |
| useEscapeKey(handler, active) | Call handler when Escape is pressed |
Layout hooks:
| Hook | Description |
| ---- | ----------- |
| useDir() | Reactive document.documentElement.dir: 'ltr' or 'rtl' |
| useMediaQuery(query) | Reactive window.matchMedia |
| usePageTitle(title) | Sets document.title to "AppName \| title" |
Focus rules (WCAG 2.4.3):
- New page: focus the main heading (
tabIndex={-1}) - Dialog/overlay open: focus first focusable element (usually close button)
- Dialog/overlay close: restore focus to trigger (
useReturnFocus) - Overlay open: set background inert (
useAriaHide, overlays do this automatically) - Escape: each overlay layer handles its own
- Paginated content: use
usePaginationFocuson page change - Accordion: leave focus on trigger, do not use
useFocusOnMounton content
Theme
Dark, light, auto, and fiesta modes as first-class features.
import { useThemeManager } from '@ulam/ube'
useThemeManager(theme, onFiestaActivated)Sets data-theme on <html> and handles fiesta mode color cycling. All ube components respond to [data-theme="dark"] automatically via CSS tokens.
CSS Usage
Foundational stylesheets
Ube's CSS is split into foundational and component-specific layers for better tree-shaking:
Foundational imports (required, load once):
@import '@ulam/ube/base-tokens.css'; /* Primitives: colors, spacing, typography, motion, sizing */
@import '@ulam/ube/base-typography.css'; /* Body text, headings, links, selection, monospace baseline */
@import '@ulam/ube/base-reset.css'; /* Normalize + ube defaults (imported by ui.css) */
@import '@ulam/ube/base-utils.css'; /* Focus rings, aria-disabled state, visually-hidden (imported by ui.css) */
@import '@ulam/ube/ui.css'; /* Shorthand: loads base-reset, base-utils, base-user-prefs, base-print */Or simply:
import '@ulam/ube/base-tokens.css'
import '@ulam/ube/base-typography.css'
import '@ulam/ube/ui.css'Component CSS
Each component imports its own CSS automatically. No additional imports needed:
import { Button } from '@ulam/ube' // buttons.css imported automatically
import { FormInputText } from '@ulam/ube' // form-input-text.css imported automaticallyUnused components are completely removed from production bundles, including their CSS.
Theming
Override any CSS custom property to retheme. All components respond to token changes immediately:
:root {
--bg: #fafaf9;
--text-heading: #1c1917;
--accent: #7c3aed;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1c1917;
--text-heading: #fafaf9;
--accent: #a78bfa;
}
}Dark mode is built-in. Use [data-theme="dark"] to explicitly set theme without relying on system preference.
Print styles
Modal, drawer, and sheet overlays are hidden on print. Content flows naturally. No additional configuration needed—included in base-print.css (via ui.css).
Reduced motion
All animations and transitions respect prefers-reduced-motion: reduce. No opt-in needed.
Design tokens
All components consume CSS custom properties. Override any token to retheme without touching component code.
Tokens are organized into two layers:
- Foundational tokens (
base-tokens.css): Global design primitives (colors, typography families, spacing scale, sizing, motion, focus) - Component tokens: Component-specific tokens defined at the top of each component's CSS file (e.g., badge colors, button icons, form control sizing)
This organization enables better tree-shaking: component tokens are removed if the component is unused.
Foundational color tokens
| Token | Description |
| ----- | ----------- |
| --bg | Page background |
| --bg-subtle | Slightly off-background (cards, panels) |
| --text-heading | Primary text (headings, labels) |
| --text-body | Body text (default paragraph text) |
| --text-muted | Secondary / placeholder text |
| --text-disabled | Disabled text (intentionally low contrast per WCAG 1.4.3) |
| --border | Default border |
| --border-control | Form control border (≥4.6:1 contrast for enabled controls) |
| --accent | Primary accent (purple) |
| --accent-bg | Accent background |
| --accent-text | Text on accent background |
| --focus | Focus outline color (≥3:1 against all surfaces) |
| --success | Success state |
| --error | Error / warning state |
| --overlay-bg | Modal/overlay backdrop |
Sizing tokens:
| Token | Value | Description |
| ----- | ----- | ----------- |
| --touch-target | 44px | Minimum touch target (WCAG 2.5.5) |
| --input-height | 4rem | Standard input/select height |
| --btn-height-standard | 44px | Standard button height |
| --btn-height-compact | 44px | Compact button height |
Spacing scale (--space-N): 1=0.25rem, 2=0.5rem, 3=0.75rem, 4=1rem, 5=1.25rem, 6=1.5rem, 8=2rem, 10=2.5rem
Typography:
| Token | Size | Description |
| ----- | ---- | ----------- |
| --font | — | Body font stack (Inter) |
| --font-display | — | Display font stack (Outfit) |
| --mono | — | Monospace font stack |
| --fs-small | 0.875rem (14px) | Metadata, labels, fine print (minimum) |
| --fs-body | 1rem (16px) | Body text, inputs, button text |
| --fs-sub | 1.25rem (20px) | Card headings, slightly emphasized text |
| --fs-heading | 1.5rem (24px) | Section headings (h3 inside panels) |
| --fs-h2 | clamp(1.5rem, 8vw, 2.25rem) | Panel titles (h2), scales up on desktop |
| --fs-h1 | clamp(1.75rem, 10.5vw, 2.667rem) | Page title, scales up on desktop |
Motion:
| Token | Value | Description |
| ----- | ----- | ----------- |
| --duration-fast | 150ms | Micro-interactions (color, border) |
| --duration-base | 250ms | Standard transitions (panel, element) |
| --ease-out | cubic-bezier(...) | Spring-y easing (slides entering) |
| --ease-in-out | cubic-bezier(...) | Balanced easing (state toggles) |
Focus:
| Token | Value | Description |
| ----- | ----- | ----------- |
| --focus-outline-width | 2px | Keyboard focus outline width |
| --focus-outline-offset | 2px | Keyboard focus outline offset |
Component-specific tokens are defined at the top of their respective CSS files (e.g., badge.css, buttons.css, form-control-toggle.css) for scope and tree-shaking.
Subpath exports
Components and utilities
| Import | Contents |
| ------ | -------- |
| @ulam/ube | Button, ButtonBack, LinkBtnStyled, LinkSkipTo, FormInputText, FormControlRadio, FormControlCheckbox, FormControlToggle, FormControlSelect, FormControlRadioGroup, FormControlRadioChip, FormControlRadioChipGroup, Badge, InfoBox, Panel, PanelFormControls, Screen, FadeTransition, IconExternalLink, useThemeManager |
Announce comes from @ulam/taho/react. Routing and overlays come from @ulam/sili/react.
See the root README for a complete framework support overview across all ulam packages.
Stylesheets
| Import | Purpose |
| ------ | ------- |
| @ulam/ube/base-tokens.css | Global design token primitives (colors, spacing, typography families, sizing, motion) |
| @ulam/ube/base-typography.css | Body text, headings, links, selection, monospace styling |
| @ulam/ube/base-reset.css | Normalize + ube-specific reset rules |
| @ulam/ube/base-utils.css | Focus rings, aria-disabled state, visually-hidden utilities |
| @ulam/ube/base-user-prefs.css | User preference media queries (reduced motion, prefers-color-scheme) |
| @ulam/ube/base-print.css | Print stylesheet (overlay hiding, content flow) |
| @ulam/ube/ui.css | Shorthand: imports all base-*.css files |
| @ulam/ube/theme-fiesta.css | Fiesta theme color cycling (optional, load after ui.css) |
Component stylesheets (e.g., buttons.css, form-control-toggle.css) are imported automatically by their components—no manual import needed.
Design principles
Accessible by default. Every component meets WCAG 2.2 AA. Keyboard navigation, focus management, and screen reader semantics are built in, not bolted on.
Token-driven. No hardcoded colors, sizes, or spacing in component code. Change a token, retheme everything.
Touch-safe. 44px minimum touch targets on all interactive elements (WCAG 2.5.5).
Motion-respectful. All animations and transitions are suppressed under prefers-reduced-motion: reduce.
RTL-aware. Direction-sensitive components (ButtonBack, overlays, layout) respond to html[dir="rtl"] automatically.
Zero opinion on data. Components accept props and call handlers. No built-in state management, no assumptions about routing libraries (beyond the included router plugin), no opinions about where your data lives.
Roadmap
Planned improvements to the @ulam/ube library:
[ ] useAriaDisabled hook documentation + examples
[a11y]— Document thearia-disabledaccessibility pattern extracted during a11yfred development. Usage: preventing Space/Enter activation while keeping disabled controls keyboard-focusable and in tab order. Includes reference implementations (Select and Toggle components) and keyboard focus outline requirements for WCAG 2.4.11 compliance.[ ] Heading levels and styles abstraction
[design]— Create a utility component or helper hook that decouples semantic heading levels (h1–h6) from visual styles (display, body, sub, etc.). Example:<HeadingText level={2} style="display">Title</HeadingText>renders an<h2>with large display-style text. Reduces cognitive load in apps where heading hierarchy doesn't match visual prominence.
