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

@vinyasa/form

v2.0.3

Published

All 25 form controls in the "Form Components" category, delivered in three staged phases:

Readme

@vinyasa/form

All 25 form controls in the "Form Components" category, delivered in three staged phases:

  • Phase 1: Icon Button, Toggle Button, Input, Search Input, Password Input, Number Input, Textarea, Checkbox, Radio Group, Switch.
  • Phase 2: Slider, Range Slider, OTP Input, Select, Combobox, Autocomplete, Multi Select, Tags Input.
  • Phase 3 (this round): Date Picker, Time Picker, Color Picker, File Upload, Dropzone, Rich Text Editor.

Installation

pnpm add @vinyasa/form @vinyasa/layout @vinyasa/icons @vinyasa/button @vinyasa/tokens react react-dom

@vinyasa/button's Button is composed directly by Icon Button and Toggle Button rather than reimplemented. Every interactive control wraps an established library rather than hand-rolling keyboard/ARIA/drag behavior — all regular dependencies of this package, not something consumers install themselves:

| Component(s) | Library | | -------------------------------------------- | ------------------------------------------------------------------- | | Checkbox, Radio Group, Switch, Toggle Button | @radix-ui/react-checkbox / -radio-group / -switch / -toggle | | Slider, Range Slider | @radix-ui/react-slider | | Select | @radix-ui/react-select | | Combobox, Autocomplete, Multi Select | @radix-ui/react-popover + cmdk | | OTP Input | input-otp | | Date Picker | react-day-picker + @radix-ui/react-popover + date-fns | | Color Picker | react-colorful + @radix-ui/react-popover | | File Upload, Dropzone | react-dropzone | | Rich Text Editor | @tiptap/react + @tiptap/starter-kit |

Tags Input has no dropdown, so it needs neither Popover nor cmdk — it's a plain chip-token text field. Time Picker wraps a native <input type="time"> rather than a library — the browser already provides an accessible, localized time-entry UI for free.

This package has no root export — every component is subpath-only (import { Checkbox } from '@vinyasa/form/checkbox', never from '@vinyasa/form'). A root barrel re-exporting all 25 components would let a bundler tree-shake the unused JS down to just the one component you import, but the CSS side-effect imports those other 24 components carry are not eligible for the same tree-shaking — confirmed empirically with both esbuild and Rollup, importing one component from a hypothetical root barrel pulled in roughly 8x more CSS than the same component imported by its own subpath. Removing the root entry entirely (rather than just documenting around it) makes that the only possible outcome, not something that depends on your bundler being clever enough to shake it out.

New tokens this package introduces

None across all three phases. Every control's chrome (border, focus ring, invalid/disabled state, floating-content elevation) reuses existing tokens.

New icons this package needed

Added to @vinyasa/icons across the three phases: CalendarIcon, ClockIcon, PaletteIcon, PaperclipIcon, UploadIcon (Phase 1, used by Phase 3's pickers/uploaders) and BoldIcon, ItalicIcon, ListIcon, ListOrderedIcon, QuoteIcon, RedoIcon, StrikethroughIcon, UndoIcon (Phase 3, for Rich Text Editor's toolbar).

Shared internal chrome

  • src/field.tsx / src/field.css.ts — label/description/error/required/focus-ring/invalid/disabled layout shared by every text-like control. Not a public component — the same "shared internal system" pattern @vinyasa/feedback's status.ts used.
  • src/selectPopover.css.ts — floating-listbox chrome (content/list/item/chip styles) shared by Combobox, Autocomplete, and Multi Select, the three "type to filter a list" patterns built on Popover + cmdk. Select, Date Picker, and Color Picker each have their own <Component>.css.ts popover-content styling instead, since their underlying libraries (@radix-ui/react-select, react-day-picker, react-colorful) have different part structures — Date Picker's content, in particular, deliberately isn't width-constrained to its trigger the way selectPopover's is, since a calendar has its own natural size.

