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

iboon-ui

v0.13.1

Published

The iBoon design system: tokens, a component stylesheet and typed React components, shared across iBoon products.

Readme

iboon-ui

The iBoon design system: design tokens, a component stylesheet, and typed React components. Shared across the iBoon product family so that a button, a table and a dialog mean the same thing in every app.

npm install iboon-ui

Two halves, usable separately

The stylesheet is plain CSS with class names — .btn, .card, .table-wrap, .badge-brand. It works from any framework, or none, in an app that imports no JavaScript from this package at all.

The components are typed React wrappers over exactly those classes, plus the behaviour markup cannot express: a focus trap, a scroll lock, a toast queue, a menu that stays on screen.

Mixing them is expected rather than tolerated. <Button variant="primary"> and className="btn btn-primary" render the same element — which is what lets an app be migrated a screen at a time instead of all at once.

Setup

// once, at your entry point
import 'iboon-ui/styles.css';

If your app already ships a CSS reset (Tailwind's preflight, for instance), take the tokens and components without this package claiming ownership of button and body:

import 'iboon-ui/tokens.css';
import 'iboon-ui/components.css';

The system uses Inter for UI and Newsreader for headings. They are not loaded here — a library that injects an @import for a font decides a network request on your behalf and cannot be overridden. Link them yourself:

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,[email protected],500;6..72,600;6..72,700&family=Inter:wght@400;500;600;700&display=swap"
  rel="stylesheet"
/>

Theme and density

Two independent axes, both set as attributes on <html>:

<html data-theme="dark" data-density="compact">

| attribute | values | default | | -------------- | --------------------- | ------------ | | data-theme | light | dark | light | | data-density | comfortable | compact | comfortable |

Dark is a real design, not an inversion: --brand and --danger become light colours, so anything filled with them takes its foreground from --on-brand / --on-danger rather than from a literal white.

Density exists because the apps genuinely differ. A screen of forms can afford air; a screen that is a fourteen-column grid of tasks cannot, and the same padding there produces a horizontal scrollbar. Only spacing and type-size tokens move — no colour, no radius, no component rule is restated.

// Your app picks once, at startup
document.documentElement.dataset.density = 'compact';

Components

import {
  Button, IconButton, Badge, Avatar, AvatarStack, Spinner, Divider,
  Card, Panel, Grid, PageHead, BackLink, Toolbar, EmptyState,
  Field, Input, Select, Textarea, Checkbox, SearchBox, FormActions, FormError,
  TableWrap, Table, EmptyRow, List, ListItem,
  StatRow, StatBox, Progress, Tabs,
  Modal, ConfirmDialog,
  ToastProvider, useToast,
  ContextMenu,
} from 'iboon-ui';

A page

<PageHead
  title="Sprint 14"
  subtitle="Nine days left. Four tasks unassigned."
  scope="Identity · IDN"
  actions={<Button variant="primary" onClick={create}>New task</Button>}
/>

<Card padded={false}>
  <TableWrap>
    <Table>
      <thead>
        <tr><th>Task</th><th>Assignee</th><th className="num">Points</th></tr>
      </thead>
      <tbody>
        {tasks.length === 0 && <EmptyRow colSpan={3}>No tasks in this sprint</EmptyRow>}
        {tasks.map((t) => (
          <tr key={t.id}>
            <td className="tcell-name">{t.title}</td>
            <td><Avatar name={t.assignee} size="sm" /></td>
            <td className="num">{t.points}</td>
          </tr>
        ))}
      </tbody>
    </Table>
  </TableWrap>
</Card>

A form

Field generates the id and wires htmlFor, aria-describedby and aria-invalid to the control inside it. Label-to-control association is the thing hand-written forms get wrong most often — the label looks right, clicking it does nothing, and a screen reader announces an unlabelled textbox.

<Field label="Sprint name" required error={errors.name}>
  <Input value={name} onChange={(e) => setName(e.target.value)} />
</Field>

<Field label="Goal" hint="What this sprint is for, in one sentence.">
  <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} />
</Field>

<FormActions>
  <Button onClick={cancel}>Cancel</Button>
  <Button variant="primary" loading={saving} onClick={save}>Save</Button>
</FormActions>

A dialog

Portalled, focus-trapped, scroll-locking, Escape-closing, focus-restoring. Nested dialogs are handled: Escape closes only the topmost, and the scroll lock is released by the last one out.

<Modal open={open} onClose={close} title="Move to sprint" size="sm"
  footer={<>
    <Button onClick={close}>Cancel</Button>
    <Button variant="primary" onClick={move}>Move</Button>
  </>}
>
  <p className="page-sub">Three tasks will move to Sprint 15.</p>
</Modal>

ConfirmDialog focuses Cancel, not the confirm button — deliberately. The dialog exists because the action is hard to undo, and the key someone reaches for to dismiss a surprise is Enter.

<ConfirmDialog
  open={open} onClose={close} onConfirm={remove}
  title="Delete this sprint?" confirmLabel="Delete" danger
>
  Its 14 tasks return to the backlog. This cannot be undone.
</ConfirmDialog>

Toasts

<ToastProvider>{app}</ToastProvider>

const { show } = useToast();
show('Sprint saved');
show('Could not reach the server', { tone: 'danger' });
show('Upload in progress', { duration: 0 }); // stays until dismissed

Errors get twice the default duration, because an error that vanishes in four seconds is an error nobody read. useToast throws when there is no provider rather than no-op'ing: a silently dropped confirmation looks exactly like the action having failed.

Buttons

| variant | use | | --- | --- | | primary | the one action the screen is for | | secondary | everything else (default) | | ghost | tertiary, in a dense row | | danger | destructive — outlined, not filled, so it is reachable without being the loudest thing on screen |

loading disables as well as spins. The reason to show a spinner is that the action is already running, and a second click would run it twice.

IconButton requires a label. An icon button with no accessible name is announced as "button", which is the same as not being there — so the type system asks for the name rather than a linter noticing later.

Colour vocabulary

Four tones, and every state in these apps maps onto one:

  • brand (pine) — settled, good, positive
  • accent (ochre) — attention, not alarm
  • danger — wrong
  • neutral — nothing to say

A fifth would have to mean something none of these do.

Icons

This package draws four icons itself (close, search, back, check) and takes everything else as a prop. It deliberately has no icon-library dependency — consumers are on incompatible majors of lucide-react, and a peer range spanning both would either break an install or quietly resolve to two copies.

import { Trash2 } from 'lucide-react';
<Button variant="danger" icon={<Trash2 />}>Delete</Button>

License

MIT