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

@hozo/patterns

v0.4.0

Published

Accessible composite UI patterns for Hozo applications.

Readme

@hozo/patterns

Accessible stateful widgets for Hozo: Dialog, Tabs, Menu, Listbox, Combobox, RadioGroup, Checkbox, Switch, Accordion, Slider, Toolbar, Tree, Tooltip, Popover, BottomSheet, Drawer, Chip, Avatar, Pagination, Stepper, OtpInput, CommandPalette and ColorPicker. Each carries its WAI-ARIA keyboard contract on the Web and the matching accessibility semantics on React Native.

Checkbox and Switch are one control with two roles, and choosing between them chooses what a person hears: a checkbox is "checked" or "not checked" and can also be "partially checked", while a switch is "on" or "off" and ARIA gives its role no third value. The checkbox is a <button role="checkbox"> rather than an <input> because a real checkbox's indeterminate state is a DOM property with no attribute — it cannot be rendered on a server, and aria-checked="mixed" can. Both carry data-hozo-state (checked / unchecked / mixed) so one class list can draw all three states without the application passing its own state back in.

Accordion takes multiple rather than being two components, and its headers are each their own tab stop — the opposite of Tabs, where the strip is one stop and the arrows move within it. That difference is what the two controls are for: a tab strip is a chooser, so stopping on each tab would make a reader pass six things to reach the content, while an accordion's headers are each a thing to act on. The arrow keys move between headers as well, because the Authoring Practices offer them and a ten-item accordion is unpleasant to Tab through. headingLevel is a prop with a guessed default, because a heading level is a fact about the page rather than about the component.

Slider keeps its arithmetic in slider-rules.ts and exports it, because the hard part of a slider is not the markup: where a drag lands, how a step snaps, and what a key does are all pure functions that both platform halves import, so the two cannot disagree. On the Web the gesture is pointer events with setPointerCapture, which is what keeps a drag working after the pointer leaves the track — exactly when a person is trying hardest to reach the end. On React Native it is a PanResponder, and the thumb also answers increment and decrement, because with a screen reader on, a swipe sends those instead of a pan and a slider that only listens to the pan cannot be moved at all. valueText becomes aria-valuetext: "3" is not an answer to "how loud".

Popover is #142's equation and nothing else: FloatingPositioner + FocusScope + DismissableLayer. It needs nothing new from @hozo/behaviors, which is what makes it worth having — the composition already existed inside DatePicker, and this is the same arrangement with the contents left to the caller.

Two decisions in it are worth knowing. It is not modal by default, because aria-modal and a Tab trap have to travel together or each does its worse half: a reader stops offering the page while the keyboard walks straight out of the panel. DatePicker is modal for a reason — a grid you arrow around is unusable with Tab escaping it — and a card with two links in it is not that. And a non-modal panel closes when focus leaves it, which is the half people forget; without it you get an open panel sitting behind the page. On React Native there is no DismissableLayer: no Escape key, and an outside press needs a full-screen catcher, which is a modal by another name. So the trigger dismisses it, and accessibilityViewIsModal stands in for aria-modal.

It is aria-haspopup="dialog" rather than "menu", which is the line between it and Menu: a reader told "menu" expects commands and a popover holds a form.

BottomSheet is the other half of #142: Portal + FocusScope + DismissableLayer and a gesture, which is the part that is deliberately not a Behaviour -- press and pan are delegated to React Native's responder system rather than owned, so the arithmetic goes in sheet-rules.ts and each platform writes the gesture in its own vocabulary. Pointer events with setPointerCapture on the Web, a PanResponder on React Native, and one module deciding which detent a flick lands on so the two cannot disagree. Its velocity convention is the same on both sides without a conversion, because gestureState.vy and a clientY delta are both positive downward and both in pixels per millisecond.

It is modal, which is the opposite end of Popover's argument and the same rule: a sheet has a scrim, something that dims the page has already said the page is unavailable, so aria-modal and the Tab trap have to say it too. The drag handle is the interesting piece of its accessibility. With one detent there is nothing to resize, so it is decoration a reader is not shown -- drag-to-dismiss already has the tap alternative WCAG 2.5.7 asks for, in the scrim. With several it becomes a named button that cycles detents and answers the arrows, because then dragging is the only way to change the size. On React Native the sheet is a Modal, so Android's back button dismisses it through onRequestClose -- the thing Popover cannot offer there -- and the escape accessibility action answers VoiceOver's two-finger scrub.

