# @robonen/primitives vs reka-ui v2.9.7 — full comparison > Generated by a 50-primitive fan-out: each primitive compared file-by-file against its reka-ui analogue, every claimed gap re-checked by an independent adversarial verifier. ## Headline - **50 primitives** compared (every exported primitive; `command` has no reka analogue and was compared vs Combobox/Listbox). - **0 primitives are strictly better than reka today.** Verdicts: **11 reka-better**, **39 robonen-better-with-gaps**, **0 robonen-strictly-better**. - Across all primitives the comparison raised **463 gaps**; the adversarial verifier **confirmed 430, downgraded 30 to partial, refuted 3** (~93% confirmed, 0.6% false-positive). - So robonen is competitive/close everywhere but **reka currently wins or ties on features in every single primitive** — to hit "better in everything" each list below must be cleared. **3 refuted false-positives (ignore these):** dialog content-ref forwarding (robonen has it via `useForwardExpose`); popper `SIDE_OPTIONS`/`ALIGN_OPTIONS` runtime export (reka doesn't export them either); popper imperative `update` (reka destructures but never uses it). ## Ranking — most work needed first | Primitive | Verdict | Gaps (C/M/m) | Verifier (conf/part/ref) | robonen wins | |---|---|---|---|---| | **toast** | reka better | 3/9/7 | 19/0/0 | 4 | | **date-picker** | reka better | 3/5/4 | 12/0/0 | 6 | | **select** | reka better | 2/9/6 | 17/0/0 | 3 | | **menu** | reka better | 2/8/8 | 18/0/0 | 4 | | **tabs** | reka better | 2/4/6 | 11/1/0 | 5 | | **toolbar** | reka better | 1/5/7 | 12/1/0 | 4 | | **checkbox** | reka better | 1/4/6 | 11/0/0 | 3 | | **toggle-group** | reka better | 1/4/6 | 11/0/0 | 4 | | **number-field** | reka better | 0/11/6 | 15/2/0 | 5 | | **radio-group** | reka better | 0/7/9 | 15/1/0 | 5 | | **aspect-ratio** | reka better | 0/4/2 | 6/0/0 | 3 | | **scroll-area** | better with gaps | 2/3/7 | 12/0/0 | 7 | | **dropdown-menu** | better with gaps | 1/5/6 | 11/1/0 | 3 | | **tree** | better with gaps | 1/5/6 | 11/1/0 | 7 | | **popover** | better with gaps | 1/4/3 | 8/0/0 | 6 | | **menubar** | better with gaps | 1/3/7 | 10/1/0 | 5 | | **accordion** | better with gaps | 1/3/6 | 9/1/0 | 5 | | **editable** | better with gaps | 1/3/4 | 7/1/0 | 6 | | **tooltip** | better with gaps | 1/3/2 | 6/0/0 | 5 | | **pin-input** | better with gaps | 1/2/10 | 12/1/0 | 6 | | **collapsible** | better with gaps | 1/2/5 | 8/0/0 | 5 | | **primitive** | better with gaps | 1/2/5 | 6/2/0 | 5 | | **hover-card** | better with gaps | 1/2/4 | 6/1/0 | 7 | | **tags-input** | better with gaps | 1/2/3 | 6/0/0 | 6 | | **roving-focus** | better with gaps | 1/1/1 | 3/0/0 | 5 | | **collection** | better with gaps | 1/0/1 | 2/0/0 | 5 | | **dismissable-layer** | better with gaps | 0/7/4 | 11/0/0 | 5 | | **navigation-menu** | better with gaps | 0/7/4 | 11/0/0 | 6 | | **listbox** | better with gaps | 0/6/4 | 10/0/0 | 7 | | **calendar** | better with gaps | 0/5/8 | 13/0/0 | 7 | | **dialog** | better with gaps | 0/5/6 | 9/1/1 | 6 | | **combobox** | better with gaps | 0/4/14 | 16/2/0 | 8 | | **context-menu** | better with gaps | 0/4/10 | 12/2/0 | 4 | | **slider** | better with gaps | 0/4/5 | 6/3/0 | 7 | | **stepper** | better with gaps | 0/4/4 | 7/1/0 | 6 | | **config-provider** | better with gaps | 0/4/3 | 7/0/0 | 6 | | **avatar** | better with gaps | 0/3/5 | 8/0/0 | 6 | | **visually-hidden** | better with gaps | 0/3/5 | 8/0/0 | 4 | | **alert-dialog** | better with gaps | 0/2/5 | 7/0/0 | 4 | | **toggle** | better with gaps | 0/2/4 | 6/0/0 | 5 | | **focus-scope** | better with gaps | 0/2/2 | 4/0/0 | 4 | | **progress** | better with gaps | 0/2/2 | 4/0/0 | 5 | | **command** | better with gaps | 0/1/12 | 12/1/0 | 6 | | **popper** | better with gaps | 0/1/6 | 4/1/2 | 6 | | **label** | better with gaps | 0/1/4 | 4/1/0 | 4 | | **separator** | better with gaps | 0/0/5 | 4/1/0 | 4 | | **pagination** | better with gaps | 0/0/4 | 3/1/0 | 6 | | **teleport** | better with gaps | 0/0/2 | 0/2/0 | 6 | | **presence** | better with gaps | 0/0/0 | 0/0/0 | 5 | | **switch** | better with gaps | 0/0/0 | 0/0/0 | 5 | --- ## Per-primitive detail ### toast — _reka-better_ **Parts reka has that robonen lacks:** ToastPortal, ToastRootImpl (impl split from ToastRoot), ToastAnnounce (visually-hidden live region), ToastAnnounceExclude, FocusProxy (head/tail focus sentinels) robonen's toast is a clean but heavily reduced reimplementation of Radix/reka toast. It correctly covers the basics — provider/viewport/root/title/description/action/close parts, polite/assertive aria-live, a label region, hotkey-to-focus (with two genuinely better bug fixes vs reka: empty-hotkey guard and reactive-duration timer restart, both regression-tested), and pause/resume on hover/focus. However it is missing most of the hard parts that make a toast accessible and production-grade: there is NO swipe-to-dismiss at all (the swipeDirection/swipeThreshold props are declared but never used — dead API), NO dedicated screen-reader announce region (ToastAnnounce/ToastAnnounceExclude with deferred double-rAF text harvesting), NO focus-order management or FocusProxy guards for the portaled viewport, NO Teleport/Collection so toasts can't be authored outside the viewport, NO focus return on close, NO elapsed-time-preserving pause (it restarts the full duration so hovering resets the countdown), NO remaining/duration slot props, NO forceMount/defaultOpen, NO DismissableLayer integration, NO window blur/focus pause, and a weaker Escape story (escapeKeyDown is emitted but never closes, and isFocusedToastEscapeKeyDownRef is dead). reka also has ToastPortal, type='button' on close, label {hotkey} interpolation, and author-error validation. Net: reka is currently materially better on accessibility, keyboard/focus, and features. The good news is robonen already ships the supporting primitives (collection, dismissable-layer, visually-hidden, focus-scope, teleport, presence), so every gap is implementable. **Critical gaps:** - ⛔ (features) No swipe-to-dismiss gesture support at all. reka implements full pointer swipe handling on the toast: pointerdown/pointermove/pointerup with direction clamping, threshold detection, pointer capture, and emits swipeStart/swipeMove/swipeCancel/swipeEnd, plus sets data-swipe and --reka-toast-swipe-move-x/y / --reka-toast-swipe-end-x/y CSS vars for animation. - _evidence:_ reka ToastRootImpl.vue pointerdown/pointermove/pointerup handlers (lines 218-280) + ToastRoot.vue swipe emit wrappers (lines 69-108) + utils.ts isDeltaInDirection/handleAndDispatchCustomEvent/SwipeEvent. robonen declares swipeDirection/swipeThreshold in ToastProvider but NEVER consumes them — there is zero pointer/swipe handling anywhere in robonen/toast. The props are dead. - ⛔ (accessibility) No screen-reader announce region. reka renders a separate visually-hidden ToastAnnounce live region with double-rAF deferred text injection (to force NVDA to announce) and harvests toast text via getAnnounceTextContent, honoring aria-hidden/hidden/display:none and ToastAnnounceExclude. - _evidence:_ reka ToastAnnounce.vue (double requestAnimationFrame + useTimeout(1000)), ToastRootImpl.vue lines 120,181-200, utils.ts getAnnounceTextContent. robonen instead puts role=status + aria-live directly on the visible toast li (ToastRoot.vue lines 129-131) with no deferred-render trick and no separate hidden announce node, so NVDA/VoiceOver announcement reliability is worse and there is no text-harvesting for SR. - ⛔ (keyboard) No focus-order management / tab reversal across toasts and no FocusProxy guards. reka programmatically reverses tab order (newest-to-oldest), uses head/tail FocusProxy VisuallyHidden sentinels to proxy focus out of the portaled viewport back into document order, and on Tab navigates between toast tabbable candidates. - _evidence:_ reka ToastViewport.vue handleKeyDown (lines 97-128), getSortedTabbableCandidates (lines 150-161), FocusProxy.vue, focusFirst/getTabbableCandidates imports. robonen ToastViewport.vue has NO keydown/Tab handling and no focus proxies at all; tabbing through portaled toasts will follow DOM order and can escape the viewport unexpectedly. **Major gaps:** - 🔶 (accessibility) No ToastAnnounceExclude part and no altText harvesting on ToastAction. reka's ToastAction wraps ToastClose in ToastAnnounceExclude so the action's altText is read by SR (and the button's own visible text is excluded). robonen's ToastAction only sets aria-label=altText on the button and provides no exclude mechanism, so action content cannot be excluded from / substituted in the SR announcement. - 🔶 (features) Toast is not teleported into the viewport and there is no Collection registration. reka teleports each ToastRootImpl into providerContext.viewport via and wraps it in CollectionItem/CollectionSlot so toasts authored anywhere in the tree render inside the viewport and are enumerable for focus ordering. - 🔶 (accessibility) On programmatic/keyboard close, focus is not returned to the viewport. reka's handleClose focuses the viewport when the toast being closed contained focus (so SR users keep context and focus isn't lost), and resets isClosePausedRef. - 🔶 (api-surface) No remaining-time / duration slot props. reka exposes a default slot with { open, remaining, duration } driven by a useRafFn countdown, enabling progress bars/countdowns. robonen's ToastRoot default slot is empty with no scope. - 🔶 (api-surface) No forceMount prop and no defaultOpen prop. reka ToastRoot supports forceMount (force render for animation libs) and defaultOpen (uncontrolled initial state distinct from the v-model). robonen only has defineModel('open', {default:true}) with no forceMount and no separate defaultOpen. - 🔶 (features) No DismissableLayer integration on the viewport. reka wraps the viewport in DismissableLayerBranch so toasts participate in the global dismissable-layer stack (correct Escape/outside-interaction layering with dialogs/popovers). robonen's viewport is a plain Primitive region with no layer participation. - 🔶 (edge-cases) No window blur/focus pause-resume and no pointermove-based pause. reka pauses the dismiss timer on window blur and resumes on window focus, and uses pointermove (not just pointerenter) plus pointerleave-with-active-element checks. robonen only pauses on pointerenter/focusin and resumes on pointerleave/focusout. - 🔶 (edge-cases) Pause does not preserve elapsed/remaining time; timer restarts from full duration on resume. reka tracks closeTimerStartTimeRef/closeTimerRemainingTimeRef and on resume continues from the remaining time. robonen's resumeTimer calls startTimer(full duration) again, so hovering repeatedly can keep a toast alive far longer than its duration and the countdown effectively resets. - 🔶 (keyboard) Escape handling differs and is weaker. reka uses onKeyStroke('Escape') globally on the root, sets isFocusedToastEscapeKeyDownRef, and closes unless event.defaultPrevented. robonen only emits escapeKeyDown on @keydown.escape bound to the toast element (must be focused), never closes on Escape, and never sets isFocusedToastEscapeKeyDownRef (the provider ref exists but is dead). **Minor gaps (7):** - ▫️ (api-surface) No disableSwipe prop. reka ToastProvider exposes disableSwipe to turn off swipe gestures (and adjusts userSelect/touchAction styles). robonen has no equivalent (and no swipe at all). - ▫️ (accessibility) ToastViewport label loses the {hotkey} placeholder feature and the function-form label. reka label supports '{hotkey}' interpolation (auto-replaced, e.g. 'Notifications (F8)') and a (hotkey)=>string function form, improving SR landmark context. robonen viewport label is a plain string with no hotkey interpolation. - ▫️ (api-surface) No ToastPortal part. reka exports a ToastPortal (TeleportPrimitive wrapper) for placing the viewport in an arbitrary container. robonen exports no Portal. - ▫️ (code-quality) ToastProvider does not validate a non-empty label and does not validate type. reka throws on empty/whitespace label and on invalid type values, catching author mistakes early. robonen performs no such validation. - ▫️ (forms) ToastClose does not set type='button'. reka's ToastClose sets type='button' when as==='button' to avoid implicit form submit. robonen ToastClose renders a button with no explicit type. - ▫️ (accessibility) ToastRoot default type differs and may be less appropriate. reka defaults type='foreground' (ToastRoot.vue line 28) for user-action toasts (assertive). robonen defaults type='background' (ToastRoot.vue line 32, polite). A behavioral/aria-live divergence worth a deliberate decision. - ▫️ (code-quality) Components do not set inheritAttrs:false where reka does, which blocks precise attribute placement around wrappers (FocusProxy/DismissableLayerBranch/Teleport). reka uses defineOptions({inheritAttrs:false}) + v-bind="$attrs". **Recommendations to make robonen strictly better:** - Implement swipe-to-dismiss: port reka's pointerdown/pointermove/pointerup logic and isDeltaInDirection/handleAndDispatchCustomEvent into ToastRoot, add swipeStart/swipeMove/swipeCancel/swipeEnd emits, set data-swipe + --primitives-toast-swipe-move-x/y / -end-x/y CSS vars, and actually consume the existing swipeDirection/swipeThreshold provider props (currently dead). Add a disableSwipe provider prop. - Add a dedicated screen-reader announce region: create a ToastAnnounce visually-hidden component (robonen already ships a visually-hidden primitive) that defers text injection with double requestAnimationFrame, and a getAnnounceTextContent util that walks the toast harvesting text while honoring aria-hidden/hidden/display:none and a new ToastAnnounceExclude (data-primitives-toast-announce-exclude / -alt). Move aria-live off the visible li into this hidden region. - Add ToastAnnounceExclude and wire ToastAction to wrap its button in it (passing altText) so SR reads altText instead of the button label, matching reka. - Teleport each ToastRoot into the provider viewport and integrate the existing collection primitive (useCollection isProvider on provider, CollectionItem on root, CollectionSlot on viewport) so toasts can be authored anywhere and are enumerable. - Implement viewport focus management: add a keydown Tab handler that reverses tab order newest-to-oldest using collection items + getTabbableCandidates, and add head/tail FocusProxy sentinels (VisuallyHidden tabindex=0) to proxy focus out of the portaled viewport. Export focusFirst/getTabbableCandidates from focus-scope for reuse. - On keyboard close, focus the viewport when the closing toast contained focus, and reset isClosePausedRef (currently isFocusedToastEscapeKeyDownRef and the focus-return behavior are missing). - Preserve elapsed time across pause/resume: track closeTimerStartTime and closeTimerRemainingTime and resume from remaining (not full duration). Add a useRafFn-based remaining countdown and expose { open, remaining, duration } as ToastRoot slot props. - Add forceMount and defaultOpen props to ToastRoot (Presence :present="forceMount || open"; defaultOpen seeds the uncontrolled model). - Wrap ToastViewport in a DismissableLayerBranch (add a Branch export to the dismissable-layer primitive) so toasts join the dismissable-layer stack; add window blur/focus pause-resume and pointermove-based pause to the viewport. - Implement global Escape handling on ToastRoot via a keydown listener: emit escapeKeyDown, set isFocusedToastEscapeKeyDownRef, and close unless defaultPrevented. - Add a ToastPortal part (Teleport wrapper) and support {hotkey} placeholder + function-form label on ToastViewport. - Add author-error validation (non-empty label in ToastProvider, valid type in ToastRoot), set type='button' on ToastClose when as==='button', and reconsider defaulting ToastRoot type to 'foreground' to match the common user-action semantics. **Where robonen is already better:** - ✅ ToastViewport hotkey matching is more correct than reka's: robonen guards against an empty hotkey array (ToastViewport.vue line 47, `if (!hotkey || hotkey.length === 0) return`) and supports modifier-key tokens ('altKey'/'ctrlKey'/'shiftKey'/'metaKey') in the hotkey array. reka's `onKeyStroke(hotkey.value, ...)` (ToastViewport.vue line 55) is registered once with the initial value (not reactive to hotkey changes) and an empty array would match every keystroke. robonen has explicit regression tests for both bugs. - ✅ ToastRoot restarts the auto-dismiss timer reactively when `duration` (root or provider) changes while open and not paused (ToastRoot.vue watch lines 83-91), backed by a dedicated regression test. reka resets closeTimerRemainingTimeRef on duration change (ToastRootImpl.vue lines 154-160) but robonen's restart behavior here is explicitly tested. - ✅ robonen normalizes the disable case more defensively in startTimer: `if (ms === Infinity || ms <= 0 || !Number.isFinite(ms)) return` (ToastRoot.vue line 57) also handles NaN, whereas reka only checks `duration <= 0 || duration === Number.POSITIVE_INFINITY` (ToastRootImpl.vue line 90). - ✅ robonen's context layer uses a typed useContextFactory and shallowRef for the viewport element (ToastProvider.vue line 34), marginally more efficient for a DOM-node ref than reka's plain ref (ToastProvider.vue line 71). ### date-picker — _reka-better_ **Parts reka has that robonen lacks:** DatePickerField backed by a true segmented DateFieldRoot (role=group, segments slot, VisuallyHidden focusable form input), DatePickerInput as an individual editable segment part (role=spinbutton, part: SegmentPart) — robonen's DatePickerInput is just an alias of the plain DatePickerField **Parts robonen has that reka lacks:** DatePickerCalendar wrapper component (reka has DatePickerCalendar too, but robonen's is a thin Primitive div; functionally parity), DatePickerField with editable/format/placeholderText plain-input ergonomics (no reka equivalent because reka is always segmented), valueFormat serializer on Root Both libraries structure DatePicker as a composition over Popover + Calendar + a Field, and robonen has real parity (or better) on the calendar grid: CalendarCellTrigger.vue implements full RTL-aware Arrow/Home/End/PageUp/PageDown(+Shift for year)/Enter/Space keyboard nav, roving tabindex, data-* states, initialFocus, and aria-selected/aria-disabled — matching reka's CalendarCellTrigger. robonen also wins on smaller surface (native Date, no @internationalized/date dependency), a valueFormat hidden-input serializer, and a friendlier closeOnSelect=true default. However, the verdict is reka-better because reka's DatePicker centers on a segmented, editable DateField (DateFieldRoot/DateFieldInput) that robonen completely lacks: robonen's DatePickerField is a single plain doing new Date(text) parsing. That single architectural gap cascades into many missing features: per-segment spinbutton keyboard editing (Arrow increment, numeric type-ahead with auto-advance, Backspace, a/p for AM/PM), RTL-aware inter-segment Left/Right navigation, time support (granularity/hourCycle/step/hideTimeZone), segment-level ARIA (role=group, role=spinbutton, aria-valuemin/max/now/valuetext). Additionally reka has preventDeselect toggle-off, a focusable VisuallyHidden native input enabling real form constraint validation (required/min/max) where robonen uses a non-validatable display:none input, required/id forwarding, a trigger that honors disabled (robonen's does not), and DatePickerContent that auto-portals with a portal prop (robonen requires manual DatePickerPortal). To become strictly better, robonen must implement a segmented DateField with full segment keyboard + ARIA, add time/granularity support, preventDeselect, a focusable hidden form input with required, disabled trigger forwarding, and content auto-portalling — then it would surpass reka given its lighter dependency profile and better defaults. Relevant files: /Users/robonen/Projects/tools/vue/primitives/src/date-picker/DatePickerRoot.vue, /Users/robonen/Projects/tools/vue/primitives/src/date-picker/DatePickerField.vue, /Users/robonen/Projects/tools/vue/primitives/src/date-picker/DatePickerTrigger.vue, /Users/robonen/Projects/tools/vue/primitives/src/date-picker/DatePickerContent.vue, /Users/robonen/Projects/tools/vue/primitives/src/calendar/CalendarCellTrigger.vue; reka: /Users/robonen/Projects/external/reka-ui/packages/core/src/DatePicker/DatePickerRoot.vue, /Users/robonen/Projects/external/reka-ui/packages/core/src/DateField/DateFieldRoot.vue, /Users/robonen/Projects/external/reka-ui/packages/core/src/DateField/DateFieldInput.vue, /Users/robonen/Projects/external/reka-ui/packages/core/src/shared/date/useDateField.ts, /Users/robonen/Projects/external/reka-ui/packages/core/src/DatePicker/DatePickerContent.vue. **Critical gaps:** - ⛔ (features) Segmented, editable date field. reka's DatePickerField renders a DateFieldRoot (role=group) whose DatePickerInput parts (DateFieldInput.vue, part: SegmentPart) are individually-focusable contenteditable segments (day/month/year/hour/minute/second/dayPeriod/literal) with role=spinbutton. robonen's DatePickerField is a single plain that does new Date(text) parsing (DatePickerField.vue:45-47). robonen has no per-segment editing, no SegmentPart model, no segments slot, no DateFieldRoot/DateFieldInput. - _evidence:_ reka DateFieldRoot.vue (segmentValues/segmentContents, role=group) + DateFieldInput.vue (data-reka-date-field-segment, role=spinbutton via useDateField commonSegmentAttrs); robonen DatePickerField.vue is a single with no segment support. - ⛔ (keyboard) Per-segment keyboard interaction. reka's useDateField.ts handles ArrowUp/ArrowDown to increment/decrement each segment, numeric type-ahead with auto-advance to next segment (focusNext), Backspace to clear, and 'a'/'p' to set AM/PM (handleDayPeriodSegmentKeydown:846-871). robonen's field only handles Enter to commit a full parsed string (DatePickerField.vue:52-54) — no arrow increment, no type-ahead, no backspace-per-segment, no AM/PM keys. - _evidence:_ reka useDateField.ts handleDay/Month/Year/Hour/Minute/Second/DayPeriodSegmentKeydown (lines 635-871) vs robonen DatePickerField.vue handleKeydown only checking e.key === 'Enter'. - ⛔ (features) Time / granularity support. reka exposes granularity ('day'|'hour'|'minute'|'second'), hourCycle (12/24), step (DateStep), and hideTimeZone on DatePickerRootProps (via DateFieldRootProps) and renders time segments accordingly (DatePickerRoot.vue:104-112 destructures granularity/hideTimeZone/hourCycle/step; DatePickerGranular.story.vue). robonen's DatePickerRoot has NO granularity/hourCycle/step/hideTimeZone props — it is date-only (toDateOnly strips time everywhere, DatePickerRoot.vue:105,183,198). - _evidence:_ reka DatePickerRoot.vue context fields hourCycle/granularity/hideTimeZone/step (lines 21-23,44) + DateFieldRoot.vue inferredGranularity/hasTime; robonen DatePickerRoot.vue exposes no time props and calls toDateOnly throughout. **Major gaps:** - 🔶 (rtl-i18n) RTL-aware segment navigation with ArrowLeft/ArrowRight between field segments. reka DateFieldRoot.vue handleKeydown (253-260) moves focus to prev/next focusable segment, with sign flipped for dir==='rtl' (nextFocusableSegment/prevFocusableSegment:227-244). robonen's plain field has no inter-segment Left/Right navigation at all (single input). (robonen's calendar grid is RTL-aware, but the field is not.) - 🔶 (features) preventDeselect / toggle-off behavior. reka's onDateChange (DatePickerRoot.vue:180-190) deselects (sets modelValue=undefined) when the already-selected date is clicked again unless preventDeselect is set; preventDeselect is a real prop. robonen's setDate (DatePickerRoot.vue:180-185) always sets the date and never deselects on re-click, and there is no preventDeselect prop. - 🔶 (forms) Form constraint validation via a focusable native input. reka renders a VisuallyHidden as="input" with feature="focusable", real type (date/datetime-local), :value, :min, :max, :required (DateFieldRoot.vue:310-323), so browser-level required/min/max validation and form participation work, and focusing it forwards focus to the first segment. robonen's hidden input is type=hidden with style="display:none" (DatePickerRoot.vue:322-331) — no required, no min/max, not focusable, and display:none inputs are excluded from constraint validation. - 🔶 (edge-cases) Trigger does not honor disabled. reka DatePickerTrigger binds :disabled="rootContext.disabled.value" (DatePickerTrigger.vue:20) so the open button is disabled with the picker. robonen DatePickerTrigger.vue never reads ctx disabled — the trigger stays clickable and ctx.onOpenToggle fires even when the picker is disabled (no disabled binding, no guard). - 🔶 (accessibility) Field-level ARIA group/state semantics. reka DateFieldRoot sets role="group", aria-disabled, data-readonly, data-invalid (DateFieldRoot.vue:294-301) and each segment carries role=spinbutton, aria-valuemin/max/now/valuetext and aria-label (useDateField commonSegmentAttrs + per-part attrs:48-189). robonen's field is a bare with only data-primitives-date-picker-field and no aria-invalid/aria-disabled/group semantics. **Minor gaps (4):** - ▫️ (forms) required prop and id forwarding. reka DatePickerRoot forwards required and id into the field/native input (DatePickerRoot.vue:103,170,173; DateFieldRoot props required/id). robonen DatePickerRootProps has name but no required and no user-supplied id prop for the field/form control. - ▫️ (features) Content auto-portals and exposes a portal prop. reka DatePickerContent wraps children in PopoverPortal automatically and accepts portal?: PopoverPortalProps (DatePickerContent.vue:7-12,28). robonen DatePickerContent.vue does NOT portal — it renders inline; consumers must manually add DatePickerPortal around it, and there is no portal prop on Content. - ▫️ (edge-cases) closeOnSelect is reactive in reka via a watcher on modelValue (DatePickerRoot.vue:143-150) and is provided as a Ref in context, so changes apply consistently from any selection source (field or calendar). robonen only closes inside setDate (DatePickerRoot.vue:184) which is the calendar path; selecting/committing via the editable DatePickerField sets ctx.modelValue directly (DatePickerField.vue:47) and does NOT trigger closeOnSelect. - ▫️ (rtl-i18n) Locale-change segment re-ordering. reka re-collects segment elements on locale change so Left/Right order matches the new locale (DateFieldRoot.vue:192-202). robonen has no segments, so changing locale only reformats the single input string — a non-issue for plain input but a real capability gap if segmented editing is added. **Recommendations to make robonen strictly better:** - Build a segmented DateField (DateFieldRoot + DateFieldInput with a SegmentPart model) and have DatePickerField/DatePickerInput render it. Each segment should be role=spinbutton, contenteditable, with aria-valuemin/valuemax/valuenow/valuetext and aria-label, replacing the single plain in DatePickerField.vue. This is the central gap. - Add per-segment keyboard handling to the field: ArrowUp/ArrowDown to increment/decrement, numeric type-ahead with auto-advance to the next segment, Backspace to clear a segment, and 'a'/'p' for AM/PM, plus RTL-aware ArrowLeft/ArrowRight to move between segments (mirror reka useDateField.ts and DateFieldRoot.vue handleKeydown). - Add time support: granularity ('day'|'hour'|'minute'|'second'), hourCycle (12/24), step, and hideTimeZone props on DatePickerRoot; stop unconditionally calling toDateOnly so time-of-day can be preserved and rendered as hour/minute/second/dayPeriod segments. - Add preventDeselect prop and make setDate deselect (set modelValue=undefined) when the currently-selected date is re-selected, unless preventDeselect is true (match reka onDateChange). - Replace the display:none hidden input with a focusable VisuallyHidden native input (type date/datetime-local, with :value/:min/:max/:required), so the control participates in browser constraint validation and focusing it forwards focus into the field. Add required and id props to DatePickerRootProps. - Forward root disabled to DatePickerTrigger (bind :disabled and guard onClick/onOpenToggle) so a disabled picker cannot be opened. - Make DatePickerContent auto-wrap its children in DatePickerPortal and accept a portal?: prop (like reka DatePickerContent.portal), so portalling is the default and DatePickerPortal becomes opt-out rather than required boilerplate. - Make closeOnSelect fire for ALL selection sources by watching modelValue at the Root (like reka) instead of only inside setDate, so committing via the field also closes the popover. - Add field-level ARIA: role=group, aria-disabled, aria-readonly, data-invalid/data-readonly/data-disabled on the field wrapper, and aria-invalid wiring driven by the existing isInvalid computed. **Where robonen is already better:** - ✅ Zero-dependency value model: robonen uses native JS Date throughout (DatePickerRoot.vue modelValue: Date | undefined) whereas reka hard-depends on @internationalized/date (DateValue, .compare/.copy/.set). Smaller bundle and no extra peer dep. - ✅ Custom hidden-input serialization: robonen exposes valueFormat?: 'iso' | ((d: Date) => string) (DatePickerRoot.vue:13-14, hiddenValue computed:256-260) for controlling the submitted string. reka has no equivalent serializer hook — it relies on the VisuallyHidden native input's normalized value only. - ✅ Better default UX for closeOnSelect: robonen defaults closeOnSelect = true (DatePickerRoot.vue:16,57) so the popover closes after picking a date; reka defaults closeOnSelect: false (DatePickerRoot.vue:85), requiring opt-in. - ✅ DatePickerField offers a simple plain-input mode with ergonomic props editable/format/placeholderText (DatePickerField.vue:4-11) plus Enter/blur commit — lower barrier for the common 'one text input' use case than reka's mandatory multi-segment composition. - ✅ modal popover support is wired end-to-end in robonen (DatePickerContent.vue FocusScope :trapped=ctx.modal, DismissableLayer :disable-outside-pointer-events=ctx.modal) and surfaced as a first-class DatePicker concern; reka delegates entirely to PopoverRoot. - ✅ Explicit isInvalid is computed and provided to the calendar context (DatePickerRoot.vue:175-178), giving day cells/styling access to validity, in addition to per-field checks. ### select — _reka-better_ **Parts reka has that robonen lacks:** BubbleSelect.vue (native VisuallyHidden form bubbling (only a plain hidden input, so no autofill, no multiple, no object values); ARIA is mis-wired (role=listbox on the viewport not the content, content id missing so trigger aria-controls dangles, items lack aria-labelledby/data-highlighted/select event). Minor gaps include no Tab/contextmenu prevention, no viewport nonce/scrollbar hiding, an always-rendered arrow, an unset content-available-height CSS var (broken viewport sizing), no forceMount, no SelectValue slot, and missing touch/pointer hardening. robonen's only genuine edges are a smoother rAF auto-scroll and a slightly broader set of exported per-part types. Verdict: reka-better; substantial work is required for robonen to reach, let alone exceed, parity. **Critical gaps:** - ⛔ (features) robonen only supports a single string value (export type SelectValue = string; modelValue typed string|undefined). reka supports any value type (AcceptableValue: string|number|boolean|object|null) AND multiple selection via the `multiple` prop, with array model values, by-field/by-fn object comparison, and toggle-on-reselect logic. - _evidence:_ reka SelectRoot.vue: props `multiple`, `by`, `modelValue?: T|Array`, `defaultValue?: T|Array`, handleValueChange() toggles array membership and keeps open when multiple; isEmptyModelValue computed; utils.ts valueComparator/compare. robonen context.ts `SelectValue = string`, SelectRoot has no multiple/by, handleValueChange always closes and assigns scalar. - ⛔ (edge-cases) robonen's SelectValue cannot show the initially-selected option's label until the dropdown has been opened at least once, because items only register into optionsSet when SelectItemText mounts, and the items are unmounted by `Presence :present=open` while closed. - _evidence:_ robonen SelectContent.vue wraps SelectContentImpl (items) in ``; SelectItem registers via onMounted/onItemTextChange only. reka SelectContent.vue renders a `` with SelectProvider when closed so SelectItemText still mounts in a detached DocumentFragment and registers options, so SelectValue shows the correct label on first paint. **Major gaps:** - 🔶 (accessibility) robonen never focuses the currently selected item when the listbox opens; it tracks selectedItemRef but no code calls .focus() on it after positioning. So opening a select with an existing value lands focus on the content container, not the selected option. - 🔶 (features) robonen's item-aligned positioning is a stub: it only pins content to triggerRect.bottom/left with a fixed minWidth. It omits the entire Radix MacOS-style algorithm: vertical centering of the selected item against the trigger, collision clamping to viewport (CONTENT_MARGIN), min/max height computation, dir=rtl horizontal math, expand-on-scroll, ResizeObserver reposition, and content-wrapper z-index copy. - 🔶 (keyboard) robonen offers no type-to-select on the closed trigger; pressing letter keys on the trigger does nothing. reka runs typeahead on the trigger so you can select an option without opening. - 🔶 (keyboard) robonen's typeahead matching is primitive: it always picks the first item whose text startsWith the buffer from the top of the list, with no forward-cycling from the current item, no repeated-character cycling, and no wrap-around. So pressing the same letter repeatedly cannot cycle through same-letter options. - 🔶 (accessibility) robonen does not isolate the rest of the page from assistive tech while the listbox is open (no aria-hidden on siblings) and installs no focus guards, so screen-reader users and Tab can escape past the listbox boundary inconsistently. - 🔶 (forms) robonen does not provide native form submission for arbitrary/multiple values. It renders a single hidden that stringifies value, so non-string values and multiple selections cannot be submitted, autofill cannot work, and no native change event bubbles. - 🔶 (accessibility) robonen's SelectItem does not associate the option with its label for AT and lacks a highlighted-state hook; it also has no cancelable select event or empty-value guard. - 🔶 (accessibility) robonen's listbox role wiring is on the wrong element and inconsistent with reka. Content has no role; the viewport carries role=listbox. reka puts role=listbox on the content element and role=presentation on the viewport, which is the correct mapping for option grouping/labelling. - 🔶 (api-surface) robonen's SelectValue has no slot and cannot render selected label(s) for object/multiple values; it renders a single persisted displayValue string only and has no data-placeholder attribute. **Minor gaps (6):** - ▫️ (keyboard) robonen's content keydown handler does not prevent Tab from moving focus out of the open listbox, and does not block contextmenu, so Tab can break the roving-focus model inside the listbox. - ▫️ (features) robonen's SelectViewport lacks a `nonce` prop and does not hide scrollbars / enable touch momentum scrolling, hurting CSP support and visual polish. - ▫️ (edge-cases) robonen's SelectArrow renders regardless of position mode, so an arrow can appear in item-aligned mode where it is meaningless; and SelectPopperPosition does not map popper CSS vars, so the viewport's max-height var is never set. - ▫️ (features) robonen's SelectContent has no forceMount option for externally controlled animation libraries, and does not implement the delayed-render-presence workaround for state-based exit transitions. - ▫️ (edge-cases) robonen's pointer handling lacks touch-device support and the open/select race protections reka added for issue #804: it opens on pointerdown for all pointer types and has no pointer-move-threshold guard before selecting on pointerup. - ▫️ (code-quality) robonen's data hooks differ from reka's conventional attributes and it does not use a Collection abstraction, reducing robustness of keyboard nav and scroll-into-view ordering. **Recommendations to make robonen strictly better:** - Generalize the value model: change SelectValue from `string` to an AcceptableValue union and add `multiple`, `by`, array model/defaultValue support in SelectRoot, mirroring reka's handleValueChange toggle logic and valueComparator/compare utils. Keep open on select when multiple. - Fix initial label display: render SelectContent children into a detached DocumentFragment (or forceMount) while closed, like reka's SelectProvider/Teleport branch, so SelectItemText registers options and SelectValue shows the selected label before the dropdown is ever opened. - Focus the selected item on open: add `watch(isPositioned, () => focusFirst([selectedItemRef.value, contentEl]))` in SelectContentImpl, and re-run it after the scroll-up button mounts in item-aligned mode. - Port the full Radix item-aligned positioning algorithm into SelectItemAlignedPosition (vertical centering on selected item, CONTENT_MARGIN collision clamping, min/max height, dir=rtl math, expand-on-scroll via a context, useResizeObserver, z-index copy). - Add typeahead on the closed Trigger and replace the naive startsWith matcher with a cycling matcher (wrapArray + getNextMatch: forward from current item, repeated-char cycling, single-char exclusion, wrap-around). Reuse a shared useTypeahead. - Wire ARIA correctly: put role=listbox + the content id on the content element (so trigger aria-controls resolves), set viewport role=presentation, add aria-labelledby=textId and data-highlighted to SelectItem, and add a cancelable `select` emit plus an empty-string value guard. - Add native form support via a VisuallyHidden with robonen's VisuallyHidden as=input (or a VisuallyHiddenInput-style helper) that dispatches native bubbling input+change events on value change via the native value setter, and gate it behind an SSR-safe useFormControl(currentElement) check (default true on SSR; otherwise el.closest('form')). - Add aria-label auto-derivation: accept an id prop, bind it on the root, and compute aria-label from document.querySelector(`[for=id]`).innerText when no explicit aria-label is supplied (guard for SSR). - Widen the value prop type to an AcceptableValue equivalent and serialize array/object/required-empty values in the hidden input the way reka's VisuallyHiddenInput does. - Extract isIndeterminate/getState into a checkbox utils.ts and reuse in Root + Indicator to remove the duplicated state ternaries. - Expand the default slot to expose both modelValue and state (not just checked) to match reka's richer slot contract, especially once trueValue/falseValue land. **Where robonen is already better:** - ✅ No runtime dependency on ohash isEqual for the common boolean case; robonen's toggle is a plain boolean flip, so it is lighter when only boolean checkboxes are used. - ✅ Micro-optimization: provides disabled via toRef(() => disabled) instead of a computed, and forwards the existing localChecked Ref directly to context without an extra computed wrapper (CheckboxRoot.vue lines 60-65) — slightly fewer reactive effects than reka's two computeds (disabled, checkboxState). - ✅ Hidden input is rendered with a concrete inline style that visually hides without an extra component layer; the markup is simpler than reka's VisuallyHiddenInput -> VisuallyHiddenInputBubble -> VisuallyHidden chain for the trivial single-boolean case. ### toggle-group — _reka-better_ **Parts reka has that robonen lacks:** VisuallyHiddenInput (form-submission hidden input rendered by Root), Composed RovingFocusGroup/RovingFocusItem wrappers, Composed Toggle base component for items reka's ToggleGroup is currently better. Both cover the core single/multiple toggle behavior, click/Arrow/Home/End navigation, loop, orientation, disabled, RTL-aware Left/Right, controlled+uncontrolled state, and roving tab-stop. But reka is meaningfully ahead on: (1) native form integration via a VisuallyHiddenInput + useFormControl (robonen has none, package-wide) - critical; (2) AcceptableValue (string/number/bigint/object/null) with deep equality vs robonen's string-only identity comparison; (3) automatic single/multiple type inference + dev validation vs robonen's explicit-only, unsoundly-typed `type`; (4) inherited global `dir` from ConfigProvider; and (5) far deeper roving focus (PageUp/PageDown, modifier-key guards, Shift+Tab tab-out, entry-focus prioritization, Safari mousedown, group tabindex) because reka composes its RovingFocusGroup/RovingFocusItem and Toggle primitives. Ironically robonen already ships an equivalent RovingFocusGroup/RovingFocusItem and Toggle primitive but the toggle-group reimplements a thinner version inline and uses none of them. robonen is genuinely better in two narrow areas: it uses proper radiogroup/radio ARIA semantics for single-select (reka uses group + aria-pressed even in single mode), and it adds a convenience `valueChange` emit and a normalized array slot value. Closing the form, value-type, type-inference, dir-inheritance, and roving-focus gaps (largely by composing robonen's own existing primitives) would make it strictly better. **Critical gaps:** - ⛔ (forms) No form integration whatsoever. reka renders a VisuallyHiddenInput (gated by useFormControl) with the selected value(s) when `name` is set, so the toggle group participates in native form submission (including array-name encoding name[0], name[1] and a required-empty-array fallback input). robonen's ToggleGroupRootProps has no `name`/`required` props, no hidden input, and there is no useFormControl/VisuallyHiddenInput utility anywhere in the primitives package. - _evidence:_ reka ToggleGroupRoot.vue lines 96-101 render guarded by `isFormControl = useFormControl(currentElement)`; FormFieldProps adds name/required (shared/types.ts). robonen ToggleGroupRoot.vue has no name/required props and grep for useFormControl/VisuallyHiddenInput/closest('form') across src returns nothing. **Major gaps:** - 🔶 (api-surface) Values are restricted to `string` only and compared by identity. reka supports AcceptableValue = string | number | bigint | Record | null for both single and multiple, and compares with deep `isEqual` (ohash) via isValueEqualOrExist, so object/number/bigint values and value-based equality work. - 🔶 (types) No automatic single/multiple type inference or coherence validation. reka infers `type` from whether modelValue/defaultValue is an array and emits dev-time errors when type and value shape disagree; robonen requires an explicit `type` prop (defaults to 'single') and performs no validation. Worse, robonen's typing lets `modelValue: string | string[]` be passed with type 'single', which silently breaks. - 🔶 (rtl-i18n) Global/inherited direction is ignored. reka resolves `dir` via useDirection(propDir) so it inherits from ConfigProvider when the prop is omitted; robonen hardcodes the default to 'ltr' and never consults useConfig().dir even though a config-provider with a reactive `dir` exists in the package. - 🔶 (code-quality) robonen does not reuse its own RovingFocusGroup/RovingFocusItem primitive (which already implements entry-focus, Shift+Tab, mousedown, PageUp/PageDown, modifier guards, data-active prioritization). The toggle-group reimplements a thinner roving algorithm inline, so it permanently lags the dedicated primitive. reka composes RovingFocusGroup/RovingFocusItem and gets all of that for free. **Minor gaps (6):** - ▫️ (keyboard) Keyboard navigation is missing PageUp/PageDown (jump to first/last). reka's RovingFocus maps PageUp->first and PageDown->last in MAP_KEY_TO_FOCUS_INTENT; robonen's inline rovingKeyToAction only handles Arrow keys + Home/End. - ▫️ (keyboard) Navigation does not guard against modifier keys, so Ctrl/Meta/Alt/Shift + Arrow still hijack focus and call preventDefault. reka returns early on event.metaKey/ctrlKey/altKey (and shiftKey unless allowShiftKey). - ▫️ (keyboard) No Shift+Tab tab-out handling or group-level entry focus. reka's RovingFocusGroup tracks isTabbingBackOut (onItemShiftTab) and sets the group container tabindex to -1 while tabbing out, plus an onEntryFocus mechanism that focuses the active/highlighted/current item with preventScrollOnEntryFocus support when focus enters the group. robonen has none of this — focus only moves between items, the group container is not part of the tab sequence, and there is no entry-focus prioritization. - ▫️ (edge-cases) Mousedown focus handling for Safari is absent. reka's RovingFocusItem focuses on mousedown (Safari does not focus buttons on click) and prevents focusing non-focusable items on mousedown; robonen's Item only has @click and @keydown. - ▫️ (code-quality) ToggleGroupItem is not composed from a Toggle primitive, so it does not inherit Toggle's slot props or future Toggle features. reka's item wraps the Toggle component and forwards its slot props (modelValue/state/pressed/disabled). robonen's item only exposes `:pressed` and reimplements the toggle button inline, duplicating logic and diverging from its own existing Toggle primitive. - ▫️ (edge-cases) The deep watch on modelValue does a manual shallow array diff and returns early when v is undefined, which means an external reset to undefined cannot clear the controlled value. reka uses useVModel (passive/deep) which correctly tracks controlled undefined and deep object changes. **Recommendations to make robonen strictly better:** - Add form integration: introduce `name`/`required` props (FormFieldProps-equivalent) on ToggleGroupRoot, a `useFormControl(currentElement)` composable (closest('form'), default true for SSR), and a VisuallyHiddenInput bubble component that emits the selected value(s) with array-name encoding and a required-empty fallback. Grab `currentElement` from useForwardExpose in the Root to feed useFormControl. - Generalize value type from `string` to an AcceptableValue (string | number | bigint | Record | null) and replace identity comparisons (.includes / ===) with a deep-equality helper (port reka's isValueEqualOrExist + isEqual). Update ToggleGroupItemProps.value and the context value type accordingly. - Replace the explicit-only `type` with discriminated SingleOrMultipleProps typing and add a useSingleOrMultipleValue-style composable that infers type from modelValue/defaultValue and logs dev-time coherence errors; this also fixes the unsound `modelValue: string | string[]` typing for single mode. - Consume the global direction: default `dir` to useConfig().dir (via a useDirection-style wrapper) instead of the hardcoded 'ltr' literal so RTL inherits from ConfigProvider. - Refactor ToggleGroupRoot/Item to compose the existing RovingFocusGroup/RovingFocusItem primitives (conditionally on `rovingFocus`) instead of the inline utils/roving-focus algorithm. This instantly adds PageUp/PageDown, modifier-key guards, Shift+Tab tab-out, entry-focus prioritization (data-active/highlighted/current), Safari mousedown focus, preventScrollOnEntryFocus, and group-level tabindex management, and removes duplicated logic. - Compose ToggleGroupItem from the existing Toggle primitive and forward its slot props (modelValue/state/pressed/disabled) so the item stays in sync with Toggle and exposes richer slot data. - Fix the controlled watch so an external reset of modelValue to undefined clears the value (do not early-return on undefined), and use deep equality when diffing object values. - Add PageUp/PageDown handling (and modifier-key guards) to utils/roving-focus.rovingKeyToAction if the inline path is retained instead of migrating to the primitive. **Where robonen is already better:** - ✅ Items are sourced from a real Collection (useCollectionProvider/getItems(true)) so DOM order survives v-for reorders, and the Root computes the enabled-items list once and reuses it for both navigation and tab-stop resolution. reka's RovingFocus also uses a Collection, so this is parity, but robonen keeps it self-contained without the extra RovingFocusGroup component overhead. - ✅ ToggleGroupRoot exposes a `valueChange` emit in addition to `update:modelValue`. reka only emits `update:modelValue` (ToggleGroupRoot.vue ToggleGroupRootEmits), so robonen gives consumers a dedicated change event without needing a watcher. - ✅ The Root default slot is given `:value="localValue"` (always the normalized string[] of selected values), and each Item slot receives `:pressed`. reka's Root slot gives `model-value` (raw) and the Item slot just forwards Toggle's slot props; robonen's normalized array slot is slightly more predictable to consume. - ✅ robonen sets `role="radiogroup"` on the Root and `role="radio"` for single-type items, which is the more semantically correct ARIA pattern for single-select. reka hardcodes `role="group"` on the Root for both single and multiple and relies on Toggle's `aria-pressed` even in single mode (no radio semantics), so robonen's single-mode accessibility semantics are actually better here. ### number-field — _reka-better_ **Parts reka has that robonen lacks:** VisuallyHiddenInput (form submission, rendered inside Root), usePressedHold composable (press-and-hold auto-repeat), useNumberFormatter / useNumberParser (Intl formatting/parsing), handleDecimalOperation (decimal-safe arithmetic), injectNumberFieldRootContext exported from index for external composition **Parts robonen has that reka lacks:** Exposed imperative methods (increment/decrement/setValue) on Root via defineExpose Both libraries ship the same four parts (Root, Input, Increment, Decrement) and cover core keyboard handling (Arrow/Page/Home/End) and ARIA spinbutton wiring. robonen is genuinely leaner in its reactivity model, exposes imperative methods on Root, and has a cleaner null empty-state contract. However reka-ui is currently materially ahead on substance: it has real form integration via a VisuallyHiddenInput plus name/required/id FormFieldProps; full Intl number formatting/parsing with locale and formatOptions; beforeinput keystroke validation; press-and-hold auto-repeat steppers; mouse-wheel support with invert/disable options; step snapping with decimal-safe arithmetic; at-boundary isIncrease/DecreaseDisabled with per-button disabled; commit-on-blur/Enter reformatting; focusOnChange; role='group'; labeled (not aria-hidden) stepper buttons; and proper Primitive/asChild polymorphism plus inheritAttrs handling. robonen also drops asChild on Root and hardcodes the Input as a literal , and it hides the stepper buttons from assistive tech. Net: reka-better today, with a clear, concrete path to make robonen strictly better. **Major gaps:** - 🔶 (forms) No form integration / hidden input. reka renders when isFormControl && name, submitting the value with the surrounding form. robonen's NumberFieldInput is the actual focusable spinbutton with role=spinbutton, type defaults to nothing/input — but it puts name/required on a role=spinbutton input, and there is NO hidden native number input for reliable form submission, and Root has no name/required/id FormFieldProps at all. - 🔶 (features) No internationalized number formatting/parsing. reka supports formatOptions: Intl.NumberFormatOptions and locale, using @internationalized/number NumberFormatter/NumberParser for currency, percent, grouping separators, decimal localization, and computing inputMode (numeric vs decimal) from resolved fraction digits. - 🔶 (edge-cases) No input validation on keystroke. reka uses @beforeinput with numberParser.isValidPartialNumber(nextValue, min, max) to reject invalid characters before they enter the field; robonen lets any text be typed and only nulls it out after the fact. - 🔶 (features) No press-and-hold auto-repeat on increment/decrement buttons. reka's usePressedHold repeats every 60ms after a 400ms initial delay while the pointer is held, and exposes data-pressed and userSelect:none. robonen buttons only fire once per click. - 🔶 (features) No mouse-wheel support. reka increments/decrements on wheel scroll when focused, with disableWheelChange and invertWheelChange props and trackpad axis-magnitude heuristics. robonen has no wheel handling. - 🔶 (edge-cases) No step snapping. reka snaps the value to the nearest valid step boundary (snapValueToStep) with a stepSnapping prop to disable it; robonen only clamps to min/max and never snaps off-grid values to the step grid. - 🔶 (edge-cases) Decimal-safe arithmetic missing. reka uses handleDecimalOperation to avoid floating-point drift (e.g. 0.1+0.2) when stepping and when computing isIncrease/DecreaseDisabled. robonen does base + delta directly, so step=0.1 accumulates 0.30000000000000004. - 🔶 (features) Increment/Decrement buttons cannot reflect at-boundary disabled state and have no per-button disabled prop. reka computes isIncreaseDisabled/isDecreaseDisabled (value at max/min) and accepts a local disabled prop, disabling the button and exposing data-disabled when the next step would exceed the bound. robonen buttons are only disabled by global disabled/readonly. - 🔶 (accessibility) Buttons are aria-hidden='true' in robonen, removing them entirely from the accessibility tree, whereas reka exposes them with aria-label='Increase'/'Decrease'. Hiding interactive controls from AT is worse for SR users who navigate by control. - 🔶 (api-surface) as-child not forwarded on Root in robonen. reka passes :as-child="asChild" to its Primitive; robonen Root passes :as only and never forwards asChild from PrimitiveProps, so polymorphic asChild rendering on the group is broken. - 🔶 (api-surface) Input/Increment/Decrement do not forward asChild either, and the Input is a hardcoded element rather than Primitive, so as/asChild on the Input is ignored. reka's Input is a Primitive (as='input' default) supporting polymorphism. **Minor gaps (6):** - ▫️ (accessibility) Input is not natively focusable/tabbable with explicit tabindex=0, and lacks autocorrect/spellcheck/aria-roledescription. reka sets tabindex=0, autocorrect='off', spellcheck='false', aria-roledescription='Number field' for better SR/typing behavior. - ▫️ (accessibility) Root is missing role='group'. reka sets role='group' on the Root container so the spinbutton + buttons are exposed as a labeled group. robonen Root renders a bare Primitive with only data-* attributes. - ▫️ (edge-cases) No commit-on-blur / commit-on-Enter reformatting. reka calls applyInputValue on @change, @keydown.enter and @blur to re-clamp, snap, and reformat the displayed text; robonen commits live on every input and never reformats/snaps on blur. - ▫️ (keyboard) No focusOnChange behavior. reka refocuses the input when value changes via buttons (focusOnChange default true) so keyboard users keep context after clicking a stepper. robonen does not refocus. - ▫️ (edge-cases) contextmenu suppression on hold missing. reka adds @contextmenu.prevent on the stepper buttons so a long press does not pop the context menu mid-hold. robonen buttons have no such handler. - ▫️ (api-surface) Root does not forward $attrs with inheritAttrs:false, and Input does not v-bind merge external props cleanly. reka sets defineOptions({inheritAttrs:false}) and v-bind="$attrs" on Root so attrs land on the primitive group rather than being lost/duplicated. robonen Root uses default attr inheritance with a manual slot only. **Recommendations to make robonen strictly better:** - Render a VisuallyHiddenInput (robonen already has a VisuallyHidden component to build on) inside Root gated on form presence, and add name/required/id FormFieldProps to NumberFieldRoot so the value participates in native form submission; move name/required off the spinbutton input. - Add a usePressedHold-style composable (400ms initial, ~60ms repeat, pointerdown/up/cancel) to NumberFieldIncrement/Decrement, expose data-pressed and set userSelect:none while held, and add @contextmenu.prevent. - Add beforeinput validation that parses the prospective value and preventDefaults invalid characters, plus commit-on-blur and commit-on-Enter that re-clamp, snap, and reformat the display. - Introduce decimal-safe arithmetic (handleDecimalOperation equivalent) for increment/decrement and add snapValueToStep with a stepSnapping prop so values align to the step grid. - Compute isIncreaseDisabled/isDecreaseDisabled in Root (value at/over bound for next step), provide them via context, add a per-button disabled prop, and disable/data-disabled the buttons accordingly. - Add wheel support with disableWheelChange and invertWheelChange props and trackpad axis-magnitude heuristics on the input. - Add Intl number formatting/parsing: formatOptions and locale props, a textValue derived from a formatter, and derive inputmode (numeric vs decimal) from resolved maximumFractionDigits instead of hardcoding 'decimal'. Pair with a useLocale that respects RTL/dir from ConfigProvider. - Improve a11y: set role='group' on Root; stop aria-hidden on the stepper buttons and give them aria-label='Increase'/'Decrease' (configurable); add aria-roledescription='Number field', autocorrect='off', spellcheck='false', and explicit tabindex='0' on the input. - Make NumberFieldInput a Primitive (as default 'input') and forward asChild on Root/Input/Increment/Decrement; set inheritAttrs:false + v-bind=$attrs on Root so attrs land on the group. - Add focusOnChange (default true) so clicking the steppers returns focus to the input. **Where robonen is already better:** - ✅ Leaner reactivity: toRef identity passthroughs and a single guarded value ref, with no @vueuse/core useVModel dependency. - ✅ Exposes increment/decrement/setValue as an imperative API via defineExpose on Root (reka exposes nothing on its Root). - ✅ Deterministic null empty-state contract (number | null) instead of reka's undefined/NaN mixing. - ✅ Auto-generates and wires a stable inputId via useId so label association works without consumer-supplied id. - ✅ Root slot exposes increment/decrement action callbacks for render-prop consumers (reka exposes only modelValue/textValue/readonly). ### radio-group — _reka-better_ **Parts reka has that robonen lacks:** Radio.vue (standalone radio usable outside a group, with its own value/name/required/checked v-model + form input + select event) reka-ui's RadioGroup is currently more capable than robonen's on several real axes. Biggest gaps: (1) value typing — reka supports AcceptableValue (number/bigint/object/null with structural isEqual) while robonen is string-only with ===; (2) keyboard — reka handles PageUp/PageDown (first/last), robonen does not; (3) RTL/i18n — reka resolves dir via ConfigProvider (useDirection) while robonen hardcodes 'ltr' and ignores its own config-provider; (4) focus management — reka composes a battle-tested RovingFocusGroup/RovingFocusItem (entry-focus prioritization, Shift+Tab tab-out, Safari mousedown workaround) whereas robonen re-implements roving focus inline and does NOT reuse the RovingFocusGroup it already ships; (5) selection extensibility — reka emits a cancelable select event and routes selection through the element's native click (so change/click events fire), robonen calls setValue directly; (6) forms — reka uses VisuallyHiddenInput with synthetic input/change dispatch, useFormControl gating, and array/object serialization, while robonen renders a plain styled input with no change events and only when name is set; (7) Indicator animation — reka wraps in Presence, robonen uses bare v-if despite having Presence available; plus minor a11y gaps (group-level required not merged into items, no aria-label derivation, Enter not suppressed) and DX gaps (no exported context injectors, thinner defineSlots, no standalone Radio). robonen is genuinely better in a few places: modern defineModel v-model, group-level data-orientation and aria-disabled on the root, a flatter single-element item (no RovingFocusItem+Radio wrapping), and fewer runtime deps / no per-item global window listeners. Net verdict: reka-better until the gaps (especially value types, RovingFocusGroup reuse, dir/ConfigProvider, PageUp/Down, select event, and Presence indicator) are closed. **Major gaps:** - 🔶 (api-surface) reka supports non-string values (string | number | bigint | Record | null via AcceptableValue) for both Root modelValue/defaultValue and Item value, using ohash isEqual for comparison. robonen hard-codes value: string on the Item and string|undefined on Root, so number/boolean/object option values are impossible. - 🔶 (keyboard) reka handles PageUp -> first and PageDown -> last keyboard navigation. robonen's roving-focus util (rovingKeyToAction) only maps ArrowUp/Down/Left/Right + Home/End and returns null for PageUp/PageDown, so those keys do nothing in robonen's RadioGroup. - 🔶 (rtl-i18n) reka's dir resolves through useDirection(propDir) which falls back to the global ConfigProvider dir; robonen's RadioGroupRoot hardcodes dir = 'ltr' default and never consults useConfig().dir, even though robonen HAS a config-provider exposing dir (and its own RovingFocusGroup already uses useConfig().dir). So an app-wide RTL ConfigProvider is ignored by RadioGroup. - 🔶 (api-surface) reka emits a cancelable 'select' CustomEvent (RADIO_SELECT) per item before checking, allowing consumers to preventDefault and block selection; robonen has no select emit/hook on RadioGroupItem at all. - 🔶 (forms) reka wires form integration through VisuallyHiddenInput which dispatches synthetic input/change events on value change (VisuallyHiddenInputBubble watch -> native setter + dispatch 'input'/'change'), enabling native form validation and event-driven listeners; it also supports array/object/required-empty value serialization. robonen renders a plain via inline styles, mirrors value but dispatches no input/change events and cannot serialize non-primitive values. - 🔶 (features) reka's RadioGroupIndicator wraps content in with the present prop, enabling exit/enter animations and forceMount-controlled animated unmount. robonen's RadioGroupIndicator uses a plain v-if (forceMount || item.checked.value) with no Presence, so it cannot animate the indicator out (it unmounts immediately) despite robonen having a Presence component available. - 🔶 (features) reka's roving focus is provided by a shared RovingFocusGroup/RovingFocusItem that handles entry-focus candidate prioritization (active/highlighted/current/first), Shift+Tab tabbing-out (isTabbingBackOut), focusableItemsCount-driven group tabindex, and a Safari mousedown focus workaround. robonen's RadioGroup re-implements roving focus inline (onItemKeyDown + isTabStop computed) and does NOT use its own RovingFocusGroup, so it lacks entry-focus prioritization, Shift+Tab tab-out handling, and the Safari mousedown focus workaround that reka inherits. **Minor gaps (9):** - ▫️ (accessibility) reka's RadioGroupItem.required = computed(() => rootContext.required.value || props.required) so the group-level required propagates to each radio's aria-required and the native input. robonen's RadioGroupItem applies only its own props.required (:aria-required="required || undefined") and never reads ctx.required, so a group-level required does NOT mark items as required for AT. - ▫️ (accessibility) reka computes an aria-label for the radio from an associated