A load-bearing fix, not just a style choice: every popover-based component (Select, Combobox, Autocomplete, Multi Select, Date Picker, Color Picker) deliberately renders its floating content without Portal. VinyasaProvider injects theme CSS custom properties as an inline style on its own wrapper element, not :root — anything rendered via createPortal to document.body (which is what those Portal components do by default) falls outside that subtree, and every var(--vinyasa-*) token resolves to nothing (the exact bug @vinyasa/feedback's Toaster hit and fixed the same way). Radix's own floating-position calculation still works correctly without the portal; only the DOM location changes. Verified in a real browser for each of these six components — the popover content genuinely renders with real, non-transparent styling, not just "doesn't crash."

A cmdk gotcha worth knowing if you touch Combobox/Autocomplete: Command.Input always overwrites any id prop you pass with its own internally-generated one, and gets its accessible name from <Command label="..."> (wired via aria-labelledby to a hidden label cmdk renders itself) — not from FieldChrome's own visible <label htmlFor>. FieldChrome has a disableLabelFor escape hatch for exactly this (omits the now-dangling htmlFor, which would otherwise silently fail to focus anything when a real user clicks the visible label). The same underlying issue — a trigger's accessible name coming from its <label>, not its own visible text content — is why Date Picker's, Color Picker's, and File Upload's own trigger buttons show their current value/state as plain rendered content rather than as part of the button's accessible name; tests and consumers alike should query these by their label text, not by the formatted date/hex/filename shown inside.

react-day-picker is styled entirely through its own classNames prop (keyed by its UI/DayFlag/SelectionState enums) rather than its default stylesheet — the same "headless, theme it yourself" contract already used for Select/Combobox's underlying libraries. DatePicker also passes defaultMonth={currentValue} so reopening the popover on an already-selected date shows that date's month, not always today's — react-day-picker's own default otherwise anchors to today's month regardless of the current selection.

Usage

Icon Button

import IconButton from '@vinyasa/form/icon-button';

<IconButton aria-label="Delete" icon={<TrashIcon />} variant="ghost" />;

aria-label is a required prop — an icon-only button has no other accessible name.

Toggle Button

import ToggleButton from '@vinyasa/form/toggle-button';

<ToggleButton pressed={bold} onPressedChange={setBold}>
	<BoldIcon />
</ToggleButton>;

Wraps Radix's Toggle with asChild, passing Button as the child — Radix owns press/keyboard/aria-pressed, Button owns the visual chrome. pressedVariant/unpressedVariant (default 'primary'/'outline') pick which Button variant renders for each state.

Input, Search Input, Password Input, Number Input, Textarea

import Input from '@vinyasa/form/input';
import SearchInput from '@vinyasa/form/search-input';
import PasswordInput from '@vinyasa/form/password-input';
import NumberInput from '@vinyasa/form/number-input';
import Textarea from '@vinyasa/form/textarea';

<Input label="Email" error="Enter a valid email address." />
<SearchInput label="Search" value={query} onChange={handleChange} onClear={() => setQuery('')} />
<PasswordInput label="Password" />
<NumberInput label="Quantity" min={0} max={10} />
<Textarea label="Bio" autoResize />

Checkbox, Switch, Radio Group

import { Checkbox } from '@vinyasa/form/checkbox';
import { Switch } from '@vinyasa/form/switch';
import { RadioGroup } from '@vinyasa/form/radio-group';

<Checkbox label="Accept terms" checked={accepted} onCheckedChange={setAccepted} />
<Switch label="Enable notifications" defaultChecked />
<RadioGroup
	options={[
		{ value: 'sm', label: 'Small' },
		{ value: 'md', label: 'Medium' },
	]}
	value={size}
	onValueChange={setSize}
/>

Slider, Range Slider

import { Slider } from '@vinyasa/form/slider';
import RangeSlider from '@vinyasa/form/range-slider';

<Slider label="Volume" value={[volume]} onValueChange={([v]) => setVolume(v)} />
<RangeSlider value={range} onValueChange={setRange} minLabel="Low" maxLabel="High" />

RangeSlider is a convenience wrapper around Slider, not a separate implementation — Radix's Slider already supports any number of thumbs via a value array; RangeSlider just fixes it at two with distinct min/max accessible labels.

OTP Input

import OtpInput from '@vinyasa/form/otp-input';

<OtpInput label="Verification code" length={6} onComplete={handleSubmit} />;

Select

import { Select } from '@vinyasa/form/select';

<Select
	label="Size"
	options={[{ value: 'md', label: 'Medium' }]}
	placeholder="Choose a size"
	value={size}
	onValueChange={setSize}
/>;

Combobox, Autocomplete

import { Combobox } from '@vinyasa/form/combobox';
import { Autocomplete } from '@vinyasa/form/autocomplete';

<Combobox label="Framework" options={frameworkOptions} value={framework} onValueChange={setFramework} />
<Autocomplete label="State" suggestions={states} value={state} onValueChange={setState} />

The entire difference between the two: Combobox only ever commits one of its own options; Autocomplete commits whatever text is in the field, with suggestions as a shortcut for filling it in rather than a constraint on it.

Multi Select

import { MultiSelect } from '@vinyasa/form/multi-select';

<MultiSelect
	label="Favorite colors"
	options={colorOptions}
	value={colors}
	onValueChange={setColors}
/>;

Selected values render as removable chips in the trigger; picking an item in the popover toggles membership without closing (unlike Combobox, which closes on selection).

Tags Input

import TagsInput from '@vinyasa/form/tags-input';

<TagsInput label="Skills" value={skills} onValueChange={setSkills} max={10} />;

Free-text tokens — press Enter or comma to commit the current draft as a tag, Backspace on an empty draft removes the last one.

Date Picker, Time Picker

import { DatePicker } from '@vinyasa/form/date-picker';
import { TimePicker } from '@vinyasa/form/time-picker';

<DatePicker label="Date of birth" value={date} onValueChange={setDate} />
<TimePicker label="Meeting time" value={time} onChange={handleChange} />

dateFormat (a date-fns format string, default 'PPP') controls how the selected date renders on the trigger.

Color Picker

import { ColorPicker } from '@vinyasa/form/color-picker';

<ColorPicker label="Brand color" value={color} onValueChange={setColor} />;

A hex-only picker (react-colorful's HexColorPicker + HexColorInput) — the trigger shows a swatch plus the current hex value, and the popover's text input keeps typed hex codes in sync with the visual picker.

File Upload, Dropzone

import { FileUpload } from '@vinyasa/form/file-upload';
import { Dropzone } from '@vinyasa/form/dropzone';

<FileUpload label="Attachments" value={files} onValueChange={setFiles} accept={{ 'application/pdf': ['.pdf'] }} />
<Dropzone label="Attachments" value={files} onValueChange={setFiles} maxSize={5 * 1024 * 1024} />

Both wrap the same useDropzone hook and share their file-list styling (file-upload/FileUpload.css.ts) — File Upload is a compact button trigger, Dropzone a full drag-and-drop area; pick whichever chrome fits the layout.

Rich Text Editor

import { RichTextEditor } from '@vinyasa/form/rich-text-editor';

<RichTextEditor label="Description" value={html} onValueChange={setHtml} />;

Wraps Tiptap (StarterKit) with a toolbar built from this package's own Toggle Button (bold/italic/strikethrough/lists/blockquote/undo/redo). value/onValueChange are HTML strings; an externally-set value is pushed into the editor via setContent only when it actually differs from the editor's current content, so it doesn't fight the user's own typing.

Subpath imports

Every component is imported by its own subpath (e.g. @vinyasa/form/date-picker) — there is no root @vinyasa/form entry, so import { X } from '@vinyasa/form' fails to resolve. See "This package has no root export" above for why.

Development

From the repository root:

pnpm --filter @vinyasa/form build
pnpm --filter @vinyasa/form test
pnpm --filter @vinyasa/form lint
pnpm storybook   # Form/*