Drawer closes #142 and is the same composition against a side edge, with the gesture on one platform only. That is #142's own split rather than a shortcut: the Web drawer is asked for an off-canvas panel, a focus trap and a scroll lock, and the gesture is listed under Native. A drawer has no grabber, so a swipe has to drag the panel's own body -- which fights text selection on a desktop and fights nothing on a phone, where there is also no Escape key to fall back on. The arithmetic is still sheet-rules.ts: it is written as "fraction showing", "travel away from open" and "velocity away from open" and knows nothing about which direction away is, so a left-hand drawer negates one axis and reuses every line of it.

side is 'start' or 'end' as well as 'left' or 'right'.

  • 'start' is the edge a line of text begins at, so a navigation drawer written side="start" opens from the right in Arabic or Hebrew without the application choosing a side per language.
  • The direction comes from HozoI18nProvider's dir when there is one (decision 008). Otherwise it comes from the platform: the document's resolved direction on the Web, I18nManager.isRTL on React Native.
  • useHozoDrawerSide is exported for a look that needs the resolved edge, as @hozo/ui's does.
  • The default is still 'left', so existing drawers do not move.

Edge-swipe-to-open is deliberately absent, in both halves: a closed drawer cannot listen at the screen's edge without keeping an invisible catcher over the application's content for as long as it is closed, which is a decision about the screen rather than about the drawer, and it collides with the platforms' own back gestures. It can never be the only way in either, since nobody who cannot swipe could reach it.

useScrollLock is the one thing a real <dialog> gives away for free and a position: fixed panel does not, so both Drawer and BottomSheet call it: without it a wheel over the scrim scrolls the page a reader has just been told is unavailable. The locks are counted, because a drawer can open a sheet and two overlays each restoring what they found would leave the page locked for good.

They are built on the headless engines in @hozo/behaviors -- focus scopes, roving focus, typeahead, dismissal and floating positioning -- which stay reusable on their own. Applications usually import the widgets from @hozo/core, which re-exports this package.

import { Dialog, Tabs, Text } from '@hozo/core'

export function Settings({ open, onClose }) {
  return (
    <Dialog open={open} onClose={onClose} accessibilityLabel="Settings">
      <Tabs
        accessibilityLabel="Settings sections"
        tabs={[
          { label: 'Profile', content: <Text>Profile</Text> },
          { label: 'Notifications', content: <Text>Notifications</Text> },
        ]}
      />
    </Dialog>
  )
}

Semantic state of Chip class slots

Chip's selection uses aria-pressed:* classes on its standalone toggle. Native still announces that toggle as togglebutton with checked; that platform announcement does not create a Web aria-checked attribute. With removal, className belongs to the outer span/View, so it does not match the inner toggle's aria-pressed:*, disabled:* or enabled:*. removeClassName belongs to the separate button and only gets that button's disabled/enabled state, never selection. Neither Web owner writes aria-checked or aria-disabled; those attributes must not be inferred from Native announcements. Literal classes use the same owner facts through private compiler callbacks, including negation and supported stacks, with explicit styles last. This does not expand arbitrary dynamic-class lowering or the candidate resolver's existing variant support.

Semantic state of Pagination class slots

Pagination's button controls match disabled:*/enabled:*; link controls match neither, and disabled links instead carry aria-disabled="true". The same distinction drives Native candidate classes and literal compiled slot callbacks. Native still announces the current item as selected, but that does not make aria-selected:* match: Web uses aria-current="page". Use currentItemClassName for current-page decoration; arbitrary aria-current value selectors are not added here. Boundary activation stays disabled on Native for both roles, and explicit slot styles retain precedence.

Semantic state of Stepper class slots

Stepper's four step/status class props belong to a button when onStepPress is provided, otherwise to a descriptive list item. Only the button can match disabled:*/enabled:*. A descriptive row matches neither positive predicate, but does match their negations in literal compiled classes, even when its step data says disabled. Event predicates, including negated ones, still require the optional button; its absence must not activate not-hover:*. Neither owner writes aria-selected or aria-disabled. Native's current-step selected announcement and real disabled activation guard remain unchanged; they are not CSS attributes. Use currentStepClassName for current status (Web uses aria-current="step" on the list item).

Candidate classes and private compiled callbacks use the same owner facts. Indicators and descriptions do not borrow disabled/enabled or ARIA semantics; text still inherits the step's typography before explicit slot overrides. This adds no compiled decorative state handling and does not expand arbitrary dynamic classes, runtime negation/enabled variants, or arbitrary aria-current value selectors. Existing hover/press propagation is unchanged, not a new cross-platform parity claim.

