Shared UI components for web frontends — framework-free core, React and Preact renderers.
  • TypeScript 72.7%
  • CSS 20%
  • JavaScript 7.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
dev 1ba976606a
All checks were successful
Security and package checks / verify (push) Successful in 1m12s
Synchronize the aggregate component stylesheet
2026-10-13 16:47:44 -04:00
.forgejo/workflows Keep native table header dividers visible during scrolling 2026-10-07 14:40:04 -04:00
dist Preserve modal closing content and refine token icons 2026-10-13 16:11:47 -04:00
scripts Add shared skeleton styles and automatic capture boundaries 2026-10-12 23:07:34 -04:00
src Synchronize the aggregate component stylesheet 2026-10-13 16:47:44 -04:00
.gitignore Fix unsafe links and capture output writes 2026-10-01 23:35:33 -04:00
AGENTS.md Maintain repository tooling instructions 2026-09-12 23:54:31 -04:00
LICENSE Initial commit 2026-08-08 19:45:32 -04:00
package-lock.json Synchronize the aggregate component stylesheet 2026-10-13 16:47:44 -04:00
package.json Synchronize the aggregate component stylesheet 2026-10-13 16:47:44 -04:00
README.md Preserve modal closing content and refine token icons 2026-10-13 16:11:47 -04:00
RELEASING.md Make Select width explicit 2026-09-19 12:53:17 -04:00
tsconfig.json Initial commit 2026-08-08 19:45:32 -04:00
tsup.config.ts Add shared skeleton styles and automatic capture boundaries 2026-10-12 23:07:34 -04:00
tsup.preact.config.ts Add shared skeleton styles and automatic capture boundaries 2026-10-12 23:07:34 -04:00
vitest.config.ts Initial commit 2026-08-08 19:45:32 -04:00

ICU UI Kit

Shared controls, notifications, tables, token selection, and loading skeletons with a framework-free core and matching React and Preact components. Styling uses CSS custom properties; no utility framework is required.

Install And Import

Install a published release tag, replacing <release-tag> below. Keep the tag and resolved commit pinned in your lockfile.

npm install --ignore-scripts 'icu-ui-kit@git+https://git.gui.icu/dev/icu-ui-kit.git#<release-tag>'

Built JavaScript, declarations, and CSS are included in the Git package. Consumers do not need to run its build or enable install-time scripts.

Entry Required Framework Use
icu-ui-kit React 18+ React components and shared helpers
icu-ui-kit/preact Preact 10 Preact components and shared helpers
icu-ui-kit/core None Notification stores, transaction helpers, formatting, and configuration
icu-ui-kit/devtools React 18+ Optional Bones inspector
icu-ui-kit/preact/devtools Preact 10 Optional Bones inspector
icu-ui-kit/vite Vite in the host project Development capture plugin

Install the framework used by your app. The Preact entry uses preact/compat for Suspense and ref forwarding; it does not require React aliases. Do not mix React and Preact component entries in one renderer.

import { Button } from "icu-ui-kit";
import "icu-ui-kit/styles.css";

export function SaveButton({ onSave }: { onSave: () => void }) {
  return <Button onClick={onSave}>Save</Button>;
}

In linked or source-checkout integrations, deduplicate the host framework to prevent duplicate renderer instances. For Vite, use resolve.dedupe: ["react", "react-dom"] or resolve.dedupe: ["preact", "preact/hooks", "preact/jsx-runtime", "preact/compat"]. Verify a hook-using component in the served app after changing dependency resolution.

Styling And Browser Requirements

Import icu-ui-kit/styles.css once, before application overrides. For smaller CSS bundles, import icu-ui-kit/styles/base.css first, then the component parts you use, such as icu-ui-kit/styles/button.css and icu-ui-kit/styles/bones.css.

Theme components through --icu-* custom properties:

:root {
  --icu-panel: #111827;
  --icu-ink: #f9fafb;
  --icu-line: #374151;
  --icu-good: #22c55e;
  --icu-focus: #60a5fa;
}

Part-specific variables and defaults are listed alongside their rules in dist/styles. Stylesheet parts available under icu-ui-kit/styles/:

