npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rn-motion-ui

v4.0.0

Published

Animated React Native + React Native Web UI components, styled with Tailwind via uniwind and powered by Moti/Reanimated.

Readme

rn-motion-ui

Animated React Native + React Native Web UI components, styled with Tailwind via uniwind and powered by an in-package Moti / Reanimated 4 layer.

The components are written as React Native primitives, so they run on native (iOS/Android via Expo or bare RN) and on the web through react-native-web.

Install

npm install rn-motion-ui
# or: bun add rn-motion-ui

Peer dependencies

Install these in your app (they are not bundled):

npm install react react-native react-native-reanimated react-native-gesture-handler

| Peer | Supported range | | --- | --- | | react | >=18.0.0 | | react-native | >=0.76.0 | | react-native-reanimated | >=4.0.0 | | react-native-gesture-handler | >=2.0.0 |

Consumer setup

This package ships TypeScript source (no prebuilt bundle). Your bundler already transpiles your own app code, so it will transpile these components the same way — but two build-time plugins are required for the components to work correctly:

  1. react-native-reanimated/plugin (Babel) — transforms Reanimated worklets. Without it, easing/animation worklets throw at runtime.
  2. uniwind — resolves the className props on RN components to styles at build time. Without it, className is ignored.

Native (Expo / Metro)

babel.config.js:

module.exports = {
  presets: ['babel-preset-expo'],
  plugins: ['react-native-reanimated/plugin'],
};

Set up uniwind per its docs (Metro transformer + a global.css Tailwind entry).

Web (Vite / react-native-web)

Alias react-nativereact-native-web, run the Reanimated Babel plugin through your React transform, and add the uniwind Vite plugin. See this repo's storybook/web/.storybook/main.ts for a working reference config.

Usage

Every module is a subpath export — import directly from its path. There is no barrel index export by design, so bundlers only pull in what you import.

import { Button } from 'rn-motion-ui/button';
import { Switch } from 'rn-motion-ui/switch';
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'rn-motion-ui/tabs';
import { useReducedMotion } from 'rn-motion-ui/hooks/use-reduced-motion';
import { MotiView } from 'rn-motion-ui/moti/view';
import { AnimatePresence } from 'rn-motion-ui/moti/presence';
import { hasKey } from 'rn-motion-ui/utils/typeguards';

Available exports

Subpaths are namespaced by category:

  • UI components — flat, e.g. rn-motion-ui/button, rn-motion-ui/tabs, rn-motion-ui/bottom-sheet, …
  • Hooksrn-motion-ui/hooks/<name> (use-reduced-motion, use-mount-effect, use-in-view, …)
  • Moti primitivesrn-motion-ui/moti/<name> (view, text, motify, presence, pressable, …)
  • Utilsrn-motion-ui/utils/typeguards

UI components

| Subpath | Component / hook | | --- | --- | | /button | Button | | /elevated-button | ElevatedButton | | /stateful-button | StatefulButton | | /glossy-button | GlossyButton | | /switch | Switch | | /checkbox | Checkbox | | /checkbox-card | CheckboxCardGroup, CheckboxCard | | /radio | Radio | | /radio-card | RadioCardGroup, RadioCard | | /star-rating | StarRating | | /input | Input | | /tabs | Tabs, TabsList, TabsTrigger, TabsContent | | /card | Card | | /text | Text | | /skeleton | Skeleton | | /loader | Loader | | /animated-badge | AnimatedBadge | | /animated-number | AnimatedNumber | | /number-ticker | NumberTicker | | /text-cascade | TextCascade | | /text-reveal | TextReveal | | /text-rolling | TextRolling | | /text-shimmer | TextShimmer | | /marquee | Marquee | | /scroll-reveal | ScrollReveal | | /scroll-progress | ScrollProgress | | /scroll-to | ScrollTo | | /smooth-scroll | SmoothScroll | | /dock | Dock | | /popover | Popover | | /drawer | Drawer | | /bottom-sheet | BottomSheet | | /full-sheet | FullSheet | | /morphing-modal | MorphingModal | | /adaptive-modal | AdaptiveModal | | /action-feedback-modal | ActionFeedbackModal | | /adaptive-dropdown | AdaptiveDropdown | | /menu-item | MenuItem | | /multi-step-menu | MultiStepMenu | | /bloom-menu | BloomMenu | | /hover-menu | HoverMenu | | /overflow-actions | OverflowActions | | /bouncy-accordion | BouncyAccordion | | /dynamic-island | DynamicIsland | | /action-swap | ActionSwap | | /animated-list | AnimatedList, AnimatedListItem | | /swipeable-list | SwipeableList | | /range-slider | RangeSlider | | /cylinder-carousel | CylinderCarousel | | /wheel-picker | WheelPicker | | /command-palette | CommandPalette | | /otp-input | OtpInput | | /feedback-widget | FeedbackWidget | | /table | Table | | /table-header | HeaderCell | | /table-row | TableRow, RowCell | | /table-types | table type definitions | | /table-utils | sorting / filtering helpers | | /use-table | useTable | | /use-column-reorder | useColumnReorder | | /file-tree | FileTree, useFileTree | | /file-system | FileSystem | | /icons | icon components | | /icon | ThemedIcon | | /ease | easing constants | | /tokens.css | design token stylesheet | | /theme/use-theme-color | useThemeColor, useThemeColors | | /breakpoints | breakpoint scale + pure resolvers | | /hooks/use-breakpoint | useBreakpoint, useBreakpointAtLeast |