Semantic state of ColorPicker swatches

swatchClassName and selectedSwatchClassName belong to each preset's radio button, not the picker group or its hex input. Literal compiled classes can read that radio's disabled:*/enabled:* and aria-checked:*, including negation and supported stacks, without an authored disabled or accessibilityState prop. Selection changes update the same snapshot used by runtime candidate classes and compiler-only callbacks. The selected slot is still evaluated only on the checked preset. As on Web, only the first valid colour-matching preset is checked; equivalent hex spellings do not create multiple checked radios, and invalid presets' fallback colours are not treated as matches.

Web writes aria-checked and disabled, not aria-disabled or aria-selected; Native's disabled announcement must not make those absent attributes match. Native radio roles and disabled activation suppression are unchanged. Explicit slot styles keep their precedence; the preset's background colour remains last because it communicates the colour itself. This does not add semantic state to the picker group, track, thumb or hex-input class props, or expand arbitrary dynamic-class or runtime negation/enabled support.

Semantic state of CommandPalette options

itemClassName and activeItemClassName belong to Web option divs: neither :disabled nor :enabled matches them. Use aria-disabled:* for unavailable commands and aria-selected:* for the highlighted return-key target; literal compiled classes support their negations and supported stacks. not-disabled:* and not-enabled:* both match an option. It has no aria-checked attribute. These facts are shared by candidate classes and generated callbacks, without requiring a corresponding prop on the palette itself.

Native still presents real buttons and suppresses disabled activation. The active slot follows the same filtered/enabled target as Return; a target is not a press. A decorative shortcut owns none of the option's semantic attributes, but inherits its text styles before its own class/style overrides. Explicit item and active-item styles keep their precedence.

This does not wire static focus variants on option slots (Web input retains focus), or static semantic variants on the shortcut, wrapper, panel or input. Existing runtime item focus and decorative hover/press behavior is unchanged, not a new cross-platform focus/pointer claim. Runtime negation/enabled and arbitrary dynamic-class support are not expanded here.

Semantic state of OtpInput

inputClassName belongs to the one real text/password input. Its internal disabled/enabled and read-only facts drive both candidate classes and literal compiler callbacks, including supported negations/stacks, even when no driver props were authored. CSS read-only includes disabled fields; readonly alone does not disable the field. Native announcements do not create Web aria-disabled/selected/checked attributes.

The wrapper div and decorative cell spans match neither disabled nor enabled. They do match read-only because they are not editable HTML elements; they do not borrow the field's focus or ARIA attributes. Existing active/filled class gates, one-field accessibility, transparent glyphs, fixed overlay geometry, caret behavior and explicit style precedence remain unchanged. Decorative semantic facts are constants at build time, not input subscriptions. Unknown ARIA and cell focus/pointer variants remain diagnosed; runtime negation/enabled limitations are not broadened.

The selector distinction follows the HTML selector definition and is checked against actual Web DOM/Tailwind separately from Native hosts.

Dynamic secondary class slots on Native

The compiler leaves dynamic class expressions on registered state-owning secondary slots until the actual pattern owner can supply its facts: Chip.removeClassName, Pagination item/current/disabled-item classes, ColorPicker swatch/selected-swatch classes, Stepper step/status-step classes, CommandPalette item/active-item classes and OtpInput.inputClassName. For example, removeClassName={classes} uses the remove button's own hover, focus and disabled state, not the ancestor's state or an early lookup without state. Literal classes still compile ahead of time through private callbacks.

Both paths use the existing slot composition order. Explicit styles follow their matching class list; a later status-specific slot still follows the base slot. A changed/absent dynamic string does not retain its old styles. The expression is evaluated by the author render, not inside a state callback. No extra event listeners, wrapper hosts or variant evaluator are added.

Dynamic expressions retain the build-time warning: only scanned candidates and the runtime resolver's supported conditions work. Breakpoints, negation and missing owner facts still warn; a descriptive step does not gain focus or pointer state. This slice does not change dynamic root className lowering, other secondary slots, or unsupported static variant refusals.

CommandPalette options are not the Web focus owner: its input retains DOM focus and announces the active option with aria-activedescendant. Dynamic item/active-item focus predicates therefore stay false even when the Native button receives an OS focus callback. Pointer/active-target decoration, activation and Native accessibility focus remain independent. Literal item focus variants keep their existing compiler refusal; use activeItemClassName to decorate the current return-key target.

Focus ownership of decorative slots