icu-ui-kit/styles/accordion.css
icu-ui-kit/styles/address.css
icu-ui-kit/styles/alert.css
icu-ui-kit/styles/amount.css
icu-ui-kit/styles/aura.css
icu-ui-kit/styles/badge.css
icu-ui-kit/styles/base.css
icu-ui-kit/styles/bones.css
icu-ui-kit/styles/button.css
icu-ui-kit/styles/chart.css
icu-ui-kit/styles/checkbox.css
icu-ui-kit/styles/chip.css
icu-ui-kit/styles/combobox.css
icu-ui-kit/styles/drawer.css
icu-ui-kit/styles/empty.css
icu-ui-kit/styles/floatingpanel.css
icu-ui-kit/styles/input.css
icu-ui-kit/styles/modal.css
icu-ui-kit/styles/navbar.css
icu-ui-kit/styles/pagination.css
icu-ui-kit/styles/popover.css
icu-ui-kit/styles/progress.css
icu-ui-kit/styles/radio.css
icu-ui-kit/styles/select.css
icu-ui-kit/styles/slider.css
icu-ui-kit/styles/snippet.css
icu-ui-kit/styles/switch.css
icu-ui-kit/styles/table.css
icu-ui-kit/styles/tabs.css
icu-ui-kit/styles/tabstrip.css
icu-ui-kit/styles/toast.css
icu-ui-kit/styles/token-picker.css
icu-ui-kit/styles/tokenicon.css

Dialog and anchored components use the browser's <dialog> and Popover APIs. Skeleton profiles use CSS container queries. The kit does not ship browser polyfills; validate these surfaces in your supported browsers. Text fields in Preact use onInput for changes as the user types.

prefers-reduced-motion disables motion. Set data-icu-motion="off" on an ancestor to disable movement in a subtree. configureMotion({ enabled: false }) sets the component default before mounting; it is read during rendering, not subscribed. Individual motion props can override that default while remaining subject to ancestor and user motion settings.

Notifications

Mount one Toasts stack at the application root, outside ancestors with overflow, transform, or contain. Wire one announcer if notifications should be spoken:

import { Announcer, Toasts, announce, configureToasts, notify } from "icu-ui-kit";

configureToasts({ onAnnounce: announce, durations: { error: 0 } });

export function Notifications() {
  return <><Toasts /><Announcer /></>;
}

const uploadId = notify("Uploading...", { status: "pending", ms: 0 });
notify("Upload complete", { id: uploadId, status: "success" });

Reusing an ID replaces its toast and resets its timer. By default, pending notifications remain until replaced or dismissed; other statuses expire after six seconds. ms: 0 pins a notification, and an explicit per-call ms overrides configured durations. configureToasts replaces the current announcement callback and duration configuration. The built-in stack has no live region: onAnnounce is silent unless configured. Toasts.items instead accepts controlled notifications whose lifecycle belongs to the host.

Transaction helpers update notifications; the host performs the transaction and determines its outcome:

import { configureTxToasts, txToastPending, txToastResolved } from "icu-ui-kit/core";

configureTxToasts({ resolveExplorerBase: (chainId) => explorers[chainId] ?? "" });
await txToastPending({ hash, chainId });
await txToastResolved(hash, outcome, chainId);

outcome is a presentation status such as "success" or "reverted", not a raw receipt status. txToastReplaced handles replacement transactions. Explorer lookups are cached by chain, including failed lookups; call configureTxToasts again to reset that cache. An empty or failed explorer lookup leaves the hash without a link. isUserRejection and txErrorMessage accept structural errors without a wallet-library dependency.

Controlled Components

Supply state and callbacks to controls rather than expecting them to fetch or persist application data. Give dialogs a visible title via labelledBy or an accessible label; their close callbacks must update host state. useDisclosure supplies simple open/close state.

TokenPicker accepts host-provided rows, search, favorites, and balances. Its renderList supports a host virtualizer, and renderFrame adapts a modal shell. TokenPickerRow can render noninteractive presentation when onSelect is omitted. PercentageControls reports percentages without calculating amounts. Amount formats base units; AmountField.header and renderInput adapt token-input layouts.

Table defaults to native table markup. htmlLayout adapts native table presentation; gridLayout accepts a host column template and externally virtualized row positions. useTableSort handles client-side sorting; fetching and virtualization remain host responsibilities.

nativeProps, presentation slots, and elementRef are available where declared by the component types. IconButton uses elementRef; Modal.initialFocus accepts a structural element reference. A button's managed loading/disabled state takes precedence over native activation. Button.href accepts relative links, fragments, http:, https:, mailto:, and tel:; rejected URLs render disabled anchors.

Modal Closing

Modal retains its last open presentation through the closing transition. onClose requests a change to controlled open; onClosed fires once after open becomes false and the dialog becomes hidden. Reopening during the transition cancels the earlier completion. Use onClosed to clear drafts and other form state. Independently updating child state and external context remain live; place close-state context providers outside the retained children.

