Skip to main content

Guide

react-select vs cmdk vs Downshift 2026

react-select vs cmdk vs Downshift compared for accessible select and combobox components. Multi-select, async search, keyboard navigation, and custom.

·PkgPulse Team·
0

react-select vs cmdk vs Downshift: Accessible Select/Combobox 2026

TL;DR

Building accessible select and combobox components is notoriously tricky — keyboard navigation, ARIA roles, screen reader announcements, async search, and multi-value inputs all have accessibility gotchas that native <select> elements don't handle well. react-select is the battle-tested workhorse — styled out of the box, multi-select, async loading, creatable options, grouped options; it handles 90% of select use cases with minimal code but carries a notable bundle (~20kB gzipped). cmdk is the command palette component popularized by Vercel, Raycast, and Linear — an accessible, keyboard-driven command menu/combobox used for fuzzy search across actions, it powers the ⌘K pattern everywhere in 2026. Downshift is the headless primitive for building any select/combobox/autocomplete with full accessibility — unstyled, low-level hooks that give you complete control, used internally by Chakra, Mantine, and others. For multi-select dropdowns with async search and built-in styling: react-select. For command palette / ⌘K / fuzzy search: cmdk. For building a fully custom, branded select/combobox from scratch with correct ARIA: Downshift.

Key Takeaways

  • react-select handles multi-selectisMulti, remove tags, creatable options built in
  • cmdk is the ⌘K standard — powers command palettes in Linear, Vercel, Raycast, shadcn/ui
  • Downshift is headless — hooks only, zero UI, WAI-ARIA 1.2 compliant patterns
  • react-select has async loadingloadOptions with debounced API fetch built in
  • cmdk has fuzzy filtering — built-in search scoring, custom filter function support
  • Downshift has three primitivesuseSelect, useCombobox, useMultipleSelection
  • cmdk is 2kB — smallest of the three, used in shadcn/ui's Command component

Use Case Map

Single select dropdown           → react-select (styled) or Downshift (headless)
Multi-select with tags           → react-select (isMulti)
Async search (API-backed)        → react-select (loadOptions) or Downshift
Command palette / ⌘K             → cmdk
Application-wide fuzzy search    → cmdk
Custom branded select            → Downshift (full control)
Grouped options                  → react-select or Downshift
Creatable options ("Add X")      → react-select (Creatable) or Downshift
Autocomplete input               → Downshift useCombobox or react-select

react-select: Batteries-Included

react-select is the most-installed React select library — styled, feature-complete, and used across enterprise applications.

Installation

npm install react-select

Basic Select

import Select from "react-select";

interface Option {
  value: string;
  label: string;
}

const options: Option[] = [
  { value: "react", label: "React" },
  { value: "vue", label: "Vue" },
  { value: "svelte", label: "Svelte" },
  { value: "angular", label: "Angular" },
  { value: "solid", label: "SolidJS" },
];

export function FrameworkSelect() {
  return (
    <Select
      options={options}
      placeholder="Select a framework..."
      isClearable
      onChange={(option) => console.log(option?.value)}
    />
  );
}

Multi-Select with Tags

import Select from "react-select";

const tagOptions = [
  { value: "typescript", label: "TypeScript" },
  { value: "react", label: "React" },
  { value: "node", label: "Node.js" },
  { value: "postgres", label: "PostgreSQL" },
  { value: "docker", label: "Docker" },
];

export function TagInput({ onChange }: { onChange: (tags: string[]) => void }) {
  return (
    <Select
      options={tagOptions}
      isMulti
      placeholder="Select technologies..."
      onChange={(selected) => onChange(selected.map((s) => s.value))}
      closeMenuOnSelect={false} // Keep open when selecting multiple
      classNamePrefix="react-select" // For CSS targeting
    />
  );
}

Async Loading

import AsyncSelect from "react-select/async";

interface UserOption {
  value: string;
  label: string;
  email: string;
}

async function searchUsers(inputValue: string): Promise<UserOption[]> {
  if (!inputValue) return [];

  const response = await fetch(`/api/users/search?q=${encodeURIComponent(inputValue)}`);
  const users = await response.json();

  return users.map((user: { id: string; name: string; email: string }) => ({
    value: user.id,
    label: user.name,
    email: user.email,
  }));
}