OtpInput has one focus owner: its real input. Put focus:* and focus-visible:* on inputClassName. The wrapper and virtual cells do not borrow that focus on either platform. Use unconditional classes in activeCellClassName to decorate the current insertion cell while the field is focused, and filledCellClassName for cells containing a character. Neither adds a focus stop. The virtual caret follows the actual input selection independently of these class lists. Negated runtime state variants still report their existing unsupported-class diagnostics; this does not expand which variants compile on a virtual cell.

CommandPalette's shortcutClassName styles a decorative Web span / Native View, not another focusable command. Native shortcuts do not borrow an item's focus:* or focus-visible:*; unconditional shortcut decoration and explicit shortcutStyle remain available. This does not change the actual Native button's focus handling, the Web input's active-descendant model, or which static shortcut variants the compiler accepts.

Stepper's clickable step owns focus, not its indicator or description. Use stepClassName or its status-specific step class props for the button's focus ring. Decorative indicator/status-indicator and description class props do not borrow focus:* or focus-visible:*. Their text still inherits the step's typography, including focus-driven text styles, unless the decorative slot overrides it. Explicit slot styles retain precedence. Static decorative focus variants remain diagnosed; this does not change the existing hover or press propagation, or add a focus stop to descriptive steps.

Verification Matrix: Chip, Avatar, Pagination, Stepper, OtpInput, CommandPalette, ColorPicker

These arrived after 0.2.0 (#141, #144, #149, #150, #152, #153). What has checked them, and what has not:

| | axe (every PR) | Virtual reader (every PR) | NVDA (weekly) | VoiceOver, macOS (weekly) | TalkBack | iOS VoiceOver | |---|---|---|---|---|---|---| | Chip | ✅ | ✅ approved | ✅ approved | ✅ approved | ⬜ | ⬜ | | Avatar | ✅ | ✅ approved | ✅ approved | ✅ approved | ⬜ | ⬜ | | Pagination (buttons, links) | ✅ | ✅ approved | ✅ approved | ✅ approved | ⬜ | ⬜ | | Stepper (list, buttons) | ✅ | ✅ approved | ✅ approved | ✅ approved | ⬜ | ⬜ | | OtpInput (code, PIN) | ✅ | ✅ approved | ⬜ not yet read | ⬜ not yet read | ⬜ | ⬜ | | CommandPalette (closed, open) | ✅ | ✅ approved | ⬜ not yet read | ⬜ not yet read | ⬜ | ⬜ | | ColorPicker | ✅ | ✅ approved | ⬜ not yet read | ⬜ not yet read | ⬜ | ⬜ |

  • axe and the virtual reader run on the Patterns/… Storybook stories (examples/storybook-demo/scripts/check-a11y.mjs, check-utterances.mjs). The virtual reader computes announcements from the ARIA and HTML-AAM specifications. It is not NVDA or VoiceOver. Its approved readings are in examples/storybook-demo/utterances/.
  • NVDA and VoiceOver walk every patterns- story weekly and on any change to examples/screen-readers/ (.github/workflows/screen-readers.yml). Their first reading of these stories was run 37790021610, and the phrases approved from it are in examples/screen-readers/expected/.
    • Stepper read correctly on NVDA. On VoiceOver it ran a step and its description together ("completedEmail and password").
    • A space between them did not survive (run 37796061776), because the step's sentence is visually hidden and absolutely positioned.
    • So the description is now part of that sentence, after a full stop, and hidden where it is drawn. Both forms are approved from run 37802449478, which reads "completed. Email and password" on both readers.
  • TalkBack and iOS VoiceOver have read none of them. The unit tests assert the accessibility props React Native is given (roles, labels, selected, disabled). They do not show what a reader says.

A ✅ in the reader columns means a person read the phrases and approved them, never only that a job ran.

Decorative control indicators

Checkbox, Switch and RadioGroup accept an optional indicator callback. It receives { checked, disabled } from the same state used for the control's accessibility attributes (Checkbox includes 'mixed'). Return { className, markClassName? } for an outline/track and optional mark/knob. The pattern owns their span/View hosts, hides them from assistive technology, and leaves their look entirely to those classes. Native resolves each slot through the project's class resolver. No indicator nodes are added when this prop is omitted. This is how @hozo/ui paints its indicators without duplicating the pattern's controlled or uncontrolled state.


Part of Hozo, a universal UI framework for React Native, compiled by Rust, with native semantics on every platform, first-class accessibility, and diagnostics for humans and AI agents alike. Most applications install @hozo/core and one build integration; see Getting started. Source and issues: github.com/iray-tno/hozo.