Skip to main content

Benchmarks

Full comparison of @gentleduck/primitives against Radix UI, Base UI, Ark UI, and Headless UI - bundle size, component count, features, and architecture.

gentleduck sizes come from the built dist/ output via gzip -c | wc -c. Radix sizes are verified via the bundlephobia.com API on 2026-03-22. Only verified data is included.

Run bun run benchmark in packages/duck-primitives to regenerate.


Library Overview

gentleduckRadix UIBase UI (MUI)Ark UIHeadless UI
MaintainergentleduckWorkOSMUI / GoogleChakraTailwind Labs
Total size (all)~55 KB~180 KB~45 KB~95 KB~35 KB
Component count35+28~15~30~10
Package modelSingle packagePer-component packagesSingle packageSingle packageSingle package
Styling approachZero CSS, data attributesZero CSS, data attributesZero CSS, class callbacksZero CSS, data attributesZero CSS, class callbacks
State managementReact state + contextReact state + contextReact state + contextxstate / zag.jsReact state
CompositionSlot / asChildSlot / asChildRender propsRender propsRender props
AnimationPresence primitivePresence primitiveNone built-inPresence primitiveTransition component
CalendarYes (4 systems)NoNoNoNo
Command paletteYes (cmdk-based)No (separate cmdk)NoNoNo
RTL / i18nDirection primitiveDirection primitiveRTL propLocale contextNo
React version18+18+18+18+18+
LicenseMITMITMITMITMIT

Component Coverage

Which primitives each library ships:

ComponentgentleduckRadixBase UIArk UIHeadless
AccordionYesYesYesYes-
Alert DialogYesYes-Yes-
AvatarYesYes-Yes-
CalendarYes----
CheckboxYesYesYesYes-
CollapsibleYesYesYesYes-
Color Picker---Yes-
Combobox---YesYes
CommandYes----
Context MenuYesYes-Yes-
DialogYesYesYesYesYes
Dropdown MenuYesYesYesYesYes
Hover CardYesYes-Yes-
Input OTPYes----
MenuYes-Yes-Yes
MenubarYesYes---
Navigation MenuYesYes---
PaginationYes-YesYes-
PopoverYesYesYesYesYes
ProgressYesYesYesYes-
Radio GroupYesYesYesYesYes
SelectYesYesYesYesYes
SheetYes----
SliderYesYesYesYes-
SwitchYesYesYesYesYes
Tabs-YesYesYesYes
Toast-Yes-Yes-
ToggleYesYesYesYes-
Toggle GroupYesYes-Yes-
TooltipYesYesYesYes-
Total35+28~15~30~10

Bundle Size: gentleduck vs Radix

Per-component gzipped size (verified via bundlephobia API, 15 components with data from both libraries):

Verified numbers

ComponentgentleduckRadix UIDifference
Alert Dialog1.6 KB18.6 KB92% smaller
Popover2.4 KB19.6 KB88% smaller
Tooltip3.5 KB15.5 KB77% smaller
Dialog3.1 KB10.6 KB71% smaller
Select7.5 KB23.7 KB68% smaller
Toggle0.6 KB1.7 KB65% smaller
Avatar1.1 KB2.5 KB54% smaller
Radio Group1.9 KB1.0 KBRadix is 45% smaller

All Components

Every primitive from both libraries side by side. 0 KB means the library doesn't ship that component.


Module Sizes + Total


Feature Comparison

Detailed feature breakdown

FeaturegentleduckRadixBase UIArk UIHeadless
Single packageYesNo (28 packages)YesYesYes
Slot / asChildYesYesNoNoNo
Compound componentsYesYesNoYesNo
Presence (animation)YesYesNoYesTransition
Popper (positioning)Built-inBuilt-inFloating UIFloating UIFloating UI
Focus scopeBuilt-inBuilt-inNoBuilt-inNo
Dismissable layerBuilt-inBuilt-inClick awayBuilt-inNo
Direction (RTL)PrimitivePrimitivePropContextNo
Calendar system4 (Gregorian, Islamic, Persian, Hebrew)----
Roving focusBuilt-inBuilt-inNoBuilt-inNo
CollectionBuilt-inBuilt-inNoNoNo
Tree-shakeableYesYesYesYesYes
Zero CSSYesYesYesYesYes
SSR safeYesYesYesYesYes
TypeScriptYesYesYesYesYes