export function UserSearch({ onSelect }: { onSelect: (userId: string) => void }) {
  return (
    <AsyncSelect
      loadOptions={searchUsers}
      defaultOptions={false} // Don't load until user types
      placeholder="Search users..."
      noOptionsMessage={({ inputValue }) =>
        inputValue ? `No users found for "${inputValue}"` : "Start typing to search"
      }
      onChange={(option) => option && onSelect(option.value)}
      // Custom option rendering
      formatOptionLabel={(option) => (
        <div>
          <strong>{option.label}</strong>
          <span style={{ color: "#666", marginLeft: 8 }}>{option.email}</span>
        </div>
      )}
    />
  );
}

Creatable Select

import CreatableSelect from "react-select/creatable";
import { useState } from "react";

interface TagOption {
  value: string;
  label: string;
  __isNew__?: boolean;
}

export function CreatableTagInput() {
  const [options, setOptions] = useState<TagOption[]>([
    { value: "bug", label: "bug" },
    { value: "feature", label: "feature" },
    { value: "documentation", label: "documentation" },
  ]);
  const [selected, setSelected] = useState<TagOption[]>([]);

  function handleCreate(inputValue: string) {
    const newOption: TagOption = { value: inputValue, label: inputValue };
    setOptions((prev) => [...prev, newOption]);
    setSelected((prev) => [...prev, newOption]);
  }

  return (
    <CreatableSelect
      isMulti
      options={options}
      value={selected}
      onChange={(selected) => setSelected([...selected])}
      onCreateOption={handleCreate}
      placeholder="Add labels..."
      formatCreateLabel={(inputValue) => `Create label: "${inputValue}"`}
    />
  );
}

Custom Styling with CSS Modules

import Select, { StylesConfig } from "react-select";

interface Option { value: string; label: string; }

const customStyles: StylesConfig<Option, false> = {
  control: (provided, state) => ({
    ...provided,
    backgroundColor: "#1a1a1a",
    borderColor: state.isFocused ? "#6366f1" : "#333",
    boxShadow: state.isFocused ? "0 0 0 2px rgba(99, 102, 241, 0.3)" : "none",
    "&:hover": { borderColor: "#6366f1" },
    borderRadius: 8,
    minHeight: 40,
  }),
  menu: (provided) => ({
    ...provided,
    backgroundColor: "#1a1a1a",
    border: "1px solid #333",
    borderRadius: 8,
    boxShadow: "0 4px 20px rgba(0,0,0,0.4)",
  }),
  option: (provided, state) => ({
    ...provided,
    backgroundColor: state.isSelected
      ? "#6366f1"
      : state.isFocused
      ? "#2d2d2d"
      : "transparent",
    color: "#e5e7eb",
    "&:hover": { backgroundColor: "#2d2d2d" },
  }),
  singleValue: (provided) => ({ ...provided, color: "#e5e7eb" }),
  input: (provided) => ({ ...provided, color: "#e5e7eb" }),
  placeholder: (provided) => ({ ...provided, color: "#6b7280" }),
};

export function StyledSelect() {
  return (
    <Select
      styles={customStyles}
      options={[{ value: "a", label: "Option A" }, { value: "b", label: "Option B" }]}
    />
  );
}

cmdk: Command Palette / ⌘K

cmdk is the accessible command menu component that powers ⌘K patterns. It's used in shadcn/ui's Command component.

Installation

npm install cmdk

Basic Command Palette

import { Command } from "cmdk";
import { useState } from "react";
import "./command.css"; // Your styles

