rn-motion-ui@4.0.0
Major Changes
-
b430163: Remove
AvailabilitySchedulercomponent. -
45fd462: Remove deprecated
visible/onCloseprops and clean up internal commentsBreaking:
visibleandonCloseprops have been removed fromBottomSheet,FullSheet,AdaptiveModal, andActionFeedbackModal. These were deprecated aliases introduced in the previous minor. Migrate toopenandonOpenChange:// Before <BottomSheet visible={open} onClose={close} /> <FullSheet visible={open} onClose={close} /> <AdaptiveModal visible={open} onClose={close} /> <ActionFeedbackModal visible={open} onClose={close} /> // After <BottomSheet open={open} onOpenChange={close} /> <FullSheet open={open} onOpenChange={close} /> <AdaptiveModal open={open} onOpenChange={close} /> <ActionFeedbackModal open={open} onOpenChange={close} />
Breaking:
state?: neverhas been removed fromMotiPressableProps. It was a no-op guard and carries no runtime effect.Internal call sites (
AdaptiveDropdown,CommandPalette,MultiStepMenu) have been updated to the new API. ThePopoverCtxinternal type is renamed toPopoverContext(unexported; no public API change). InlineRN FALLBACK vs webimplementation notes have been removed from component files. -
fc3b682: Remove
NotFoundcomponent.
Minor Changes
-
a2ff66d:
FileSystem: the background context menu now opens over the empty areagetBackgroundContextMenuActionsused to need a view to right-click. The placeholder that stands in for the file area — an empty folder, a search with no hits, filters that match nothing, a folder still loading — is now mounted in the same background surface the list and icons views use, so a right-click (web) or long-press (native) anywhere in it opens the background menu. An empty folder is exactly where a "New folder" action matters most.It uses the same single-open coordination as the views, so opening it closes any other file-system menu.
Also: the background menu's title at the root now comes from the
titleprop instead of a hardcoded'Files'. Inside a folder it is the folder name, as before. -
22b260f:
FileSystem: multi-selection — Ctrl/Cmd-click, Shift-range, long-press, and a selection boxselectionMode="multiple"lets more than one entry be selected at a time, with the gestures a file browser is expected to have:- Ctrl-click (Cmd-click on macOS), or a long-press on touch: toggle the entry under the pointer in or out of the selection.
- Shift-click: take the contiguous run from the anchor — the last entry picked without Shift — to the entry pressed. The anchor stays put, so shift-clicking around grows and shrinks one run rather than accumulating; hold Ctrl/Cmd as well to add the run to what is already selected.
- A selection box dragged across empty space, web only, in all four views — the list, the icons grid, any columns pane, and the gallery filmstrip (which bands horizontally, being a horizontal list). Everything the band touches is selected live as it is drawn; hold Ctrl/Cmd as you start it to add rather than replace. A box only starts from a point that is not on an entry, so a drag that begins on a row still moves that row.
A plain press still replaces the selection, and a press on the background still clears it. All four views paint the selection, and the status bar counts it with a Clear affordance once there is more than one.
The ordering a Shift-range runs through comes from the view you pressed, not from the store: the list view runs through its rows as drawn (an expanded folder's children included, since they sit between their parent and its next sibling), and the columns view keeps each pane to itself, so a range never jumps across the trail into a sibling folder.
The selected set arrives through a new
onSelectedItemsChange(items), in the order the entries were picked.onSelectionChange(item)is unchanged and now follows the lead — the entry added most recently — which is what the columns trail, the columns preview pane and the gallery stage keep showing.renderBodygainsselectedEntries, andrenderFootergainsselectedCountandclearSelection.Dragging an entry that belongs to a multi-selection now moves the whole selection:
onMovereports every path in onesourcesarray instead of firing per entry. Members the drop would not actually move — the destination itself, entries already inside it, a folder dropped into its own subtree — are filtered out first, and nothing fires when that leaves the list empty. Dragging an unselected entry is still a single-entry drag.Two things to know before switching it on:
- Long-press is already the entry context menu's trigger on touch, and multi-selection takes it over. With
getContextMenuActionsthe menu still opens on right-click on web, but on touch it becomes unreachable — so pick one, or surface those actions elsewhere. - With
draggable, a hold on native starts a drag (at 300 ms) before a long press resolves (at 500 ms), so the toggle gesture is effectively web-only in the list and icons views.
Two fixes fall out of the same work, and apply whatever
selectionModeis set to:- Entry rows and tiles now carry
aria-selected. They only ever setaccessibilityState={{ selected }}, which react-native-web does not map to anything, so on web the highlight fill was the only thing saying an entry was picked — assistive tech was told nothing at all. - A drag in the grid view now only lifts a tile when the press actually landed on one. It used to resolve the press to the nearest tile, so a press in the padding or in a gutter between tiles would lift a neighbour you had not touched.
The default is
selectionMode="single", which behaves exactly as before — except that re-selecting the entry you had already selected before navigating away and back no longer fires a duplicateonSelectionChange. -
dd54f5d:
FileSystem: newrenderEmptyStateslotReplaces the placeholder that stands in for the file area when there is nothing to show, so "This folder is empty" is no longer the only option.
args.reasonsays which of the four cases you are drawing —'empty-folder','no-search-results','no-filter-matches', or'loading'— andargs.labelcarries the copy the built-in placeholder would have used, ready to reuse. The rest of the args (currentPath,folderName,view,searchValue,isSearching,hasActiveFilters) describe the state that emptied it.The slot is per-reason rather than all-or-nothing: return
undefinedto fall through to the built-in placeholder for that state, so you can take over the empty folder and leave the loading spinner and the no-results message alone. Returnnullto draw nothing.Like
renderBody, it is called as a plain function rather than rendered as a component — don't call hooks directly in it, put them in a component you render inside the returned tree.<FileSystem items={items} renderEmptyState={({ reason, folderName }) => reason === "empty-folder" ? ( <DropZone folder={folderName} onPick={upload} /> ) : undefined } />
FileSystemEmptyStateArgsandFileSystemEmptyStateReasonare exported alongside it. Whatever the slot returns is mounted in the same background surface the built-in placeholder uses, sogetBackgroundContextMenuActionsstill opens over it. -
643d0ff: Add
MenuItem— a shared menu-row primitive, now exported asrn-motion-ui/menu-itemCommandPaletteandMultiStepMenueach carried their own near-identical menu-row markup (leading icon, label, active highlight, trailing slot). That row is now a single component with two visual modes selected byiconBackgroundColor:- Default — CommandPalette style: animated
bg-surface-selectedoverlay, 16 px themed icon,py-2padding,text-smlabel. - iOS-style (
iconBackgroundColorset) — Settings/MultiStepMenu style: coloured rounded-square icon,bg-primary/75active highlight,h-11row,text-baselabel.
import { MenuItem } from "rn-motion-ui/menu-item"; <MenuItem icon={Bell} label="Notifications" active={isActive} onPress={select} />;
MultiStepMenu'sMenuRowandCommandPalette's internalCommandRoware now thin wrappers over it — no public API change to either, beyondMenuRowProps['icon']being typed as the exportedMenuItemIcon(structurally identical to the previous localIconRenderer) andCommandIconPropsbecoming an alias of the sharedIconProps(widened with the optionalstrokeWidth,styleandaccessibilityLabelfields; existing icon renderers stay assignable).BottomSheet's sheet container moves ontocn()+ theSURFACE_CLASSNAMEladder. Two visual consequences: it now carriesshadow-elevated-3alongsidebg-surface-3, and its non-full-sheet top radius changes fromrounded-t-2xltorounded-t-lg.Also folded template-literal class concatenation into
cn()inActionFeedbackModal, dropped the now-unneededuseSortedClassesbiome-ignore comments, and rewrote theAdaptiveDropdown/HoverMenustories to use the shared row instead of local one-off copies. - Default — CommandPalette style: animated
-
b2d501d:
CardChoice→RadioCard, now animating per card, plus a new multi-selectCheckboxCardBreaking:
CardChoicehas been renamed toRadioCardto say what it is — a
card-shaped radio — and to pair with the newCheckboxCard. The subpath moved
with it; there are no deprecated aliases.Old New rn-motion-ui/card-choicern-motion-ui/radio-cardCardChoiceRadioCardCardChoiceGroupRadioCardGroupCardChoiceGroupPropsRadioCardGroupPropsCardChoicePropsRadioCardPropsThe default group
testIDprefix follows the rename:card-choice-group→
radio-card-group, so derived ids becomeradio-card-group-card-<value>,
-ring,-dotand-badge.// Before import { CardChoice, CardChoiceGroup } from "rn-motion-ui/card-choice"; <CardChoiceGroup value={plan} onValueChange={setPlan}> <CardChoice value="monthly" title="Monthly" subtitle="$12/mo" /> </CardChoiceGroup>; // After import { RadioCard, RadioCardGroup } from "rn-motion-ui/radio-card"; <RadioCardGroup value={plan} onValueChange={setPlan}> <RadioCard value="monthly" title="Monthly" subtitle="$12/mo" /> </RadioCardGroup>;
Breaking: the shared gliding dot is gone.
RadioCardGroupused to render a
single dot that measured each card's radio ring (measureInWindow) and glided
between them. Selection now animates per card instead: the ring's border and the
card's border cross-fade betweenborderandinfo, the background tint
cross-fades in the same pass, and the dot fades and scales in place. No geometry
is measured, so selection no longer depends on layout settling.What changes for callers:
- The selected accent is
info, notprimary. The ring border, dot, card
border and background tint all resolve from--color-info, so selection reads
as state rather than as the page's brand action colour. The dot also grew from
10 px to 14 px inside the 20 px ring. Thebadgepill is unaffected — it stays
primary, since it labels the offer, not the selection. radio-card-group-indicatorno longer exists. Each selected card renders
its own dot at<card testID>-dot. Previously that id only appeared on
standalone cards; inside a group it is now present too.transitionretimes the cross-fade, not a glide. The default moved from
MOTION_SNAPPY(a spring, appropriate for travel) toTIMING_FAST(150 ms
timing, appropriate for a fade). A spring is still accepted.RadioCardtakes its owntransition, overriding the group's — the same
group-cascades-to-card shapeCheckboxCarduses forcheckTransition.classNameandstylenow target the animated card surface, the bordered
padded box inside the pressable. APressablecan't be animated directly, so
the border and tint live on aMotiViewinside it and the pressable keeps only
flex-1. Visual overrides (padding, radius, border) behave as before; an
override of the card's outer footprint (e.g. a fixedwidth) now sizes the
surface withinflex-1rather than the pressable itself. Wrap the card to
control its outer box.
Fixed:
RadioCardnow setsaria-checkeddirectly instead of
accessibilityState={{ checked }}, which react-native-web does not forward — the
selected state never reached the DOM on web, so screen readers announced every
card as unchecked. MatchesRadioandCheckbox.RadioCardalso gained an
accessibilityLabelprop, defaulting totitle, so a card answers with its own
name rather than its concatenated text content.New:
rn-motion-ui/checkbox-card— exportsCheckboxCardand
CheckboxCardGroup, the multi-select counterpart toRadioCard. Same card
anatomy (title, subtitle, badge,numericsubtitle, custom children), with
Checkbox's animated box in place of the radio ring: theinfofill and the
check mark cross-fade on toggle and the box springs down on press. Selection uses
the sameinfoaccent asRadioCard, so the two read as one family.Because any number of cards can be checked at once,
CheckboxCardGroupowns only
the selected-value array. It takesrole="group"; each card answers
accessibilityRole="checkbox"witharia-checked/aria-disabled.Props follow the heroui-native names already used by
Switch—isSelected,
onSelectedChange,isDisabled. Group-levelisDisabledandcheckTransition
cascade to every card, and a card can override either.import { CheckboxCard, CheckboxCardGroup } from 'rn-motion-ui/checkbox-card'; // Grouped — the group owns the selected array const [addons, setAddons] = useState<string[]>(['support']); <CheckboxCardGroup value={addons} onValueChange={setAddons}> <CheckboxCard value="seats" title="Extra seats" subtitle="$4/mo each" numeric /> <CheckboxCard value="support" title="Priority support" subtitle="$29/mo" badge="Popular" numeric /> </CheckboxCardGroup> // Standalone — the card is driven directly <CheckboxCard isSelected={on} onSelectedChange={setOn} title="Audit log" subtitle="$12/mo" />
New types:
CheckboxCardProps,CheckboxCardGroupProps. - The selected accent is
-
7bb97f1:
StatefulButton: external reset signal,afterReset, andautoReset→shouldAutoResetBreaking:
autoResetis renamed toshouldAutoReset. It keeps the same meaning — return to idle once the success/error window closes — and the samefalsedefault. Rename the prop at the call site; there is no deprecated alias.New
shouldReset. A reactive signal, not a mode: raise it and the button resets to idle immediately, wherever it happens to be. It is edge-triggered on the rise, so a parent that leaves it pinnedtrueresets the button once rather than on every press — lower it and raise it again to reset again. Raising it on an idle button with nothing in flight does nothing.A mid-flight reset takes effect at once instead of waiting for the pending action: the in-flight run is orphaned, so when its promise finally settles it neither shows its outcome nor opens a terminal window, and
afterSuccess/afterErrorstay silent for that run.New
afterReset. Fires whenever a reset actually returns the button to idle, from either path — theshouldResetsignal or theshouldAutoResetwindow end.The two props answer different questions and compose:
shouldAutoResetdecides what happens when a run's terminal window ends,shouldResetlets the parent cut a run short at any point.const [resetSignal, setResetSignal] = useState(false); <StatefulButton onPress={submit} shouldReset={resetSignal} afterReset={() => setResetSignal(false)} />;
-
736a452:
Switch: heroui-native prop names + compound sub-componentsBreaking: Props have been renamed to align with heroui-native conventions. Update call sites accordingly — there are no deprecated aliases.
Old New checkedisSelectedonCheckedChangeonSelectedChangedisabledisDisabledCompound sub-components.
Switchis now a compound component; the following sub-components are available:Switch.Thumb— sliding pill thumb. Spring-animated; squishes lightly on press. Accepts athumbTransitionoverride and render-function children(props: SwitchRenderProps) => ReactNode.Switch.Label— pressable label container. Tapping it toggles the switch (like an HTML<label>). Disabled automatically whenisDisabledis set.Switch.StartContent— absolutely-positioned icon slot on the left (start) side of the track; typically holds an icon visible when the switch is off.Switch.EndContent— absolutely-positioned icon slot on the right (end) side of the track; typically holds an icon visible when the switch is on.
When no
childrenare provided,<Switch.Thumb>is rendered automatically, preserving the existing visual behaviour.New exports:
useSwitch()hook for accessing switch state from within sub-components, andSwitchRenderProps,SwitchThumbProps,SwitchLabelProps,SwitchContentPropstypes.// Before <Switch checked={on} onCheckedChange={setOn} disabled={false} label="Enable" /> // After — basic (drop-in) <Switch isSelected={on} onSelectedChange={setOn} isDisabled={false} label="Enable" /> // After — custom thumb with icon slots <Switch isSelected={on} onSelectedChange={setOn}> <Switch.StartContent><MoonIcon /></Switch.StartContent> <Switch.Thumb /> <Switch.EndContent><SunIcon /></Switch.EndContent> </Switch>
-
6b591be:
Switch: custom colour themes, defaulting toinfoNew:
theme. The switch's three fills — the selected track, the unselected
track and the thumb — are now a theme rather than two hardcoded classes. Pass a
built-in name, or an object to override individual slots.<Switch isSelected={on} onSelectedChange={setOn} theme="success" />
Six built-ins, one per status token plus the monochrome
primary:info
(default),primary,success,warning,danger,special. Each pairs a
vivid track with the thumb colour that stays legible on it — the status fills
take awhitethumb,primarytakesprimary-foregroundinstead, because
primaryis near-white in dark mode and a white thumb would vanish into it. The
grey off-track is shared by all six, so a row of mixed themes reads as one
family.Breaking: the default look changed. The selected track was
bg-primary
(near-black on light, near-white on dark) and the thumb wassurface-3. The
defaultinfotheme makes the track theinfoblue and the thumbwhitein
both schemes, matching the accentRadioCardandCheckboxCardalready use for
selection — selection reads as state rather than as the page's brand action
colour. The unselected track is unchanged (muted-foregroundat 60%). Pass
theme="primary"for the previous appearance:// What theme="primary" restores — the previous default look <Switch isSelected={on} onSelectedChange={setOn} theme="primary" />
Custom themes. An object overrides slots on top of
info, so anything left
out keeps the default —{ track: '#0ea5e9' }still gets the grey off-track and
the white thumb. Each slot takes one of three things:Slot value Resolves to 'accent'the --color-accenttoken, so it follows light/dark and consumer@themeoverrides'special/70'the same token re-alphaed to 70%, as Tailwind's slash modifier does '#0ea5e9','rgba(0,0,0,0.4)'itself — any literal CSS colour RN parses // Tokens — tracks the theme <Switch isSelected={on} onSelectedChange={setOn} theme={{ track: 'accent', trackOff: 'muted', thumb: 'accent-foreground' }} /> // A literal brand colour for the on-track, defaults for the rest <Switch isSelected={on} onSelectedChange={setOn} theme={{ track: '#0ea5e9' }} />
Fills are set through
stylefrom resolved values rather than by a utility
class, because a slot accepts an arbitrary CSS colour, which no class can carry.
Token names still go through the theme bridge, so a themed slot follows
light/dark exactly as a class would.New testIDs. The track and thumb now carry ids derived from the switch's own:
<testID>-trackand<testID>-thumb(switch-track/switch-thumbby
default). Previously neither was addressable.useSwitch()gained two fields.colorsholds the active theme's three
fills resolved to concrete sRGB —Switch.Thumbpaintsthumb, and custom
content can read the track fills to match them.testIDis the switch's
resolved id, which sub-components derive their own from.New types:
SwitchThemeName,SwitchThemeColors,SwitchColor,SwitchColors
— all exported fromrn-motion-ui/switch. -
e7fe0f1: Theme:
whiteandblackare now first-class tokensTwo absolute colors join the token sheet. Unlike every other color token they do not flip with the theme —
oklch(100% 0 0)andoklch(0% 0 0)in light, dark, and on native — so they cover the places where a fixed color is the design intent rather than an oversight: a glyph sitting on a vivid status fill, a gloss highlight, a scrim.They are available everywhere the other tokens are — the
bg-white/text-black/border-whiteutilities, anduseThemeColor/useThemeColors:<Text className="text-white">Legible on a vivid fill in both schemes</Text>
const white = useThemeColor("white"); // "rgb(255, 255, 255)"
ThemeTokengains'white' | 'black', and both are declared in all three places a token lives — the@themeblock, the two dark blocks, and the native OKLCH tables — socheck-token-paritycovers them like the rest. Being achromatic, they pass throughnpx rn-motion-ui-tokensretinting untouched.Reach for these instead of a hardcoded
#fff/#000. For anything that should track the theme,foreground/surface-Nare still the answer.
Patch Changes
-
5c135e4:
FileSystem: fix filter-pill preset no-op and date-range modal stale draftFilter-pill date preset — picking a new date preset on an existing filter pill (e.g. changing "1 month ago" to "3 days ago" via the value chip) was a silent no-op.
setFilterDatePresetmatches onfilter.id; the pill was passing the filter's facet type instead, so nothing ever matched.Date-range modal draft — closing and reopening the custom date range modal for the same facet showed the previous visit's draft instead of reseeding from the filter's stored bounds. The draft state is now scoped inside
AdaptiveModal, which unmounts its children on close (wide path viaAnimatePresence+useModalRender; narrow path viaBottomSheet'sisMountedguard). TheDateRangeRequestcarries anidcounter so reopening the same facet gets akeychange and re-runs the lazy initialisers from the updatedinitialRange.Two regression stories cover both fixes:
Demo: Re-value a filter pillandDemo: Custom range starts fresh each visit. -
74d2e8b:
FileSystem: the selected row now reads as a selection rather than as the primary fillSelection in the list, icons and columns views was painted with
primary— the monochrome token consumers are meant to override with their own brand color. So a selected row went near-black in light mode and near-white in dark, and any consumer who retintedprimarygot their brand color as the selection highlight whether or not that was the intent.It is
infonow — the vivid blue that already reads as "this one is picked" in a file browser, on both schemes, and is not the token a consumer is invited to repaint. The label, the row's metadata columns, and the expand chevron sit on that fill aswhiterather thanprimary-foreground, which on a vivid blue is what legibility actually wants.Nothing to change on your side unless you were relying on
primaryto tint file-system selection; if you were, that hook is gone on purpose. -
f3dd5fa:
FileSystem: migrate internal state from React Context to per-instance Zustand storeNo public API change. Each
FileSystemmount now owns acreateStore-based Zustand store instead of a single React Context value, so sibling instances never share state and re-renders are limited to the slices that actually changed (useShallowon every slice hook).The old
use-file-system,use-file-system-filters, anduse-file-openinternal hooks are removed; all consumers now call the new granular slice hooks (useFileSystemNavigation,useFileSystemEntries,useFileSystemSearch,useFileSystemFilters,useFileSystemSelection,useFileSystemViewer,useFileSystemLayout,useFileSystemConsumer) and their matching action hooks. -
fe8d207:
StarRating: warmer default gold, and inactive stars sit onaccentrather thanborderTwo color changes, both visible without touching a prop:
- The default
activeStarColormoves from#edde51to#fec700— the same fixed, theme-exempt gold intent, but warmer and more saturated, so a filled star reads as gold rather than as pale yellow. - Inactive stars now fall back to the theme
accentcolor instead ofborder.borderis a translucent hairline token (oklch(0% 0 0 / 0.1)), which is right for a 1 px rule and too faint for a filled glyph — empty stars were nearly invisible on light surfaces.accentis opaque and tracks the theme, so the empty half of a rating stays legible on both schemes.
Pass
activeStarColor/inactiveStarColorto keep the previous values. - The default