layout-pusher
v1.0.4
Published
A React library for stack-based layout navigation with mobile-like animations and transitions
Maintainers
Readme
Layout Pusher
A React library for stack-based layout navigation with smooth, mobile-like animations. Inspired by iOS and Telegram navigation patterns.
Features
- Stack-based Navigation - Push and pop layouts like native mobile apps
- Smooth Animations - Fade-focused transitions with subtle transforms (Telegram-style)
- Gesture Support - Swipe from left edge to go back
- State Preservation - Component state persists when navigating back
- TypeScript Ready - Full type definitions included
- Lightweight - ~7KB gzipped, zero dependencies (except peer deps)
- Customizable - Multiple animation presets and custom configurations
Installation
npm install layout-pusher framer-motionyarn add layout-pusher framer-motionpnpm add layout-pusher framer-motionPeer Dependencies
Layout Pusher requires the following peer dependencies:
react>= 18.0.0react-dom>= 18.0.0framer-motion>= 10.0.0
Quick Start
import { LayoutPusherProvider, LayoutStack, LayoutViewProps } from 'layout-pusher';
// Define your layouts
function HomeScreen({ push }: LayoutViewProps) {
return (
<div>
<h1>Home</h1>
<button onClick={() => push(DetailScreen, { id: 123 })}>
View Details
</button>
</div>
);
}
function DetailScreen({ data, goBack }: LayoutViewProps<{ id: number }>) {
return (
<div>
<button onClick={goBack}>Back</button>
<h1>Detail #{data?.id}</h1>
</div>
);
}
// Set up your app
function App() {
return (
<LayoutPusherProvider initialLayout={HomeScreen}>
<LayoutStack style={{ width: '100%', height: '100vh' }} />
</LayoutPusherProvider>
);
}Core Concepts
The Layout Stack
Layout Pusher manages a stack of layouts. When you "push" a new layout, it animates in on top of the current one. When you "pop", the top layout animates out, revealing the previous one.
┌─────────────────┐
│ Layout C │ ← Top (visible)
├─────────────────┤
│ Layout B │ ← Faded/dimmed
├─────────────────┤
│ Layout A │ ← More faded
└─────────────────┘Layout Props
Every layout component receives these props automatically:
interface LayoutViewProps<T = unknown> {
layoutId: string; // Unique ID for this layout instance
data?: T; // Props passed when pushing this layout
goBack: () => void; // Pop this layout
push: (Component, props?) => string; // Push a new layout
replace: (Component, props?) => string; // Replace current layout
popTo: (id: string) => void; // Pop to a specific layout
popToRoot: () => void; // Pop all layouts except the first
canGoBack: boolean; // Whether there's a layout to go back to
depth: number; // Current stack depth
}API Reference
Components
<LayoutPusherProvider>
The root provider component that manages the navigation stack.
<LayoutPusherProvider
initialLayout={HomeScreen} // Initial layout component
initialProps={{ user: 'John' }} // Props for initial layout
config={{
animation: 'slide', // 'slide' | 'fade' | 'scale' | 'none'
animationDuration: 300, // Duration in milliseconds
gestureEnabled: true, // Enable swipe-back gesture
preserveState: true, // Preserve component state
}}
>
{children}
</LayoutPusherProvider><LayoutStack>
The container that renders the stack of layouts with animations.
<LayoutStack
className="my-stack"
style={{ width: '100%', height: '100vh' }}
/><LayoutHeader>
An optional header component with built-in back button.
<LayoutHeader
title="Settings"
showBackButton={true}
onBack={() => console.log('Going back')}
rightAction={<button>Save</button>}
/>Hooks
useLayoutPusher()
The main hook for navigation actions.
function MyComponent() {
const {
push, // Push a new layout
pop, // Go back one layout
popTo, // Go back to a specific layout by ID
popToRoot, // Go back to the first layout
replace, // Replace current layout
canGoBack, // Boolean: can we go back?
depth // Current stack depth
} = useLayoutPusher();
return (
<button onClick={() => push(OtherScreen, { foo: 'bar' })}>
Navigate
</button>
);
}useLayoutStack()
Read-only access to the stack state.
function MyComponent() {
const {
stack, // Array of all layouts in the stack
currentLayout, // The top layout
depth // Stack depth
} = useLayoutStack();
return <div>Current depth: {depth}</div>;
}useLayoutAnimation()
Access to animation state (useful for custom animations).
function MyComponent() {
const {
isPushing, // Currently pushing a layout
isPopping, // Currently popping a layout
direction, // 'forward' | 'backward' | null
animationType, // Current animation type
animationDuration // Current duration
} = useLayoutAnimation();
return null;
}Navigation Methods
push()
Push a new layout onto the stack.
// Basic push
push(ProfileScreen);
// Push with data
push(ProfileScreen, { userId: 123 });
// Push returns the layout ID
const layoutId = push(ProfileScreen, { userId: 123 });pop()
Remove the top layout from the stack.
goBack(); // or pop() from useLayoutPusherpopTo()
Pop to a specific layout by its ID.
const homeId = push(HomeScreen);
push(SettingsScreen);
push(ProfileScreen);
// Later: go directly back to home
popTo(homeId);popToRoot()
Pop all layouts except the first one.
// From any depth, go back to the root
popToRoot();replace()
Replace the current layout without animation.
// Replace current layout
replace(NewScreen, { data: 'value' });Configuration
Animation Types
<LayoutPusherProvider config={{ animation: 'slide' }}>| Type | Description |
|------|-------------|
| slide | Slide from right with fade (default, Telegram-style) |
| fade | Simple opacity fade |
| scale | Scale up/down with fade |
| none | No animation |
Custom Timing
<LayoutPusherProvider
config={{
animationDuration: 400, // milliseconds
}}
>Gesture Control
<LayoutPusherProvider
config={{
gestureEnabled: true, // Enable swipe-back from left edge
}}
>Styling
CSS Variables
Customize the appearance using CSS variables:
:root {
/* Stack container background (visible during transitions) */
--layout-pusher-bg: #000000;
/* Individual layout background */
--layout-pusher-layout-bg: #ffffff;
/* Header styles */
--layout-pusher-header-bg: #ffffff;
--layout-pusher-header-border: #e0e0e0;
--layout-pusher-header-text: #333333;
--layout-pusher-header-hover: rgba(0, 0, 0, 0.05);
--layout-pusher-header-active: rgba(0, 0, 0, 0.1);
}Dark Mode Example
:root {
--layout-pusher-bg: #0a0a0a;
--layout-pusher-layout-bg: #1a1a1a;
--layout-pusher-header-bg: #1a1a1a;
--layout-pusher-header-border: #333333;
--layout-pusher-header-text: #ffffff;
}TypeScript
Layout Pusher is written in TypeScript and exports all types:
import type {
LayoutViewProps,
LayoutItem,
LayoutPusherConfig,
LayoutPusherContextValue,
LayoutHeaderProps,
LayoutStackProps,
AnimationType,
} from 'layout-pusher';
// Type your layout data
interface UserData {
userId: number;
name: string;
}
function UserProfile({ data }: LayoutViewProps<UserData>) {
// data is typed as UserData | undefined
return <div>Hello, {data?.name}</div>;
}
// Push with type safety
push(UserProfile, { userId: 1, name: 'John' });Advanced Usage
Nested Navigation
You can nest multiple LayoutPusherProviders for complex navigation:
function MainApp() {
return (
<LayoutPusherProvider initialLayout={TabNavigator}>
<LayoutStack />
</LayoutPusherProvider>
);
}
function TabNavigator() {
const [tab, setTab] = useState('home');
return (
<div>
<div className="tab-content">
{tab === 'home' && (
<LayoutPusherProvider initialLayout={HomeScreen}>
<LayoutStack />
</LayoutPusherProvider>
)}
{tab === 'settings' && (
<LayoutPusherProvider initialLayout={SettingsScreen}>
<LayoutStack />
</LayoutPusherProvider>
)}
</div>
<TabBar value={tab} onChange={setTab} />
</div>
);
}Programmatic Navigation
Use the hook outside of layout components:
function NavigationButton() {
const { push, canGoBack, goBack } = useLayoutPusher();
return (
<div>
{canGoBack && <button onClick={goBack}>Back</button>}
<button onClick={() => push(NextScreen)}>Next</button>
</div>
);
}Animation Presets
Access built-in animation presets:
import {
slidePreset,
fadePreset,
scalePreset,
telegramSpring,
telegramTween,
} from 'layout-pusher';
// Use in custom Framer Motion components
<motion.div
initial={slidePreset.initial}
animate={slidePreset.animate}
exit={slidePreset.exit}
transition={telegramSpring}
/>Examples
Basic Chat App Structure
function ChatList({ push }: LayoutViewProps) {
const chats = [...];
return (
<div>
{chats.map(chat => (
<div
key={chat.id}
onClick={() => push(ChatView, { chatId: chat.id })}
>
{chat.name}
</div>
))}
</div>
);
}
function ChatView({ data, goBack, push }: LayoutViewProps<{ chatId: number }>) {
return (
<div>
<header onClick={() => push(ChatInfo, { chatId: data?.chatId })}>
<button onClick={goBack}>Back</button>
<span>Chat {data?.chatId}</span>
</header>
<MessageList chatId={data?.chatId} />
</div>
);
}
function ChatInfo({ data, goBack, popToRoot }: LayoutViewProps<{ chatId: number }>) {
return (
<div>
<button onClick={goBack}>Back</button>
<h1>Chat Info</h1>
<button onClick={popToRoot}>Back to Chat List</button>
</div>
);
}With Header Component
function SettingsScreen({ goBack }: LayoutViewProps) {
return (
<div>
<LayoutHeader
title="Settings"
rightAction={
<button onClick={handleSave}>Save</button>
}
/>
<div className="content">
{/* Settings content */}
</div>
</div>
);
}Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- iOS Safari (latest)
- Android Chrome (latest)
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
License
MIT License - see LICENSE for details.
Changelog
1.0.2
- Initial public release
- Telegram-style fade animations
- Swipe-back gesture support
- TypeScript definitions
- CSS variable theming