export function CommandPalette() {
  const [value, setValue] = useState("");

  return (
    <Command label="Command Menu">
      <Command.Input
        placeholder="Search commands, pages, users..."
        value={value}
        onValueChange={setValue}
      />
      <Command.List>
        <Command.Empty>No results found for "{value}"</Command.Empty>

        <Command.Group heading="Navigation">
          <Command.Item onSelect={() => (window.location.href = "/dashboard")}>
            Dashboard
          </Command.Item>
          <Command.Item onSelect={() => (window.location.href = "/settings")}>
            Settings
          </Command.Item>
          <Command.Item onSelect={() => (window.location.href = "/profile")}>
            Profile
          </Command.Item>
        </Command.Group>

        <Command.Group heading="Actions">
          <Command.Item onSelect={() => console.log("New project")}>
            Create new project
          </Command.Item>
          <Command.Item onSelect={() => console.log("Invite team")}>
            Invite team member
          </Command.Item>
        </Command.Group>

        <Command.Separator />

        <Command.Group heading="Help">
          <Command.Item onSelect={() => console.log("Docs")}>
            Open documentation
          </Command.Item>
        </Command.Group>
      </Command.List>
    </Command>
  );
}

Dialog Command Palette (⌘K Modal)

import { Command } from "cmdk";
import { useState, useEffect } from "react";

export function CommandDialog() {
  const [open, setOpen] = useState(false);

  // Open with ⌘K or Ctrl+K
  useEffect(() => {
    function onKeyDown(e: KeyboardEvent) {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault();
        setOpen((o) => !o);
      }
    }

    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, []);

  if (!open) return null;

  return (
    <div
      style={{
        position: "fixed",
        inset: 0,
        background: "rgba(0,0,0,0.5)",
        display: "flex",
        alignItems: "flex-start",
        justifyContent: "center",
        paddingTop: "15vh",
        zIndex: 9999,
      }}
      onClick={() => setOpen(false)}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: 640,
          maxHeight: "60vh",
          background: "#1a1a1a",
          borderRadius: 12,
          border: "1px solid #333",
          boxShadow: "0 25px 50px rgba(0,0,0,0.5)",
          overflow: "hidden",
        }}
      >
        <Command>
          <Command.Input
            placeholder="Type a command or search..."
            style={{
              width: "100%",
              padding: "16px",
              background: "transparent",
              border: "none",
              borderBottom: "1px solid #333",
              color: "#e5e7eb",
              fontSize: 16,
              outline: "none",
            }}
          />
          <Command.List style={{ maxHeight: "calc(60vh - 56px)", overflowY: "auto" }}>
            <Command.Empty style={{ padding: 16, color: "#6b7280" }}>
              No results found.
            </Command.Empty>

            <Command.Group heading="Recent">
              <Command.Item onSelect={() => setOpen(false)}>
                Open PkgPulse dashboard
              </Command.Item>
            </Command.Group>
          </Command.List>
        </Command>
      </div>
    </div>
  );
}

shadcn/ui Command Component

// This is exactly how shadcn/ui wraps cmdk:
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  CommandShortcut,
} from "@/components/ui/command"; // shadcn/ui wrapper

export function ShadcnCommand() {
  return (
    <Command className="rounded-lg border shadow-md">
      <CommandInput placeholder="Type a command or search..." />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            Calendar
          </CommandItem>
          <CommandItem>
            Search Emoji
          </CommandItem>
          <CommandItem>
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>
            Profile
            <CommandShortcut>⌘P</CommandShortcut>
          </CommandItem>
          <CommandItem>
            Billing
            <CommandShortcut>⌘B</CommandShortcut>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  );
}

Downshift: Headless Primitive

Downshift provides WAI-ARIA compliant hooks for building any select or combobox — zero styling, maximum control.

Installation

npm install downshift

useSelect Hook

import { useSelect } from "downshift";

interface Option { value: string; label: string; }

const options: Option[] = [
  { value: "react", label: "React" },
  { value: "vue", label: "Vue" },
  { value: "svelte", label: "Svelte" },
];

