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

@tapestry-ui/combobox

v0.3.0

Published

A WCAG 2.1 AA combobox for Preact — type to filter or open the whole list, APG keyboard, match highlighting, headless behavior, skin via CSS. The polished replacement for a native select.

Readme

@tapestry-ui/combobox

A combobox for Preact: type to narrow, or open the whole list like a select. WCAG 2.1 AA, headless behavior, skinned entirely through CSS.

Part of Tapestry UI — components forged inside a woven-tapestry game and a private war-room dashboard, then published so they have to survive strangers.

npm i @tapestry-ui/combobox
import { Combobox } from '@tapestry-ui/combobox';
import '@tapestry-ui/combobox/styles.css'; // structural only — no colors

<Combobox
  label="Property"
  options={properties.map((name) => ({ value: name }))}
  value={chosen}
  onChange={setChosen}
  placeholder="all properties"
/>

Why not a <select>

A native select cannot be styled past its edges and cannot be typed into. With eighteen options, "scroll until you see Fairview" is the wrong interaction — you already know the name. This is both halves at once: a text field that filters as you type, and a chevron that shows you everything.

The two contracts

Typing is a query, never a value. A component that replaces a <select> must never end up holding something nobody chose. Abandon the field half-typed and the committed label comes back. Only Enter, Tab, a click, or — with allowCustom — blurring on free text commits anything.

The container contract, and its one exception. Tapestry UI components never own scrolling. The popup is the exception, deliberately: it is an overlay over the page rather than part of your layout, so capping its height constrains nothing you laid out. The cap is still yours — --tui-combobox-list-max-h: none removes it.

Props

| Prop | Type | Notes | |---|---|---| | options | ComboboxOption[] | { value, label?, description?, disabled? }; label defaults to value | | label | string | required — the accessible name, read before the value | | showLabel | boolean | render the label visibly too (default: screen readers only) | | value / onChange | string / (value) => void | controlled; the component reports intent and never drifts | | defaultValue | string | uncontrolled starting value | | placeholder | string | | | allowCustom | boolean | commit typed text that matches no option | | openOnFocus | boolean | show the whole list the moment the field is focused | | filter | (query, option) => boolean | the query arrives lowercased and de-accented | | emptyLabel | string | shown when nothing matches (default "no match") | | toggleLabel | string | accessible name for the chevron | | maxVisible | number | rendered-row cap (default 100); the rest are counted, not drawn | | disabled / required | boolean | | | name | string | adds a hidden input so the field posts like a <select> | | class / inputClass / listClass / optionClass / id | string | skin hooks |

foldCase(text) is exported — the same lowercase-and-strip-accents the default filter uses, in case you write your own.

Keyboard

| Key | Does | |---|---| | ↓ / ↑ | open the list; move the highlight, wrapping at the ends | | Home / End | first / last option | | Enter | commit the highlighted option (and not submit the form) | | Tab | commit the highlighted option and move on | | Esc | close the list; again restores the committed label | | typing | filter, highlighting the matched run |

Disabled options are never a destination. DOM focus stays in the input the whole time — the highlight is carried by aria-activedescendant, so a screen reader reads the option without losing the text field, and the option count is announced from a polite live region as the list narrows.

Styling

Structural CSS ships in styles.css; it picks no colors. Skin it with custom properties, or ignore the file and style the [data-part] hooks yourself. Never fork the package to restyle it.

| Property | Default | |---|---| | --tui-combobox-pad | 0.4em 2em 0.4em 0.6em | | --tui-combobox-focus | 2px solid currentColor | | --tui-combobox-toggle-w | 2em | | --tui-combobox-chevron-w | 0.625em | | --tui-combobox-list-max-h | 15em (none to uncap) | | --tui-combobox-option-pad | 0.4em 0.6em | | --tui-combobox-z | 20 |

Parts: root (data-state="open|closed", data-placement="below|above"), label, field, input, toggle, list, option (data-state="active|idle"), option-label, option-desc, match, empty, more, status, value.

The popup opens upward on its own when there is no room below it — a phone keyboard eats the bottom half of the screen, and a popup under the fold is a popup that isn't there.

License

MIT © Brandon Minton