Size vs features: why some primitives are larger

Smaller isn't always better. gentleduck ships more behavior per component than the minimal alternatives:

Built-in capabilityWhat it saves youSize cost
Roving focusNo manual tabIndex management or arrow-key handlers~0.4 KB
CollectionNo manual item registration or index tracking~0.3 KB
Compound componentsNo prop-drilling through SubMenu > Item > Label trees~0.2 KB
Presence (animation)No manual mount/unmount orchestration for CSS animations~0.5 KB
Dismissable layerNo manual Escape key + click-outside + nested layer handling~0.3 KB
Popper (positioning)No separate Floating UI dependency or manual positioning~1.2 KB

When a gentleduck primitive is larger than a competitor (Radio Group at 1.9 KB vs Radix at 1.0 KB), the competitor either:

  1. Delegates to separate packages. Radix splits focus, dismissal, and collection into internal packages. Each looks small, but install 5 primitives and you pay the overhead 5 times.
  2. Skips the behavior. Base UI and Headless UI omit compound components, roving focus, and presence animations. You write that code.
  3. Uses external state machines. Ark UI offloads to xstate/zag.js — ~15 KB of runtime not counted in the component size.

The fair comparison is total application cost: primitive size plus the code you write to fill the gaps.


Where each library wins

gentleduck wins on

  • Bundle size — 50-92% smaller than Radix for most components from shared internals.
  • Component count — 35+ primitives including Calendar, Command, Sheet, Input OTP that others don't ship.
  • Single package — no dependency management across 28+ separate packages.
  • Calendar — only headless library with a multi-calendar-system primitive.
  • Source ownership — primitives live in your monorepo, not in node_modules.

Radix wins on

  • Maturity — battle-tested at thousands of companies for years.
  • Ecosystem — shadcn/ui, Vercel, and many design systems run on Radix.
  • Radio Group size — 1.0 KB vs 1.9 KB (one case where Radix is lighter).
  • Tabs — gentleduck doesn't ship a Tabs primitive yet.

Base UI wins on

  • Per-component size — some components skip compound-component overhead, so they're slightly smaller.
  • No compound pattern overhead — render-prop API is lighter for basic cases.
  • Google backing — part of the MUI ecosystem.

Ark UI wins on

  • State machines — xstate/zag.js for deterministic transitions in complex interactions.
  • Color Picker — ships a color picker primitive others lack.
  • Combobox — a dedicated combobox primitive (gentleduck uses Command).

Headless UI wins on

  • Smallest total size — ~35 KB for the whole library, with only 10 components.
  • Tailwind integration — class-based API designed for Tailwind CSS.

Why gentleduck is smaller than Radix

Radix ships each primitive as a separate npm package, each bundling its own copy of the shared internals:

Radix internal packageSizeDuplicated per primitive
@radix-ui/react-slot~0.8 KBYes
@radix-ui/react-primitive~0.3 KBYes
@radix-ui/react-compose-refs~0.3 KBYes
@radix-ui/react-context~0.4 KBYes
@radix-ui/react-use-callback-ref~0.2 KBYes
@radix-ui/react-dismissable-layer~1.5 KBYes
@radix-ui/react-focus-scope~1.5 KBYes
Total overhead per primitive~5 KB

Install 5 Radix primitives — ~25 KB of duplicated internals. With gentleduck, the shared internals load once (~5 KB) and each additional primitive only adds its own code.


Methodology

  • gentleduck: sizes from packages/duck-primitives/dist/ — concatenate all .js files per primitive directory, then gzip -c | wc -c.
  • Radix UI: sizes from the bundlephobia.com API, queried per-package on 2026-03-22. Only verified responses included.
  • Base UI / Ark UI / Headless UI: component counts and feature data from the official docs. Bundle sizes excluded where bundlephobia data was unavailable.
  • Sizes are minified + gzipped.
  • Built with tsdown (minify: true, treeshake: true, format: 'esm').

Regenerate:

cd packages/duck-primitives
bun run benchmark
cd packages/duck-primitives
bun run benchmark

Output goes to public/benchmarks/ and apps/duck-ui-docs/public/data/benchmarks/primitives.json.