@patternmode/stacksheet
v1.3.6
Published
Typed, animated sheet stack system. Zustand store + Motion animations with Apple-style depth stacking.
Maintainers
Readme
Stacksheet
Typed, animated sheet stacking for React.
Stacksheet gives you a provider and a hook for opening, pushing, replacing, and navigating Sheets from anywhere in your app. It handles the motion, layering, focus management, scroll locking, and mobile/desktop presentation details for you.
npm install @patternmode/stacksheetPeer dependencies: react >= 18, react-dom >= 18.
What it includes
- Direct-component API:
open(Component, props) - Optional type registry for large apps:
open("user-profile", "u1", data) - Stacked navigation:
open,push,replace,swap,navigate,pop,close - Desktop side Sheets and mobile bottom Sheets from one API
- Built-in focus trapping, Escape handling, scroll lock, and Android back gesture support
- Drag, Swipe, and Snap Point behavior for bottom Sheets
- Classic Layout with auto header, or Composable Layout with
Sheet.*Sheet Parts
Quick start
Create a sheet instance once and use it across your app:
import { createStacksheet } from "@patternmode/stacksheet";
import "@patternmode/stacksheet/styles.css";
export const { StacksheetProvider, useSheet } = createStacksheet();Wrap your app:
import { StacksheetProvider } from "./sheets";
export function App() {
return (
<StacksheetProvider>
<YourRoutes />
</StacksheetProvider>
);
}Open a sheet from any component:
import { useSheet } from "./sheets";
function UserProfile({ userId }: { userId: string }) {
const { close } = useSheet();
return (
<div>
<h2>User {userId}</h2>
<button onClick={close}>Done</button>
</div>
);
}
function ViewProfileButton() {
const { open } = useSheet();
return <button onClick={() => open(UserProfile, { userId: "u_abc" })}>View profile</button>;
}Typed registry mode
If your app has a fixed set of known sheet types, you can pre-register them for string-keyed, compile-time checked actions:
import { createStacksheet } from "@patternmode/stacksheet";
const { StacksheetProvider, useSheet } = createStacksheet<{
"user-profile": { userId: string };
settings: { tab?: string };
}>();
function UserProfile({ userId }: { userId: string }) {
return <div>User {userId}</div>;
}
function Settings({ tab }: { tab?: string }) {
return <div>Settings: {tab}</div>;
}
function App() {
return (
<StacksheetProvider
sheets={{
"user-profile": UserProfile,
settings: Settings,
}}
>
<YourRoutes />
</StacksheetProvider>
);
}
function OpenSettingsButton() {
const { open } = useSheet();
return (
<button onClick={() => open("settings", "settings-root", { tab: "billing" })}>
Open settings
</button>
);
}Composable layout
Classic mode is the default: Stacksheet owns the standard Panel chrome and header around your Sheet content.
If you want full control over Sheet structure, use layout="composable" and build the Sheet with exported Sheet Parts:
import { Sheet } from "@patternmode/stacksheet";
function App() {
return (
<StacksheetProvider layout="composable">
<YourRoutes />
</StacksheetProvider>
);
}
function SettingsSheet() {
return (
<>
<Sheet.Handle />
<Sheet.Header>
<Sheet.Back />
<Sheet.Title>Settings</Sheet.Title>
<Sheet.Close />
</Sheet.Header>
<Sheet.Body>
<div className="p-4">Sheet content</div>
</Sheet.Body>
</>
);
}For custom controls and Sheet metadata inside a Sheet, use useSheetPanel().
Accessibility
The default Panel is rendered as a dialog with focus management, keyboard navigation, and focus restoration built in.
You can set a global label:
createStacksheet({ ariaLabel: "Settings Sheet" });Or override it per sheet:
const { open } = useSheet();
open(UserProfile, { userId: "u_abc" }, { ariaLabel: "User profile for Jane" });In composable mode, use Sheet.Title and Sheet.Description so the Panel gets aria-labelledby and aria-describedby automatically.
Documentation
Full docs, interactive playground, and API reference:
- Getting Started
- Hooks
- Configuration
- Composable Parts
- Accessibility
- Drag and Dismissal
- Styling
- Type Registry
License
MIT