Keep the component mounted while closing. keepMounted={false} lets the kit unmount it after the transition; removing the component directly skips this lifecycle.

<Modal open={open} onClose={() => setOpen(false)} onClosed={() => setDraft("")}>
  {content}
</Modal>

Loading Skeletons

Use skeletons while content is unavailable. Set hasData when cached content can remain visible during refresh or subsequent failure.

Method Use Requirement
Build capture, the default Repeatable layouts known before loading A captured BoneSheet and its generated CSS
Runtime capture, explicit opt-in Editor canvases and other unpredictable layouts Mounted, measurable presentation content or a captured fallback

Both methods produce sheets for the same Bones renderer and support the same styles. Build rendering does not measure the live DOM. Runtime capture keeps one content instance mounted, hides it while pending, and overlays its measured skeleton. A runtime cold start cannot infer geometry from content that has not rendered.

Capture A Build Layout

Render real presentation components with synthetic data at a fixture URL. Include the same fonts, styles, and representative container widths as the application. Keep network, wallet, and other side effects inert in fixtures; capture does not replace authorization.

Create a manifest such as bones.config.json:

{
  "version": 1,
  "styles": "src/bones/bones.css",
  "registry": "src/bones/registry.ts",
  "profiles": [
    { "key": "compact", "viewportWidth": 390, "query": {} },
    { "key": "wide", "viewportWidth": 1280, "query": { "minViewportWidth": 1024 } }
  ],
  "captures": [{
    "id": "catalog",
    "path": "/?bones-probe=catalog",
    "selector": "[data-bones-root]",
    "readySelector": "[data-bones-ready]",
    "sheet": "src/bones/Catalog.json"
  }]
}

Run the app or fixture server first. Configure BROWSERLESS_URL and BROWSERLESS_TOKEN for a Browserless v2 service that can reach its origin, then run:

npx --no-install icu-bones capture bones.config.json --origin http://localhost:4173
npx --no-install icu-bones check bones.config.json --origin http://localhost:4173

localhost means the Browserless host in these URLs; use a reachable origin when the service runs elsewhere. Origin selection is --origin, then BONES_ORIGIN, then the manifest's optional origin. Capture waits for the ready selector and fonts, and fails if the measured layout does not settle. check recaptures and fails on stale output. Regenerate sheets after changing fonts or geometry; do not edit generated rectangles by hand.

A capture can override profiles and pass collector policy through options. The first profile must be an unconditional fallback. Use containerWidth with containerSelector for constrained layouts, and query ranges for viewport/container selection. Horizontal geometry adapts to the container; vertical dimensions and text wrapping depend on the captured profile. Both collectors normalize text-bar height from font size and line height.

Import the generated CSS and render the sheet:

import { BonesBoundary } from "icu-ui-kit";
import type { BoneSheet } from "icu-ui-kit/core";
import catalog from "./bones/Catalog.json";
import "./bones/bones.css";

<BonesBoundary pending={loading} hasData={Boolean(data)} sheet={catalog as BoneSheet}>
  <Catalog data={data} />
</BonesBoundary>;

Alternatively, pass <Bones sheet={sheet} /> as Loading.skeleton. Loading displays error content and an optional retry action when no usable data exists. BonesBoundary accepts sheet or a named entry under BonesRegistryProvider; build mode defers its children during initial loading. Lazy registry entries use fallback while resolving. useBoneSheet reports ready, loading, missing, or error. Direct sheets are available synchronously for server rendering. BoneSuspense supplies a captured fallback to framework Suspense.

Capture Runtime Content Selectively

<BonesBoundary
  capture="runtime"
  pending={loading}
  hasData={Boolean(data)}
  sheet={capturedFallback}
  captureOptions={{ cacheKey: layoutRevision, paused: dragging, storage: "memory" }}
>
  <Canvas />
</BonesBoundary>;

Use outer className/style for the frame and contentClassName/contentStyle for the capture stage. Source and overlay share that stage. before and after keep persistent controls outside the hidden source; hideContent={false} permits an intentional content overlay. Hidden source content is inert and excluded from accessibility traversal. withBones(Component) uses the same boundary, forwarding component props and refs while reserving its optional bones prop for boundary controls.

For custom composition, useBoneCapture returns rootRef, sheet, capture(), and invalidate(). Capture observes content, resizing, assets, fonts, scroll, and theme changes. It coalesces measurements into animation frames and waits for fonts. enabled: false or paused: true stops measurement. Schedule capture() after a drop or an externally managed layout change; invalidate() discards the current cached sheet. Never mount a duplicate effectful component merely to measure it.

