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, then unmounted. Underprefers-reduced-motionevery animation collapses to the same plain opacity fade with no exit layer at all — the cross-fade is information, the transforms are decoration.A full-width slide has to be clipped or the travelling pages paint outside the
Tabsbox, soslidepanels mount inside anoverflow: hiddenwrapper. The clip is scoped to the motion: an arriving panel lifts it once it has landed, which keeps shadows and any overlay a panel raises inline from being cut off for the rest of the panel's life, and a departing panel simply stays clipped until it unmounts.contentTransitionis the matching escape hatch, partial likeindicatorTransition: pass only the fields you want changed and the rest come from that animation's default (180 ms timing forfade, 280 ms linear forslide, a spring fordropIn).Story: the playground gains a Content animation chip row wired to the live controlled set, a section with one tab set per animation for clicking through them side by side, and a modal-width sample where the full-width slide reads properly.
Demo: Slide (both directions)tours forward and back at that width. -
0cae697: Per-entry
testIDs in the file browsers, so every row/tile is addressable on its own. The id is keyed by the path that already identifies the entry (folders keep their trailing slash), the wayTablekeys rows by id. No new props: the ids derive from the component's roottestID, falling back to the component name when it is omitted.FileTree— each row is${testID ?? 'file-tree'}-row-${path}. A row pinned by the sticky headers is a second copy of a row that may also be in the list below it, so it takes${testID ?? 'file-tree'}-sticky-row-${path}instead — one query resolves to one node, whichever copy a test means. Previously every row shared a single${testID}-row.FileSystem— each entry is${testID ?? 'file-system'}-entry-${path}, the same id in all four views (list rows, icons tiles, columns rows, gallery filmstrip tiles), so a test that switches views keeps its queries.
Additional per-item
testIDs, filling the gaps left by the previous release:CardChoice— acceptstestIDand forwards it to the cardPressable(standalone or inside aCardChoiceGroup). Inside a group it now defaults to${group testID ?? 'card-choice-group'}-card-${value}, keyed by thevaluethat already identifies the card, so cards are addressable without threading ids through each one. The radio ring is-ring, its standalone dot-dot, the badge-badge, and the group's gliding indicator${testID ?? 'card-choice-group'}-indicator. A standalone card has no group and novalueto key on, so its inner ids only appear when you pass atestID.RadioGroupItem— each item defaults to${group testID ?? 'radio-group'}-item-${value}, with the ring at-controland the group's gliding indicator at${testID ?? 'radio-group'}-indicator. Previously only an explicitly passedtestIDreached the item'sPressable.CommandItem— new optionaltestIDfield; forwarded to each row'sPressableinCommandPalette.BouncyAccordionItem— new optionaltestIDfield; forwarded to each row's triggerPressable.TabsContent— acceptstestIDand forwards it to the content wrapper.
-
a981e3b: New
ThemedIconatrn-motion-ui/iconwraps any icon fromrn-motion-ui/iconsand resolves its stroke colour from the active theme, so an icon can be placed by the name of the surface it sits on rather than by a colour threaded down from a hook call.Two ways to name that colour,
tokenwinning if both are given:varianttakes anyButtonVariantorElevatedVariantname and maps it to that fill's legible partner —variant="primary"givesprimary-foreground,variant="ghost"givesmuted-foreground,variant="success"givessuccess-foreground, and the outline/ghost danger variants give thedangerhue itself since there is no fill to sit on. The mapping is the same oneElevatedButton'selevatedContentColorandButton's label cva already use, so an icon passed as a button adornment lands on the colour that button's own label would. Defaults tosecondary, i.e. the plainforegroundtoken.tokenskips the lookup and resolves aThemeTokendirectly, for icons whose colour isn't a button variant — asuccess-foregroundcheck inside a green circle, or a colour that flips between two tokens on a state,token={isActive ? 'foreground' : 'muted-foreground'}.
Everything else in
IconProps(size,strokeWidth,style,accessibilityLabel) is forwarded untouched.Internally, the components that were each calling
useThemeColor/useThemeColorssolely to hand a colour to an icon now use it instead:ActionFeedbackModal,BloomMenu,BouncyAccordion,CommandPalette,FeedbackWidget,FileTree's search input,Input,OtpInput,OverflowActions,Table's pagination footer, and theFileSystemtoolbar, header, list view, menus, filter menu, filter pills, and date-range modal. Rendered colours are unchanged. Where the icon wanted theforegroundtoken anyway, the wrapper is dropped altogether — icons already fall back toforegroundwhen given nocolor, as inBloomMenu's cells.MenuRowinMultiStepMenugainsiconColor, defaulting to thewhiteit previously hard-coded. That default is right for the vivid iOS-style icon squares the row is built around, buticonBackgroundColoris a free-form colour, and a pale or neutral fill needs a darker icon to stay legible. Its active label also moves from a literaltext-whitetotext-primary-foreground, which is the same colour but follows the theme.
Patch Changes
- ab84da1:
GlossyButtonlabels now use the Button family's type ramp instead of their own. The ramp moves toLABEL_TEXT_CLASSinbutton-scale.ts, and bothButton'slabelcva andGlossyButtonread it, so a glossymdrenders the same text as a flatmd— which is whatStatefulButton'schip="glossy"was already doing for its rolling label. Visible change: glossy labels gofont-mediumon thetext-xs/text-sm/text-baseramp rather thanfont-normalat a fixed 17px (14px atsm).ElevatedButtonis unchanged — AlignUI pins its chips to 14px at every size.