export function CustomSelect() {
  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    highlightedIndex,
    getItemProps,
  } = useSelect({
    items: options,
    itemToString: (item) => item?.label ?? "",
    onSelectedItemChange: ({ selectedItem }) => {
      console.log("Selected:", selectedItem?.value);
    },
  });

  return (
    <div style={{ position: "relative" }}>
      <label {...getLabelProps()}>Framework</label>
      <button
        {...getToggleButtonProps()}
        style={{
          width: 200,
          padding: "8px 12px",
          background: "#1a1a1a",
          border: "1px solid #333",
          borderRadius: 6,
          color: "#e5e7eb",
          cursor: "pointer",
          display: "flex",
          justifyContent: "space-between",
        }}
      >
        {selectedItem?.label ?? "Select framework..."}
        <span>{isOpen ? "▲" : "▼"}</span>
      </button>

      <ul
        {...getMenuProps()}
        style={{
          display: isOpen ? "block" : "none",
          position: "absolute",
          top: "100%",
          width: "100%",
          background: "#1a1a1a",
          border: "1px solid #333",
          borderRadius: 6,
          listStyle: "none",
          padding: 4,
          margin: 0,
          zIndex: 100,
        }}
      >
        {isOpen &&
          options.map((option, index) => (
            <li
              key={option.value}
              {...getItemProps({ item: option, index })}
              style={{
                padding: "8px 12px",
                borderRadius: 4,
                background: highlightedIndex === index ? "#2d2d2d" : "transparent",
                cursor: "pointer",
                color: "#e5e7eb",
              }}
            >
              {option.label}
            </li>
          ))}
      </ul>
    </div>
  );
}

useCombobox Hook (Autocomplete)

import { useCombobox } from "downshift";
import { useState } from "react";

const allItems = ["TypeScript", "JavaScript", "Python", "Rust", "Go", "Swift", "Kotlin"];

