Releases: iv-stpn/rn-motion-ui
Release list
rn-motion-ui@5.2.0
Minor Changes
-
b56144c: Draggable: collision algorithms, axis constraint, drag bounds, and handle sub-component
- New
collisionAlgorithmprop ('intersect'|'contain'|'center') switches zone hit testing from point-based to rect-vs-rect. The draggable's live rect (computed from its lift-time box offset by pointer delta) is tested against each zone's measured box using the chosen strategy. Falls back to the existing point-in-rect test when unset or when nosourceRectis available. - New
dragAxisprop ('x'|'y'|'both') constrains pointer movement to a single axis during the drag. The ghost and zone targeting respect the clamped position;onDragMovestill receives the raw (unclamped) point. - New
dragBoundsRefprop accepts a ref to a boundaryView. The drag ghost is clamped inside that view's window-coordinate rect on every frame. Pan-transport only (touch on web, native); HTML5 drags are controlled by the browser. - New
<Draggable.Handle>sub-component restricts drag initiation to a sub-area. Multiple handles per draggable are supported; as long as at least one is mounted, the host'sGestureDetectoris suppressed.
Dragzone:
skipRectMeasurefor programmatic hit testingZones that compute hit testing through another mechanism (e.g. arithmetic position in
SortableList) can now setskipRectMeasure={true}. The zone is never measured, never participates in measure sweeps, and always passes the spatial hit test — the consumer'sacceptspredicate is the sole gate.SortableList: Reanimated-powered UI-thread animations
SortableListnow drives item position animations entirely on the UI thread viareact-native-reanimatedshared values anduseAnimatedReaction. Insertion index updates write directly to aSharedValuewithout triggering React re-renders; items read the shared values in worklets and animatetranslateYwithwithTiming. The commit (on drop) snaps items to their new canonical positions via adropVersionshared value bump in auseLayoutEffect— the user never sees an intermediate frame. TheactiveIndex(forrenderItem'sisDraggingflag) stays as React state, so only the dragged item re-renders on lift/drop. - New
-
ee276b3: New
IconButtoncomponent — a purpose-built icon-only button supercedingButton size="icon"IconButtonis a standalone component for icon-only actions. It shares the same 8 visual variants asButton(primary,secondary,ghost,danger,special,inverse,outlineDanger,ghostDanger) and adds theicon/iconBackgroundColor/iconColorAPI fromMenuItemfor coloured icon tiles (iOS Settings style).Key differences from
<Button size="icon">:iconprop takes aComponentType<IconProps>— the icon component itself, not a pre-built elementiconBackgroundColoroptionally wraps the icon in a coloured rounded-square tileiconColoroverrides the variant-derived icon stroke colouraccessibilityLabelis required — an icon-only button needs an accessible name- No
children,leftAdornment, orrightAdornment— the icon IS the content - Sizes:
'sm' | 'md' | 'lg'(24×24, 32×32, 40×40 px squares)
Existing
<Button size="icon">continues to work.ButtonSpinneris now exported frombutton-internalsto power the loading state. -
d8bf5e6: OtpInput: refreshed API with alpha/alphanumeric types, ref handle, and renamed props
BREAKING prop renames (pre-release):
length→numberOfDigitsmask→secureTextEntryonChange→onTextChangeonComplete→onFilledstatus/OTPStatus→OtpInputStatus
New features:
typeprop —'numeric' | 'alpha' | 'alphanumeric'. Controls which characters each slot accepts. ThesanitizeandapplyEditlogic functions now accept atypeparameter.refhandle (OtpInputRef) — exposesfocus(),blur(), andclear()imperatively.autoCompleteprop — forwarded to the hiddenTextInput.stickBlinkMsprop — customise the cursor blink interval.
Internal:
applyEditnow takes a single options object ({ prev, raw, length, anchor, type }) instead of positional arguments. Tests updated accordingly. -
9b9b09c: ReorderableList: remove ghost mode (indicator-only)
ReorderableListis now indicator-mode only. Themodeprop,renderPreviewprop, and all ghost-mode state (previewKeys,ghostKey,flipRects,movedKey) are removed.- Breaking:
modeprop removed —'ghost'is no longer accepted - Breaking:
renderPreviewprop removed - FLIP animation system removed (Animated.View wrappers,
measureInWindowtracking, easing curves) - Items now render in plain
Viewwrappers instead ofAnimated.View ReorderableItemadds a mount-time measure effect so zone rects are populated before a drag can landisPastThresholdhelper removed fromreorderable-list-reorder;insertionPositionno longer acceptsghostHeight
For real-time visual reordering during drag, use the new
SortableListcomponent (rn-motion-ui/sortable-list). - Breaking:
-
43fb467: Rename
DnDList→ReorderableListThe
./dnd-listexport is replaced by./reorderable-list. All associated types and helpers are renamed accordingly:DnDList→ReorderableList— the main container componentDnDItem→ReorderableItem— individual draggable rowsdndReorder→reorderableListReorder— the reorder helper
Import from
rn-motion-ui/reorderable-listinstead ofrn-motion-ui/dnd-list. The old path is removed. -
d6be9ea: New
SortableListcomponentA drag-to-reorder list where items visually reorder in real-time during the drag — the dragged item is dimmed at its preview position while other items animate to close the gap or make room.
Built on the existing gesture primitives (
Draggable,Dragzone,DragManager), so it inherits their transport story and isolation model. Each item computes its visual position as a pure function of(index, activeIndex, insertionIndex)and animatestranslateYto reach it — no rect measurement, no FLIP snapshots, no tree reordering during the drag.- New export:
rn-motion-ui/sortable-list - Requires a fixed
itemHeightprop (every item must share the same height) - Isolates itself inside a
<DragManager isolate>— two lists on the same page are independent - Supports
renderPreviewfor a custom drag ghost - The reorder commits on drop; cancelling reverts items to their original positions
- New export:
-
a37019b: Table: move border and background colours out of the component into configurable
classNamepropsThe Table component previously hardcoded
border-border border-bon rows, headers, cards, and footer, as well asbg-surface-selectedon the selected-row overlay,bg-borderon skeleton pulses, andbg-primary/bg-dangeron row insert/delete buttons. Those are now removed from the component internals and exposed as newclassNameprops:selectedClassName— classes merged onto the selected row/card background overlaydropIndicatorClassName— classes merged onto the column-reorder drop indicatorskeletonClassName— classes merged onto the skeleton pulse bars during loadingemptyClassName— classes merged onto the empty-state wrapper
Stories preserve the classic appearance via a
CLASSIC_TABLEdefaults object spread onto each<Table>instance. Remove or override individual entries to customise. -
1c8a226: ToggleGroup: replace Button children with
itemsprop; addpillvariant; fix bordered outer borderBREAKING:
ToggleGroupno longer accepts Button children. Replacechildrenwith the newitemsprop ({ value: string; label: ReactNode }[]). TheselectedVariant,unselectedVariant, andpressModeprops are removed. Thesizeprop is now'sm' | 'md' | 'lg'(drops'icon').BREAKING:
ButtonandElevatedButtonno longer accept avalueprop. This was only used by the old ToggleGroup pattern and is now removed fromBaseButtonProps.New
pillvariant: abg-muted rounded-fullcontainer with a spring-animated sliding indicator (bg-surface-3/dark:bg-black) that glides behind the selected item's text. RespectsuseReducedMotion().Fixed
borderedvariant: now renders a visible outer border (border border-border rounded-interactive) in addition to the existing inner divider borders.Items are now flat
Pressable+Textsurfaces (no longer Button components), with uniformpx-3horizontal padding.
Patch Changes
-
c7992e0: Button: prune
outline,ghostPrimaryvariants; tighten label + ripple coloursThe
outlineandghostPrimaryvariants are removed. All internal and story usages ofoutlineswitch toghost. Label colour map simplified:primarynow usestext-foreground,secondaryusestext-surface-1,ghostusestext-foreground. Filled-ripple set updated (secondaryadded,primaryremoved) so the white shimmer only fires on opaque dark fills. Spinner colour resolution consolidated.New helpers in
button-scale.ts:buttonRadiusClass()(CSS twin ofbuttonRadius()),STATE_ICON_SIZE, andSTATE_BUTTON_GAP_CLASSNAMEfor proportional state-icon spacing per size. -
c7992e0: Extract shared
CloseButtoncomponent; add close button to MorphingModalA new
CloseButtoncomponent replaces the inlinePressable+CloseLineicon pattern used across AdaptiveModal, FullSheet, and MorphingModal. The component is a simple themed close icon button with consistent hit slop and accessibility label.MorphingModalgains ashowCloseprop that renders aCloseButtonin the top-right corner of the panel.FullSheet'scloseIconprop now accepts any ReactNode (previously just an icon override) — pass a<CloseButton>or any custom element. -
30569fe: **Drag overlay: ghost fade-out, HTML5 positioning, S...
rn-motion-ui@5.1.0
Minor Changes
-
2a3f08b: Breaking: token rename —
--spacing-button-*→--spacing-interactive-*,--radius-button-*→--radius-interactiveThe box geometry tokens (height, horizontal padding, corner radius) that were previously named after buttons have been renamed because they are shared by Button, Input, OtpInput, Tabs, and ButtonGroup. Consumers who overrode
--spacing-button-md,--radius-button-mdor their CSS utility classesh-button-*,rounded-button-*,px-button-pad-*must update to the new names.New: four-category corner radius system
Radius is now split into
--radius-interactive(buttons, inputs, tabs),--radius-card,--radius-menu, and--radius-modal— each family independently tunable. A new sharedlib/radius.tsmodule exports the corresponding pixel constants and Tailwind class strings, replacing the definitions that lived insidebutton-scale.ts.Menu: staggered item entry, faster exit, size-aware separators
Each menu item now fades in with a 25ms stagger delay, driven by a new
MOTION_MENU_ENTERspring preset. Exit duration was cut to 150ms, enter scale deepened to 0.85, and per-item offset increased to 12px.MenuSeparatorandMenuLabelnow accept asizeprop so their thickness and font size track the menu scale.Button: press animation modes and continuous spinner
Buttons gain a
pressModeprop —scale(default, uniform),scaleY(vertical compression for segmented controls), andnone. The loading spinner was rewritten from declarative MotiView loop to imperative ReanimatedwithRepeatso it no longer restarts on every parent re-render.Tabs: new
sizepropTabs now accepts
size(sm|md|lg) to control trigger height via the interactive surface family tokens, aligning with Button and Input at the same size.Input: size-aware text and padding
The Input text box now scales its font size and horizontal padding per the
sizeprop, matching the interactive surface family.Dock, Table, Loader, FeedbackWidget, ButtonGroup, and others
All components updated to use the renamed tokens and consolidated radius constants.
Patch Changes
-
b561c15: Tighten interactive surface sizing
Interactive component heights reduced by 4px per tier (sm: 28, md: 36, lg: 44) and horizontal padding trimmed by 2px (sm: 10, md: 14, lg: 18). MenuItem, MultiStepMenu sidebar, and OTP slot line height updated accordingly. CSS spacing tokens synced across
tokens.cssandstorybook/demo/tokens.css. -
f5b3a55: refactor: migrate inline style props to className where possible
Inline
styleprops (flexDirection, overflow, width, opacity, textAlign) moved to Tailwind utility classes across Marquee, SwipeableList, Table, Button, and ElevatedButton.CHECKBOX_COL_WIDTHrenamed toCHECKBOX_COLUMN_WIDTHand relocated fromtable-types.tstotable-utils.ts. No behavioural changes.
rn-motion-ui@5.0.3
Patch Changes
- 4754dab: - ButtonGroup: new form component for grouping buttons with segmented,
toolbar, and grid layouts- FeedbackWidget: refactored morphing animation using shared layout springs;
replacedAnimatePresencewrapper with coordinated scale/translate
transitions on individual views (SPRING_SWAP,SPRING_LAYOUT); container
now animateswidthinstead of justborderRadius - Input: added
outline-noneto the text field; fixed iOS text vertical
alignment viatextAlignVertical: 'center'andlineHeight: 0 - FileSystem header: removed bottom border
- HoverMenu:
width="trigger"now setsminWidthfrom the trigger
measurement instead of a fixedwidth, allowing panels to grow wider than the
trigger when content overflows - MorphingModal: added
elevationprop and storybook elevation control - AdaptiveModal: fixed missing
labelon the elevationChoicecontrol in
storybook
- FeedbackWidget: refactored morphing animation using shared layout springs;
- 1cc4430: - check-readme script:
--fixnow auto-inserts missing component rows into
packages/ui/README.mdby extracting PascalCase exports from each component's
source, so the UI components table stays in sync without manual edits- Husky pre-push hook: runs
check-readme.mjs --fixautomatically,
regenerating stale README blocks and inserting unpublished component rows
before every push
- Husky pre-push hook: runs
- 880c1b7: - AnimatePresence: removed unused
presenceAffectsLayoutprop (was accepted
for API compatibility but never implemented) - df6a662: - Table: new
columnLayoutStyle()utility for consistent column width
resolution across header, row cells, and skeleton pulses;containerWidth
removed fromHeaderCell,RowCell,TableRow, andSkeletonCellPulse—
each now usescolumnLayoutStyle(column.width, colWidth)internally- Table: horizontal
ScrollViewnow only wraps the header + body when
columns actually overflow the container; when they fit, no scroll wrapper is
added, avoiding responder-tree interference with long-press menus and the
column-reorder drop indicator - Table: FlatList performance tuned with
windowSize,
maxToRenderPerBatch,initialNumToRender,updateCellsBatchingPeriod, and
nestedScrollEnabledfor smoother large-table rendering - BottomSheet: replaced
flex-1withgrowin the sheet body for UniWind
v4 compatibility - Replaced template-literal
classNameconcatenation with thecn()utility
acrossFeedbackWidget,Checkbox,StarRating,Switch,
AdaptiveDropdown,AdaptiveModal,BottomSheet,FullSheet,HoverMenu,
MorphingModal, andPopover
- Table: horizontal
rn-motion-ui@5.0.2
Patch Changes
- 01a8bf9: fix: replace workspace:* protocol with ^0.0.2 for rn-motion-ui-icons dependency so upstream consumers can resolve it
rn-motion-ui@5.0.1
Patch Changes
- Updated dependencies [646025b]
- rn-motion-ui-icons@0.0.2
rn-motion-ui@5.0.0
Major Changes
-
44a0672: FileSystem: drag and drop now runs on
Draggable/Dragzone/MultiDragManager.Every view had brought its own drag. The list and icons grids shared one hook, the
columns pane had a second one that mirrored its architecture, web had a third for
the HTML5 half and external drops a fourth — five hooks, each measuring boxes,
hit-testing points and tracking a session, and each with its own idea of which
folder a release belonged to. Adding a view meant writing a sixth.They are gone, replaced by the components the library already ships. An entry is a
<MultiDraggable>, a folder is a<Dragzone>, and the panes and the background are
zones too — so the ladder a drop falls down (entry, then the column under it, then
the open folder) is expressed as zone priority rather than as branches inside a
resolver.propsare unchanged:draggable,onMoveandonExternalDropmean
exactly what they did.What changes is behaviour that used to differ per view, and now cannot:
- The three draggable views drag identically — list, icons and columns — because
none of them implements dragging any more. They resolve a drop through one hit
test rather than three that agreed by hand. - A multi-select drag carries the selection, via
MultiDragManager: drag one of
three selected rows and all three move. The members left behind fade, and lifting
an unselected entry still moves just it. - Autoscroll while dragging near an edge now works in the columns panes too,
each scrolling on its own, where before only the list and icons grid had it. - A drop is resolved from measured boxes, so a touch pan and a mouse drag land
on the same folder. Previously the web path read the DOMdroptarget and the
pan path hit-tested rows, which could disagree at a row boundary.
Two fixes fall out of the same work:
onExternalDropnow fires for an in-library<Draggable>from elsewhere on the
page, not just for an OS file drag. Its documented contract always covered "a
custom element on the page that sets drag data"; a payload with no FileSystem
entries in it is foreign whether or not this library started the drag, and it
reaches the consumer either way.- The hover highlight stands down for the length of a drag, in the list, icons
and columns views alike, so it cannot mark one cell while a zone outlines
another. It comes back on the first pointer move after the drop. A mouse drag is
an HTML5 drag and the browser stops the pointer stream while one runs, so the
highlight now takes the lift itself as its cue rather than waiting for a
pointercancelthat not every engine sends.
FS_DRAG_CONTAINER_TEST_IDstill names each draggable view's scroll surface, and
every entry answers to<root>-entry-<path>in all four views. The internal hooks
useFileSystemDrag,useFileSystemColumnsDrag,useFileSystemIconsDrag,
useFileSystemDragWebanduseFileSystemExternalDropare deleted; none was
exported from the package. - The three draggable views drag identically — list, icons and columns — because
-
706dac3: Breaking:
rn-motion-ui/iconsis gone. Icons live inrn-motion-ui-icons.The 109 icons this package used to re-export were a subset of Lucide,
hand-picked because each one had to be committed as generated source. That is a
bad deal for a consumer: the icon you want is either in that list or it does not
exist for you, and the list only grew when a component here happened to need
something.rn-motion-ui-iconsreplaces it with the whole MingCute set — 3335 icons, one
subpath each. Every icon this package renders internally now comes from there,
so what components use and what you can use are the same set.-import { Check, ChevronRight } from 'rn-motion-ui/icons'; +import { CheckLine } from 'rn-motion-ui-icons/icons/check-line'; +import { RightLine } from 'rn-motion-ui-icons/icons/right-line';
IconPropsmoved too, and is no longer exported from this package at all:-import type { IconProps } from 'rn-motion-ui/icons'; +import type { IconProps } from 'rn-motion-ui-icons/icon-props';
Install it alongside this package —
rn-motion-uidepends on it, so anything
that takes an icon (ThemedIcon,CommandIcon,BloomIcon,FileSystem's
action icons) is already typed against the newIconPropsand needs no change
beyond the import.strokeWidthis gone fromIconProps. Lucide's geometry is stroked and
took a width; MingCute ships fill and stroke variants with the weight baked into
the path, so there is nothing to widen. Drop the prop — it is a type error now.
Where this package passedstrokeWidth={2.5}for a slightly heavier check
(Input,OTPInput,StatefulButton,AnimatedBadge), those icons now render
at MingCute's own weight, which is a visible but deliberate change.Names do not carry over: MingCute names its own icons, and most differ from
Lucide's. Every icon is suffixed-lineor-fill(1667 line, 1668 fill), and
the component name is the PascalCase of the file —icons/check-lineexports
CheckLine. The mapping used for the internal migration, if you were relying on
the same names:was (Lucide) now (MingCute) AlertCircle,Infoicons/information-line→InformationLineAlertTriangleicons/alert-line→AlertLineCheckicons/check-line→CheckLineChevronDown/Up/Left/Righticons/down-line/up-line/left-line/right-lineCircleicons/round-line→RoundLineFileText,ScrollTexticons/file-line→FileLineFolderClosed,FolderKanbanicons/folder-line→FolderLineGripVerticalicons/dots-vertical-line→DotsVerticalLineLoaderCircleicons/loading-line→LoadingLineMoreHorizontalicons/more-1-line→More1LinePlusicons/add-line→AddLineTrash2icons/delete-2-line→Delete2LineUsericons/user-2-line→User2LineXicons/close-line→CloseLineThe rest resolve the same way: kebab-case the concept, add
-lineor-fill,
and the export is its PascalCase.
Minor Changes
-
a4e9e3e: AdaptiveDropdown: rename
headerRight→headerSuffix, removeshowCloseprop; simplify header layout (no fixed height, no border, no built-in close button).BottomSheet: increase top corner radius to
rounded-t-2xl. -
99e42b1: Breadcrumbs: new component — the trail FileSystem already drew, now its own.
A breadcrumb trail: the levels above the current one, each a way back, with the
current one as plain text at the end. It knows nothing about what a level is —
pass the segments outermost-first and read back the pressedid, so a folder
path, a route key and a wizard step are all the same thing to it.import { Breadcrumbs } from "rn-motion-ui/breadcrumbs"; <Breadcrumbs items={[ { id: "", label: "Files" }, { id: "documents", label: "Documents" }, { id: "documents/reports", label: "Reports" }, ]} onNavigate={navigateTo} />;
A deep trail scrolls horizontally by default, keeping one line. Set
maxVisible
instead to hold it to a fixed number of levels: the middle folds behind a…
that says how much it hides and hands those levels back when pressed, so nothing
becomes unreachable.scrollable={false}wraps instead of scrolling.The rest of the surface:
separatorreplaces the chevron with any node,size
picks the text scale ('sm'|'base') and takes the separator and icons with
it, aniconper item rides ahead of its label, andcurrentIdpicks which level
is the destination —nullmakes every level pressable, for a trail whose leaf is
not where you are.className,contentClassNameanditemClassNamereach the
container, the segment row and each segment.Accessibility: the container is a
listnamedBreadcrumb, every earlier level
is abuttonnamedGo to {label}(override per item withaccessibilityLabel),
and the current level is text — being the one unpressable segment is what marks
it as current. RN has noaria-current, so the trail does not claim one.FileSystemnow renders this component instead of its own private trail. No
API change and no visual change: same placement under the header, same hiding at
the root, samerootLabelas the leading segment, and the sameGo to {label}
names its stories already query. Both trails — the bar and the per-row ones under
search results — are now built from onebuildCrumbs. -
ae616b8:
Card: passonPressand the surface becomes pressableA card that stands for something you can open had to be wrapped in a
Pressable
by hand, which meant a second element around the one that already draws the
frame. GiveCardanonPressand it renders as thePressableitself:<Card elevation={2} onPress={() => open(project.id)}> <Text>{project.name}</Text> </Card>
Omit it and nothing changes — t...
rn-motion-ui-icons@0.0.2
Patch Changes
- 646025b: Icon components now forward all remaining props to the underlying
<Svg>element via...props.IconPropsextendsSvgPropsfrom react-native-svg, so consumers can pass any SVG prop (hitSlop, onLayout, pointerEvents, opacity, etc.) through to the root SVG.
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-cardCardChoiceRadioCardCardChoiceGroupRadioCardGroupCardChoiceGroupPropsRadioCardGroupPropsCardChoiceProps...
rn-motion-ui@3.4.0
Minor Changes
-
281ac6a: feat(a11y): accessibility sweep of the overlay, carousel, progress and decorative components, plus a writing-direction primitive
Modal semantics.
BottomSheetandActionFeedbackModalnow exposerole="dialog"witharia-modal, take anaccessibilityLabel, and contain keyboard focus on the web through the newuseFocusTraphook — react-native-web rendersModalas an ordinary fixed<div>, so Tab previously walked straight out of an open sheet and into the page behind it, where a keyboard user could operate controls they could not see. Native already had containment fromModalitself, so the hook is a no-op there.BottomSheetalso gainscloseAccessibilityLabel(default'Close'): the backdrop is now a labelled button, because the drag handle it sits next to is a pointer-only affordance and was the only way to dismiss the sheet. The handle itself is now hidden from assistive technology.Announcements.
ActionFeedbackModalwraps its state content in a persistent live region, so a spinner resolving to success or error is announced instead of changing silently. iOS gets an explicitannounceForAccessibility—accessibilityLiveRegionis Android-only and VoiceOver does not re-read a subtree that mutated under it.Values.
CylinderCarouselis now an adjustable control with a position value and working increment/decrement actions, giving it a non-pointer way to change slides for the first time.ScrollProgressreportsrole="progressbar"and a live percentage, mirrored off the UI thread in 5% steps so the indicator stays frame-driven.RangeSlideris fixed as part of this: it set React Native's nestedaccessibilityValue, which react-native-web does not read at all — it forwards only the flataria-value*props — so on the web the slider announced no value whatsoever. Every value-bearing component now emits both spellings.Decorative content.
SkeletonandMarquee's duplicated track are hidden from assistive technology on native as well as web. The marquee previously read its entire contents out twice on iOS and Android.Writing direction. New
rn-motion-ui/hooks/use-direction(useDirection,useIsRTL) andrn-motion-ui/hooks/direction-provider(DirectionProvider). These exist becauseI18nManager.isRTLcannot be the answer on its own: react-native-web'sI18nManageris a stub whoseisRTLis hard-codedfalse, so any component branching on it is silently LTR-only in every browser. The hook reads the right source per platform, and the provider states it explicitly for a subtree.Marqueeis the first component wired up:directionaccepts the logical values'start'(new default, identical to the old'left'under LTR) and'end'alongside the existing physical ones, and mirrors its travel under RTL — where the platform flips the belt's own row and the old direction tore a gap open in the loop instead of cycling.Tabswas audited and needs no change: its indicator and slide direction are both computed from measured geometry, which the platform mirrors along with the layout, so they come out right in either direction. That is now covered by an RTL story rather than left as an assumption.TabsListgained an optionaltestID— the sliding indicator is exposed as${testID}-indicator, so its position can be asserted.RangeSlidernow mirrors under RTL: minimum on the right, filling leftwards, the way a native slider does in an RTL locale. Four things flip together — the pointer mapping (locationXis measured from the physical left edge whichever way the page reads, so without this the slider painted mirrored and then jumped to the wrong value on the first press), the fill's growth origin, the thumb's travel, and the tick positions. A new optionalwritingDirectionprop opts out, for a track whose axis is a thing rather than a quantity — a timeline or a seek bar.Tablecell alignment now follows the writing direction when a column does not setalign. Previously the default paired a direction-relativealignItems: 'flex-start'with a hard-lefttextAlign, so under RTL the text sat on the left inside a right-aligned cell. An explicitalign: 'left' | 'right'stays physical — a column of numbers asking forrightmeans right. Column order is untouched and now documented as the consumer's call: the table renders thecolumnsarray as given, since whether the first column belongs on the right depends on what the data means.Tablecolumn drag-to-reorder now mirrors as well. Its drop boundaries are accumulated from column widths in column order rather than measured, so unlikeTabsit could not inherit the platform's mirroring — the boundary table describes the logical axis while the pointer'spageXis physical, and under RTL the two run opposite ways. Dropping a column on the trailing physical edge now appends it in both directions, and the drop indicator lands on the boundary it marks rather than a column away. The row and column action overlays follow the trailing edge too, instead of pinning to the right.That geometry moved out of the hook into three new pure exports on
rn-motion-ui/table-utils—columnBoundaries,dropIndexAt,dropIndicatorX— so the same drop-target maths a custom header needs is available without reimplementing it, and is unit-testable without a gesture.No breaking changes: every new prop is optional and the defaults preserve current behaviour.
-
6c97690: feat(FileSystem):
renderBodyslot for wrapping the file arearenderBodydecorates the file area instead of replacing it. WhererenderHeaderandrenderFooterhand you a state snapshot and take whatever you return, this one also hands youstate.content— the active view, or the empty/loading placeholder standing in for it — so a drop hint, an upload overlay or a details rail can sit alongside the four views without reimplementing any of them. Returningstate.contentunchanged is a no-op.<FileSystem renderBody={({ content, isEmpty }) => ( <View className="flex-1"> {content} {isEmpty ? <DropHint /> : null} </View> )} />
The snapshot is the state that produced the content —
currentPath,entries,view,selectedEntry,searchValue,isSearching,hasActiveFilters,isLoadingCurrentFolder,isEmpty— exported asFileSystemBodyState, so a wrapper tracks the same selection and folder the views do without recomputing any of it.isEmptyis not the same as "the placeholder is showing": the columns view keeps its panes over an empty folder, since that is how Finder lets you walk back up a trail, so it only yields to the placeholder while searching or filtering.Unlike the header and footer slots,
renderBodyis called as a plain function rather than mounted as a component. An inline arrow is a new function identity on every render, and a component whose type changes remounts its entire subtree — here that subtree is the active view, so every keystroke in the search field would have reset its scroll offset, its panes and any in-flight drag. Calling it keeps the returned elements in the parent's own tree, where reconciliation compares them by position as usual. The consequence for callers: don't call hooks directly insiderenderBody— put them in a component you render inside the returned tree.The wrapper renders inside the file-area node rather than around it, so
bodyClassNamestill applies and the area keeps its flex sizing and web text-selection guard however you nest things. Give the returned treeflex-1(orsize-full) if it should fill the area the way the built-in views do. -
6c97690: feat(elevated): export
SURFACE_CLASSNAME, and drop the built-in frame fromFileSystemand theAdaptiveDropdownpanelNew
SURFACE_CLASSNAMEonrn-motion-ui/elevated— a level-indexed map pairing each surface background with the matching elevation shadow, so a custom surface can take both halves of the ladder at one level without callingsurfaceBackgroundandelevatedShadowseparately.import { SURFACE_CLASSNAME } from "rn-motion-ui/elevated"; <View className={SURFACE_CLASSNAME[5]} />; // bg-surface-5 shadow-elevated-5
It is a plain record, not a function, so it is indexed rather than clamped:
surfaceBackgroundandelevatedShadowstill take any number and clamp it into range, while an out-of-range index here is a type error and, from untyped JS,undefined. Reach for the functions when the level is computed at runtime.Visual change.
FileSystem's root no longer drawsrounded-xl border border-border, andAdaptiveDropdown's floating panel no longer drawsborder border-border. Both now render an unframed surface, leaving the frame to the container they sit in — aFileSysteminside a card or a pane of its own was stacking two borders, and there was no way to opt out.FileSystemtakes the old chrome back throughclassName="rounded-xl border border-border"; the sharedcnresolves consumer classes last-wins, so it applies. The dropdown panel has no such escape hatch —contentClassNamereaches the body inside the panel, not the panel itself — so its border cannot currently be restored from the outside. It keeps itsrounded-2xland itselevationshadow, which is what separates it from the page.Internally, the per-file
cncopies inCard,SkeletonandAdaptiveModal— each a comment claiming the package ships no sharedcn— are replaced by the realsrc/lib/cn.ts. Those copies only concatenated, so a consumer class and a component default targeting the same utility group both survived into the class string and the winner came down to stylesheet order. They now resolve last-wins in the consumer's favour, which is what their prop docs already promised. -
58c7e45: feat(hooks): export
useSafeInsetsat `rn-motion-ui...
rn-motion-ui@3.3.0
Minor Changes
-
465ac98: feat:
useBreakpoint— width breakpoints without resize re-rendersNew
rn-motion-ui/hooks/use-breakpointexportsuseBreakpoint()and
useBreakpointAtLeast(value). Both subscribe toDimensionsbut store only the
resolved tier, so a component re-renders when the breakpoint flips rather than
on every resize frame the wayuseWindowDimensionsdoes.The scale (
base/sm/md/lg/xl/2xl) mirrors Tailwind's default
screensand is the single source of truth for responsive decisions in the
package — the pure helpers live inrn-motion-ui/breakpointsfor components that
measure their own container instead of the window.Every component that previously hard-coded a cutoff now accepts an override:
AdaptiveModal,FullSheet—wideBreakpoint(default'sm', was a literal 640)AdaptiveDropdown—wideBreakpoint(default'md', was a literal 768)FileSystem—breakpoints={{ minimal, compact, tablet }}for its
container-measured header tiers (defaults 360 / 560 / 768), plus
contextMenuWideBreakpoint(default'md', was a literal 768) for the window
width at which entry context menus open as a cursor-anchored panel rather than
a bottom sheet
Each takes a breakpoint name or a raw pixel number. Defaults are unchanged, so
this is additive. -
ab84da1: One shared box for the whole button family, driven by tokens.
Button,ElevatedButton,GlossyButtonandActionSwapButtonhad each grown their own height/padding/radius table, so anmdof one type didn't line up with anmdof another. They now all read the same geometry fromtokens.css—--spacing-button-{sm,md,lg}(32/40/48px),--spacing-button-pad-{sm,md,lg}(12/16/20px) and--radius-button-{sm,md,lg}(8/10/12px) — so a row of mixed button types has one baseline, and overriding a token retunes every type at once.ActionSwapButtonjoins the family properly: it takes ashapeprop ('pill' | 'rounded', default'pill'so existing buttons look the same), itssizeis now the family'sButtonSize, and its label uses the family's type ramp instead of a duplicate of it.ActionSwapButtonSizeis now an alias ofButtonSizeandActionSwapButtonShapeofButtonShape— both still exported.Visible changes, per type:
Button—mdandlglose 4px of horizontal padding (20→16, 24→20); theroundedshape moves off a flat 12px radius onto the 8/10/12 ramp;icongrows from 32 to 40px so it squares themdheight.ElevatedButton— padding grows 2–4px per size (10→12, 14→16, 16→20);icongrows from 32 to 40px. Radii are unchanged (AlignUI's 8/10/12 is what the shared ramp was drawn from), and its 14px label is still the documented opt-out from the type ramp.GlossyButton—mdgrows 36→40px andlg44→48px to join the family's height ramp; padding drops atmd/lg(20→16, 24→20) and grows atsm(10→12);icongrows 36→40px; theroundedshape moves off a flat 12px radius onto the ramp. The 2px inset around the label is gone, so a glossy label sits at the same inset as a flat one.ActionSwapButton— same height and padding as before at every size. Its content gap is now a flat 8px (was 6 atsmand 10 atlg).
Adornment spacing is one value across the family now (8px).
ElevatedButtonpreviously spaced its content at 12px and pulled icons back in by 4px, which netted the same 8px beside a label — the difference only showed with two adornments.StatefulButton's success/error padding squeeze is derived from the shared padding rather than tabulated, so it stays proportional if a token is overridden. -
de66bc8: feat(ui):
FileSystemheadless header/footer slots + per-region classNamesrenderHeaderandrenderFooterreplace the built-in toolbar and status bar
with your own UI. Each receives the same state the default region renders from,
so a custom header wires navigation, search, sort and filters without
reimplementing any of the logic:<FileSystem items={items} renderHeader={({ folderName, canGoBack, goBack, searchValue, setSearchValue, layout }) => ( <MyToolbar … /> )} />
The state shapes are exported as
FileSystemHeaderStateand
FileSystemStatusState. Both include the responsive hints the built-in header
uses (layout,isCompact), so a custom region can collapse at the same widths.For the common case of restyling rather than replacing, four class hooks merge
onto the built-in regions:headerClassName,bodyClassName,footerClassName
and the existingclassName. The tworender*props take precedence over their
matching*ClassName.Defaults are unchanged — omit everything and the component renders exactly as
before. -
19c5bbc:
HoverMenu's render-proptriggernow receives{ open, toggle }instead of just{ open }, matchingAdaptiveDropdown. A trigger that is pressable in its own right (aButton, aPressable) claims the press, so the wrapper's own toggle never fires —toggleis what lets such a trigger open the menu. Also addstriggerIsPressable: set it and the wrapper drops its button role,aria-expanded,onPressand tab stop, since the trigger already carries all four. Without it, web renders a<button>inside a<button>and keyboard users get two tab stops for one control. Hover stays on the wrapper either way, so web hover-open is unaffected. Both are additive — a plain node trigger keeps the wrapper-owns-the-press behaviour unchanged.Stories: add the
glossytrigger kind to the shared storyTriggerButton, which gives every overlay playground that showcases trigger variants (ActionFeedbackModal, AdaptiveModal, BottomSheet, CommandPalette, FullSheet, MorphingModal) a GlossyButton chip. TheHoverMenuandAdaptiveDropdownplaygrounds gain that same Trigger chip row, so all four launch styles can be swapped under one live overlay; each keeps its previous plain-node trigger in a section of its own to demonstrate the wrapper-owns-the-press path. -
f2d4ba4: feat(overlays): safe-area insets on by default for full-screen overlays
FullSheet,BottomSheet,Drawer, andAdaptiveModalnow accept asafeAreaprop (defaulttrue) that applies device safe-area insets — status-bar top and home-indicator bottom — to the overlay content.When
react-native-safe-area-contextis installed and a<SafeAreaProvider>is present in the tree, real device insets are used. If the package is absent, insets fall back to zero so existing consumers without it are unaffected.Pass
safeArea={false}to opt out and manage insets yourself. -
8d996ce: Breaking —
StatefulButton'selevatedprop is replaced bychip.elevatedwas a boolean with one alternative to the flat button; there are now two chip keys, so the flag becomes a mode:-<StatefulButton elevated onPress={submit}>Save</StatefulButton> +<StatefulButton chip="elevated" onPress={submit}>Save</StatefulButton>
Omitting
chiprenders the flat button, exactly as omittingelevateddid.elevatedis gone rather than deprecated — it shipped one release ago in 3.2.0, and keeping a boolean that means "one particular chip" beside the mode it is a subset of reads worse than the rename costs.The new value is
chip="glossy": theGlossyButtonkey (domed SVG gradient, inset bevel, OKLCH-derived cast) driven through the same machine. Either key keeps its full appearance through loading/success/error instead of greying out, and each state adopts the matching variant — idle/loading map the flat variant onto that key's palette (danger family →danger,special/inversecarry over, everything else → the key's neutral fill), success switches to thesuccesskey, error to thedangerkey. Full fill, gloss, rim and cast, not a flat overlay: neither chip paints the flat button's crossfaded colour plate, because it has a variant to switch instead. Glossy dims whole-key via opacity rather than recolouring its label, so its idle content colour comes fromglossyContentColorand holds constant across states.The success/error horizontal padding squeeze is now derived from the family's shared
--spacing-button-pad-*rather than tabulated per size, so retuning a padding token keeps the squeeze proportional. -
fd1d111:
Tabsgains a choice of content-panel animation.contentAnimationonTabssets it for every panel, andanimationon a singleTabsContentoverrides it for that panel only:fade(default) — the existing cross-fade with a 4 px settle, unchanged, so nothing shifts for current consumers.slide— the panel you land on travels a full container width in from the side the selection moved towards, while the panel you left is pushed out the opposite way, so the pair reads as one page displacing another rather than as a nudge. Sized for mobile screens and modals. Direction is read off the triggers' measured rects rather than the order the panels were declared in, so it also holds for controlled changes: a programmatic jump to a tab slides the same way a press on that tab would. Travel distance is measured on theTabsroot, so the first panel — which has no previous page to push out — just fades in.dropIn— the panel falls from above on a springy scale-up.
fadeanddropInare enter-only:TabsContentrenders nothing for the tab it isn't showing, so a switch is an unmount plus a fresh mount, with no exiting layer to co-ordinate.slideis the exception, since a page swap only reads as one if the page you left is visibly pushed aside. The outgoing panel keeps its subtree mounted for the length of the push, leaves the layout flow immediately so it can't displace the panel replacing it, and finishes the trip as an absolutely positioned layer over the spot it held — hidden from assistive tech and non-interactive while it travels, t...