Responsive breakpoints

Every responsive decision in the library resolves against one scale, which mirrors Tailwind's default screens so a md: class and a useBreakpoint() === 'md' check flip at the same width:

| Name | Min width | | --- | --- | | base | 0 | | sm | 640 | | md | 768 | | lg | 1024 | | xl | 1280 | | 2xl | 1536 |

useBreakpoint

Returns the current tier and re-renders only when that tier changes. useWindowDimensions pushes a render for every resize frame — dragging a window edge across 200 px re-renders the whole subtree ~200 times even though the layout decision never moved. This subscribes to the same Dimensions event but stores only the resolved breakpoint.

import { useBreakpoint, useBreakpointAtLeast } from 'rn-motion-ui/hooks/use-breakpoint';

const breakpoint = useBreakpoint();               // 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
const breakpoint = useBreakpoint({ md: 720 });    // move just the md edge
const isWide = useBreakpointAtLeast('md');        // one cutoff, boolean, flips once
const isWide = useBreakpointAtLeast(900);         // raw pixels work too

Overrides are matched by value, not object identity, so an inline {{ md: 720 }} literal doesn't re-subscribe on every render.

Per-component overrides

Components that pick a layout from the window width take a wideBreakpoint prop — a breakpoint name or a raw pixel number:

| Component | Prop | Default | | --- | --- | --- | | AdaptiveModal | wideBreakpoint | 'sm' (640) | | FullSheet | wideBreakpoint | 'sm' (640) | | AdaptiveDropdown | wideBreakpoint | 'md' (768) |

<AdaptiveModal wideBreakpoint="md" … />   {/* stay narrow until 768 */}
<AdaptiveDropdown wideBreakpoint={900} … />

AdaptiveModal also keeps isWideScreen to bypass width entirely, and MultiStepMenu still receives isWideScreen from its caller.

FileSystem is the exception: it adapts to its own measured width, not the window's, so it collapses inside a narrow parent too. Its tiers are container widths and are overridden as a group:

<FileSystem breakpoints={{ minimal: 320, compact: 520, tablet: 720 }} … />

Measuring your own container

The resolvers are pure and React-free, so a component that measures itself can reuse the same thresholds:

import { breakpointForWidth, isWidthAtLeast } from 'rn-motion-ui/breakpoints';

const tier = breakpointForWidth(measuredWidth);          // 'md'
const isWide = isWidthAtLeast(measuredWidth, 'lg');      // boolean

FileTree

A path-first file tree — the path is the identity, there are no numeric IDs. Feed it a flat list of paths; missing ancestor directories are inferred, single-child folder chains are flattened, and rows sort dirs-before-files in natural order. Works on native and web (react-native-web): tap to select/expand, long-press to multi-select or open context actions (or, when draggable, to pick a row up and drag it — on web a mouse just click-drags, no hold), and on web the usual Ctrl/Shift-click, right-click, double-click, and keyboard navigation all work.

import { FileTree } from 'rn-motion-ui/file-tree';

<FileTree
  paths={['src/app/index.tsx', 'src/components/button.tsx', 'README.md']}
  gitStatus={{ 'src/app/index.tsx': 'M', 'README.md': 'A' }}
  showSearch
  onSelectionChange={(paths) => console.log(paths)}
/>;

Directories carry a trailing slash in every callback ('src/'), files never do. Selection, expansion, and search are each controllable (selectedPaths + onSelectionChange, etc.) or uncontrolled (defaultSelectedPaths), mirroring Table. Opt into draggable for drag-to-move (emits onMove with a full old→new remap) and renamable for inline rename (onRename).

Imperative escape hatch

For power users, useFileTree() returns a stable FileTreeController you can drive directly and hand to <FileTree model={...} />. When a model is passed it owns all state, so the declarative data/state props are ignored while callbacks still fire.

import { FileTree, useFileTree } from 'rn-motion-ui/file-tree';