export function AutocompleteInput() {
  const [inputItems, setInputItems] = useState(allItems);

  const {
    isOpen,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getInputProps,
    highlightedIndex,
    getItemProps,
    selectedItem,
  } = useCombobox({
    items: inputItems,
    onInputValueChange: ({ inputValue }) => {
      setInputItems(
        allItems.filter((item) =>
          item.toLowerCase().startsWith((inputValue ?? "").toLowerCase())
        )
      );
    },
  });

  return (
    <div>
      <label {...getLabelProps()}>Language</label>
      <div style={{ display: "flex" }}>
        <input
          {...getInputProps()}
          style={{
            padding: "8px 12px",
            background: "#1a1a1a",
            border: "1px solid #333",
            borderRadius: 6,
            color: "#e5e7eb",
            width: 200,
          }}
        />
        <button
          {...getToggleButtonProps()}
          aria-label="toggle menu"
          style={{ marginLeft: 4, padding: "8px 12px", cursor: "pointer" }}
        >
          {isOpen ? "▲" : "▼"}
        </button>
      </div>

      <ul
        {...getMenuProps()}
        style={{
          display: isOpen && inputItems.length ? "block" : "none",
          position: "absolute",
          background: "#1a1a1a",
          border: "1px solid #333",
          borderRadius: 6,
          listStyle: "none",
          padding: 4,
          margin: 0,
          zIndex: 100,
          width: 200,
        }}
      >
        {isOpen &&
          inputItems.map((item, index) => (
            <li
              key={item}
              {...getItemProps({ item, index })}
              style={{
                padding: "8px 12px",
                background: highlightedIndex === index ? "#2d2d2d" : "transparent",
                fontWeight: selectedItem === item ? "bold" : "normal",
                cursor: "pointer",
                color: "#e5e7eb",
                borderRadius: 4,
              }}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  );
}

useMultipleSelection Hook

import { useMultipleSelection, useCombobox } from "downshift";

const allTags = ["react", "typescript", "node", "postgres", "redis", "docker", "kubernetes"];

export function MultiTagInput() {
  const [inputValue, setInputValue] = useState("");
  const availableTags = allTags.filter((tag) => !selectedItems.includes(tag));

  const {
    getSelectedItemProps,
    getDropdownProps,
    addSelectedItem,
    removeSelectedItem,
    selectedItems,
  } = useMultipleSelection<string>({ initialSelectedItems: [] });

  const filteredTags = availableTags.filter((tag) =>
    tag.toLowerCase().includes(inputValue.toLowerCase())
  );

  const { isOpen, getMenuProps, getInputProps, getItemProps, highlightedIndex } =
    useCombobox({
      inputValue,
      items: filteredTags,
      onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) {
        if (
          type === useCombobox.stateChangeTypes.InputKeyDownEnter ||
          type === useCombobox.stateChangeTypes.ItemClick
        ) {
          if (newSelectedItem) {
            addSelectedItem(newSelectedItem);
            setInputValue("");
          }
        }
        if (type === useCombobox.stateChangeTypes.InputChange) {
          setInputValue(newInputValue ?? "");
        }
      },
    });

  return (
    <div style={{ border: "1px solid #333", borderRadius: 8, padding: 8, background: "#1a1a1a" }}>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 4 }}>
        {selectedItems.map((item, index) => (
          <span
            key={item}
            {...getSelectedItemProps({ selectedItem: item, index })}
            style={{
              background: "#2d2d2d",
              color: "#e5e7eb",
              padding: "2px 8px",
              borderRadius: 4,
              display: "flex",
              alignItems: "center",
              gap: 4,
              cursor: "default",
            }}
          >
            {item}
            <button
              onClick={() => removeSelectedItem(item)}
              style={{ background: "none", border: "none", cursor: "pointer", color: "#9ca3af" }}
            >
              ×
            </button>
          </span>
        ))}
        <input
          {...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
          placeholder="Add tags..."
          style={{ background: "transparent", border: "none", color: "#e5e7eb", outline: "none" }}
        />
      </div>

      <ul
        {...getMenuProps()}
        style={{
          display: isOpen && filteredTags.length ? "block" : "none",
          listStyle: "none",
          padding: 4,
          margin: 0,
        }}
      >
        {isOpen &&
          filteredTags.map((item, index) => (
            <li
              key={item}
              {...getItemProps({ item, index })}
              style={{
                padding: "6px 8px",
                background: highlightedIndex === index ? "#2d2d2d" : "transparent",
                borderRadius: 4,
                cursor: "pointer",
                color: "#e5e7eb",
              }}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  );
}

import { useState } from "react";

Feature Comparison

Featurereact-selectcmdkDownshift
Styled✅ Out of boxNo (bring CSS)❌ Headless
Multi-selectisMultiNouseMultipleSelection
Async loadingAsyncSelectNo (custom)Custom
Creatable optionsCreatableSelectVia handlerCustom
Fuzzy searchBasic (startsWith)✅ Built-inCustom
Command paletteNo✅ Purpose-builtCan build
Keyboard nav✅ WAI-ARIA 1.2
Screen reader✅ Best-in-class
Grouped options✅ Groups✅ Custom
Virtual scrollNoNoCustom
Bundle size~20kB~2kB~7kB
npm weekly10M3M3M
GitHub stars27k10k12k
React required✅ (React only)
TypeScript

Accessibility Deep Dive and Screen Reader Compatibility

Accessible select and combobox components are among the hardest UI primitives to implement correctly because the WAI-ARIA specification for combobox changed substantially between ARIA 1.1 and ARIA 1.2, and many implementations still follow the older pattern. Downshift implements the ARIA 1.2 combobox pattern, which is the current correct approach — it manages aria-expanded, aria-autocomplete, aria-activedescendant, and role="combobox" on the input element and role="listbox" on the dropdown. react-select uses a custom accessibility implementation that works well with NVDA on Windows and VoiceOver on macOS but has had historical issues with some combinations of screen reader and browser. cmdk uses the ARIA combobox pattern with role="combobox" on the input and role="option" on items, which means standard screen reader announcement patterns apply. Testing with actual screen readers — not just axe-core — is essential before shipping any custom select or combobox to production; the specification is complex enough that automated tools miss important behavioral requirements.

Performance at Scale: Virtual Scrolling and Large Option Lists

All three libraries struggle with option lists exceeding several thousand items because they render all options into the DOM simultaneously. react-select has no built-in virtualization — rendering 10,000 options creates 10,000 DOM nodes and scrolling becomes janky. The community-maintained react-window-select package wraps react-select with react-window for virtualization, but requires additional setup. Downshift is headless and leaves virtualization entirely to you — pair it with TanStack Virtual or react-window for lists over 1,000 items. cmdk handles medium-sized lists well because its filtering reduces visible items quickly, but if your command palette has 5,000+ commands, implementing virtual scrolling via TanStack Virtual inside the Command.List container is necessary. For async search scenarios (API-backed lookups), none of this matters — you debounce the input and only show 10-50 results at a time, keeping DOM footprint minimal regardless of the total dataset size.

TypeScript Integration and Generic Types

react-select is strongly typed through its generics — Select<Option, IsMulti, GroupBase> lets you specify the option type, whether multi-select is enabled, and the group structure. The type inference can be verbose, particularly when using StylesConfig or formatOptionLabel, where you must explicitly pass the generic parameters to avoid any escaping. Downshift's hook-based API is also generically typed: useSelect<Option> ensures all returned functions and state are typed to your option type. cmdk's Command.Item renders children without a typed option model, which is consistent with its design for arbitrary command actions rather than data-driven option lists. For projects using strict TypeScript, Downshift provides the best type coverage for custom data models, while react-select is satisfactory for standard option shapes. Avoid using react-select's any escape hatches in formatOptionLabel if you can express the option type explicitly.

Bundle Size Implications and Code Splitting

react-select's approximately 20KB gzipped includes Emotion for CSS-in-JS styles, which can surprise teams auditing their bundle. If your project doesn't already use Emotion, this dependency is bundled unnecessarily. The alternative is using the unstyled variant (react-select v5+) which removes the Emotion dependency and lets you style with CSS modules or Tailwind — reducing the bundle contribution significantly. cmdk at 2KB is negligible and contains no styling dependencies. Downshift at 7KB is also lean. For applications using shadcn/ui, the Command component (based on cmdk) and the Select component (based on Radix UI primitives, not Downshift) are already available — there's no reason to add react-select or Downshift if shadcn's components cover your use case. Code splitting these libraries is straightforward since they're typically used in specific feature pages; a dynamic import on the component that uses react-select keeps it out of the main bundle.

Migration from Legacy Solutions

Teams migrating from older select libraries (Select2, Chosen, jQuery UI autocomplete) to modern React options typically land on react-select as the safest migration target — it covers the same feature set with a React API and maintains the familiar dropdown UX that jQuery-era users expect. The defaultInputValue and inputValue props support both controlled and uncontrolled patterns, making incremental migration possible. Teams building new applications from scratch in 2026 typically choose based on whether they need a command palette (cmdk) or a traditional form select (react-select or Downshift). The combination that's emerged as idiomatic in shadcn/ui-based apps is: cmdk for the command palette, shadcn's Select (Radix) for simple single-select dropdowns, and react-select or custom Downshift for complex multi-select with async search. This covers the full range of select use cases without redundant dependencies.

When to Use Each

Choose react-select if:

  • Need multi-select with tag/pill UI out of the box
  • Async server-side search with debouncing
  • Creatable options ("Add new option")
  • Grouped options with headers
  • Quick implementation with built-in styling (can override)
  • Don't need command palette — need a traditional dropdown

Choose cmdk if:

  • Building a command palette or ⌘K experience
  • Fuzzy search across commands, pages, or actions
  • Application-wide keyboard-driven navigation
  • Using shadcn/ui (it's based on cmdk)
  • Want instant filter as user types across a large list of items

Choose Downshift if:

  • Building a fully custom branded select/combobox
  • Need fine-grained ARIA control
  • Component library internals (what Chakra and Mantine do)
  • Complex state (conditional disabled items, custom filtering)
  • Want zero runtime CSS dependency — pure JS behavior hooks
  • Multi-value combobox with complex tag input patterns

Methodology

Data sourced from react-select documentation (react-select.com), cmdk documentation (cmdk.paco.me), Downshift documentation (www.downshift-js.com), npm weekly download statistics as of February 2026, GitHub star counts as of February 2026, and community discussions from the React Discord and WAI-ARIA specification.


Related: Floating UI vs Tippy.js vs Radix Tooltip for positioning the dropdown overlay, or Radix UI vs Headless UI vs Ariakit accessible components for broader accessible component primitives.

See also: React vs Vue and React vs Svelte

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.