Keep one skeleton owner per region. A build-captured header and a runtime canvas can coexist. For independently owned slots inside a captured parent, use data-bones-region so the parent collector excludes that region. A single renderer can hand off from a captured fallback to runtime geometry; overlapping renderers for the same region are unsupported.

Runtime storage defaults to memory. storage: "none" disables caching; "session" persists public geometry in session storage. Include page, layout, and theme revisions in cacheKey. Cache identity also includes region, collector options, renderer revision, measured width, and document theme. Limits are 20 entries, 4 MiB total, and 512 KiB per entry. Sheets omit text and media URLs but still reveal layout structure: keep private geometry in memory and call clearBoneCaptureCache() on sign-out or identity changes. Clearing removes Bones caches and refreshes active captures without clearing unrelated session storage.

Appearance And Capture Policy

BonesTheme.config supplies defaults; local Bones.config or boundary skeletonConfig overrides them. BONES_ANIMATIONS contains pulse, wave, shiver, drip, slide, beat, shaker, shatter, and none for both capture methods.

BonesConfig controls colors, fallback radius, speed (slow, normal, rapid, or a positive multiplier), direction, cascade, shimmer angle, and shatter cell size/order. Shatter falls back to pulse above 512 fragments, or with list: true. Adaptive rules use host-provided conditions; they do not query network or battery APIs. Reduced motion takes precedence. Loading and BonesBoundary also accept optional enter/exit transitions, revealOnExit, disabled, and minDuration (default zero).

BonesBox, BonesParagraph, and BonesIgnore mark explicit boxes, approximate text, and exclusions. Paragraphs support line or word shapes, line counts, and alignment. Equivalent attributes are data-bones-box, data-bones-static, data-bones-opacity, data-bones-paragraph, data-bones-paragraph-mode, data-bones-paragraph-align, and data-bones-ignore. data-bones-atomic captures an element as one bone; data-bones-children opts it back into composition. Both collectors clip descendants to their root and overflow boundaries.

BoneCaptureOptions also supports strategy: "root-only", maxDepth, maxBones (up to 10,000), exclusion selectors/tags, atomic selectors, and paint: "literal". The default paint policy preserves recognized ICU theme-token references. Exceeding the bone limit falls back to a measured root placeholder.

Watch And Inspect

npx --no-install icu-bones capture bones.config.json --origin http://localhost:4173 --watch

Optional manifest watch paths default to src; generated outputs are excluded. For Vite development, add icuBonesPlugin({ manifest: "bones.config.json", origin }) from icu-ui-kit/vite. It captures after server startup and coalesces hot updates. Optional captureEntry serves a separate fixture entry at /__icu-bones-capture; keep it separate from the product entry so it stays out of production bundles.

Import BonesInspector from the matching devtools entry only in a development gallery. It outlines bones and reports structural metadata without displaying content, asset URLs, or cache keys.

Capture outputs must stay inside the manifest directory after directory-symlink resolution; final output symlinks are rejected. Run against a stable workspace with filesystem support for no-follow opens. Concurrent directory replacement is outside this containment guarantee.

API Reference

React and Preact expose matching names. Component props and generic adapter contracts are declared in dist/index.d.ts and dist/preact/index.d.ts; framework-free declarations are in dist/core/index.d.ts.

icu-ui-kit/core:

AddressOptions AnnouncePoliteness AnnouncerState BONES_ANIMATIONS Bone BoneContainer BoneSheet
BoneTier BonesAnimation CompactOptions MotionOptions OUTCOME TimeInput Toast ToastDurations
ToastInput ToastLink ToastOptions ToastPlacement ToastStatus TruncateOptions TxOutcome
TxToastOptions TxToastReplacedOptions TxToastReplacement TxToastTransaction announce callHost
clearAnnouncements clearToasts compact configureAddress configureMotion configureToasts
configureTxToasts dismissToast displayAddress explorerBase formatAddress formatTimeAgo
getAddressChecksum getAnnouncerState getToasts isChunkLoadError isTxOutcome isUserRejection
motionAttrs motionEnabled notify pauseToasts pushToast reloadOnce resumeToasts
setAddressChecksum shortHash subscribeAddress subscribeAnnouncer subscribeToasts timeAgoTick
toEpochMs toastError toastInfo toastSuccess toastWarning truncateAddress txErrorMessage
txExplorerUrl txToastPending txToastReplaced txToastResolved

React and Preact entries:

Accordion AccordionProps AccordionVariant Address AddressProps Alert AlertProps AlertTone
AlertVariant Amount AmountField AmountFieldProps AmountProps AnnouncePoliteness Announcer
AnnouncerProps AnnouncerState Aura AuraProps AuraShape BONES_ANIMATIONS Badge BadgeProps
BadgeSize BadgeTone Bone BoneCaptureOptions BoneContainer BoneHorizontal BoneProfile
BoneRegistry BoneSheet BoneSuspense BoneTier Bones BonesAdaptiveRule BonesAnimation
BonesBoundary BonesBoundaryProps BonesBox BonesConditions BonesConfig BonesIgnore BonesParagraph
BonesProps BonesRegistryProvider BonesTheme BonesTransition Button ButtonGroup
ButtonGroupOrientation ButtonGroupProps ButtonProps ButtonSize ButtonTone ButtonVariant Card
CardBody CardFooter CardHeader CardProps Chart ChartMark ChartProps ChartScale ChartSeries
Checkbox CheckboxProps CheckboxSize Chip ChipProps ChipSize ChipTone ChipVariant Code ComboBox
ComboBoxProps ContextMenu ContextMenuEntry ContextMenuItem ContextMenuProps ContextMenuSeparator
ContextMenuTrigger Disclosure Divider Drawer DrawerProps DrawerSide EmptyState EmptyStateProps
ErrorBoundary ErrorBoundaryProps Field FieldProps FloatingPanel FloatingPanelProps
FormatAmountOptions IconButton IconButtonProps Input InputProps InputVariant Kbd Loading
LoadingProps Modal ModalBody ModalFooter ModalHeader ModalProps ModalSize MotionOptions Navbar
NavbarProps NavbarSlot OUTCOME Pagination PaginationProps PaginationVariant PercentageControls
PercentageControlsProps PointerDownLike Popover PopoverProps Position Progress ProgressProps
ProgressSize ProgressTone RadioGroup RadioGroupProps RadioOption Select SelectOption SelectProps
SelectVariant Size Slider SliderProps SliderSize Snippet SnippetPlacement SnippetProps
SnippetVariant SortDescriptor Spinner SpinnerSize StatTile StatTileProps Switch SwitchProps
SwitchSize TabItem TabPanel TabPanelProps TabStrip TabStripItem TabStripProps Table TableColumn
TableGridOptions TableProps TableSort Tabs TabsProps Textarea TextareaProps TimeAgo TimeAgoProps
Toast ToastDurations ToastInput ToastLink ToastOptions ToastPlacement ToastStatus Toasts
ToastsProps TokenIcon TokenIconProps TokenPicker TokenPickerProps TokenPickerRow
TokenPickerRowProps Tooltip TooltipPlacement TooltipProps TxOutcome TxToastOptions
TxToastReplacement TxToastTransaction UseDraggablePanelOptions UseDraggablePanelResult
UseFloatingPanelOptions UseFloatingPanelResult UseResizablePanelOptions UseResizablePanelResult
VisuallyHidden announce boneContainerStyle boneStyle callHost clearAnnouncements
clearBoneCaptureCache clearToasts compact configureAddress configureMotion configureToasts
configureTxToasts dismissToast displayAddress explorerBase formatAddress formatAmount
formatTimeAgo getAddressChecksum getAnnouncerState getToasts hasAmount isChunkLoadError
isTxOutcome isUserRejection makeStatusChip motionAttrs motionEnabled notify pageRange
pauseToasts progressPercent progressValueText pushToast reloadOnce resumeToasts sanitizeAmount
setAddressChecksum shortHash sliderPercent sliderValueText subscribeAddress subscribeAnnouncer
subscribeToasts timeAgoTick toEpochMs toastError toastInfo toastSuccess toastWarning
truncateAddress txErrorMessage txExplorerUrl txToastPending txToastReplaced txToastResolved
useBoneCapture useBoneSheet useContextMenu useDisclosure useDraggablePanel useFloatingPanel
useResizablePanel useTableSort withBones

Develop The Kit

Use npm 12.2.0 with Node 22.22.2+ in the Node 22 line, Node 24.15.0+ in the Node 24 line, or Node 26+. From the repository root:

npm ci --ignore-scripts
npm run typecheck
npm test
npm run build
npm run verify:package

npm run build generates React, Preact, core, devtools, and Vite exports plus styles. Commit rebuilt dist/ with source changes: Git consumers using --ignore-scripts load those files directly. npm run styles rebuilds only CSS. npm run audit:prod and npm run audit:all fail on dependency advisories or scanner errors. node scripts/verify-firefox.mjs runs the separate native-browser checks and requires Firefox (FIREFOX_PATH selects its executable).

License

MIT — see LICENSE.