function Explorer() {
  const tree = useFileTree({ paths, initialExpansion: 'open', selectionMode: 'multiple' });
  return <FileTree model={tree} onSelectionChange={(paths) => save(paths)} />;
}

Theming

Design tokens

The library ships a canonical Tailwind token sheet that you import once in your app's global CSS:

@import "tailwindcss";
@import "rn-motion-ui/tokens.css";

This registers 13 semantic color tokens as Tailwind CSS utilities (bg-primary, text-foreground, border-border, …) and applies automatic dark-mode overrides via @media (prefers-color-scheme: dark).

Token reference

| Token | Light | Dark | Usage | | --- | --- | --- | --- | | surface | oklch(99% 0 0) | oklch(9% 0 0) | Page / screen background | | foreground | oklch(15% 0 0) | oklch(96% 0 0) | Default text | | card | oklch(97% 0 0) | oklch(13% 0 0) | Card / panel surfaces | | muted | oklch(97% 0 0) | oklch(16% 0 0) | Subtle backgrounds | | muted-foreground | oklch(50% 0 0) | oklch(60% 0 0) | Secondary / placeholder text | | border | oklch(15% 0 0 / 0.06) | oklch(99% 0 0 / 0.08) | Dividers and outlines | | primary | oklch(15% 0 0) | oklch(96% 0 0) | Primary actions / fills | | primary-foreground | oklch(99% 0 0) | oklch(15% 0 0) | Text on primary | | secondary | oklch(97% 0 0) | oklch(18% 0 0) | Secondary fills | | secondary-foreground | oklch(15% 0 0) | oklch(96% 0 0) | Text on secondary | | danger | oklch(62% 0.22 25) | oklch(66% 0.22 25) | Error / danger states | | success | oklch(70% 0.18 155) | oklch(72% 0.18 155) | Success states | | warning | oklch(78% 0.18 75) | oklch(80% 0.18 75) | Warning states |

Overriding tokens

Add a @theme block after the import to override any token for your brand:

@import "rn-motion-ui/tokens.css";

@theme {
  /* Brand blue as primary */
  --color-primary:            oklch(52% 0.22 250);
  --color-primary-foreground: oklch(99% 0 0);
}

For manual dark mode (.dark class on <html> instead of prefers-color-scheme):

@import "rn-motion-ui/tokens.css";

/* tokens.css already handles system dark-mode; add .dark support too: */
@layer base {
  .dark {
    --color-primary: oklch(70% 0.22 250);
  }
}

Retinting the neutral palette

The neutral ramp (surfaces, foregrounds, borders, muted) is not pure grey — every step carries a faint shared tint, declared in the sheet as --neutral-hue / --neutral-chroma. Those two are documentation, not inputs: uniwind folds every color to a hex at bundle time, and a var() inside oklch() never folds on native, so the tint has to be baked into the literals. To change it, generate your own sheet:

npx rn-motion-ui-tokens --hue 250 --chroma 0.006

That writes a tokens.css into the current directory. Import it instead of rn-motion-ui/tokens.css — it's a full replacement, not an overlay:

@import "tailwindcss";
@import "./tokens.css";

Every neutral-tinted literal is rewritten to the new hue, with each token's chroma scaled proportionally so partial tints stay partial. Status colors (danger, success, warning, info) and true achromatic values are left alone, as are the shadow recipes, all three theme blocks, and anything you'd added upstream. --chroma 0 drops the tint for a strictly neutral grey ramp. Run it with no arguments and you get the shipped sheet back verbatim.

| Flag | Default | | --- | --- | | --hue <deg> | shipped --neutral-hue; cyclic, so -20 and 400 both work | | --chroma <c> | shipped --neutral-chroma; 00.4 | | --out <path> | ./tokens.css | | --force, -f | refuse to overwrite unless passed | | --stdout | write to stdout instead of a file |

Regenerate after upgrading rn-motion-ui so new tokens land in your copy.

Animated and SVG colors — useThemeColor

Reanimated worklets and react-native-svg props require resolved color strings, not CSS utility classes. Use useThemeColor to read the current token value at runtime:

import { useThemeColor } from 'rn-motion-ui/theme/use-theme-color';
import { useAnimatedStyle } from 'react-native-reanimated';

function Spinner() {
  const color = useThemeColor('primary');
  const style = useAnimatedStyle(() => ({ borderColor: color }));
  return <Animated.View style={[styles.ring, style]} />;
}

On web it reads the live CSS custom property (--color-primary) so consumer @theme overrides are automatically respected. On native it uses useColorScheme() to select from the static light/dark maps, matching the values in tokens.css with no provider required.

Use useThemeColors() when you need several tokens at once:

import { useThemeColors } from 'rn-motion-ui/theme/use-theme-color';

const { primary, danger, success } = useThemeColors();

License

MIT © Ivan Stepanian