A production-ready, TypeScript-first React multi-step flow system with animations, navigation history, and customizable layouts
npm install @bkincz/frame



A production-ready, TypeScript-first React multi-step flow system with animations, navigation history, and customizable layouts.
- Multi-Step Flows - Create complex, nested flows with automatic navigation history
- Animations - Smooth GSAP-powered transitions between steps and flows
- Customizable Layouts - Full control via render props or use the built-in responsive grid
- Modal & Fullscreen - Switch between centered modals and fullscreen layouts per flow or step
- Inert Management - Automatically makes background non-interactive in modal mode with exclusion list support
- Navigation Management - Intelligent back/next buttons with flow chaining support
- Type Safety - Full TypeScript support with comprehensive type definitions
- Framework Agnostic - Works with Next.js, Vite, Create React App, and more
- Lifecycle Hooks - React to flow and step events for analytics, data fetching, etc.
``bash`
npm install @bkincz/frameor
yarn add @bkincz/frameor
pnpm add @bkincz/frame
`tsx
// flows/checkout.tsx
import type { FlowDefinition } from '@bkincz/frame'
export const createCheckoutFlow = (): FlowDefinition => ({
flow: {
cart: {
components: [CartStep],
heading: 'Your Cart',
subheading: 'Review your items',
},
payment: {
components: [PaymentStep],
heading: 'Payment',
},
confirmation: {
components: [ConfirmationStep],
heading: 'Order Complete',
},
},
config: {
variant: 'modal', // or 'fullscreen'
},
})
`
`tsx
// app.tsx or main.tsx
import { setFlowRegistry } from '@bkincz/frame'
import { createCheckoutFlow } from './flows/checkout'
setFlowRegistry({
checkout: {
factory: createCheckoutFlow,
title: 'Checkout',
description: 'Complete your purchase',
},
})
`
`tsx
import { FrameContainer } from '@bkincz/frame'
function App() {
return (
<>
>
)
}
`
`tsx
import { FrameAPI } from '@bkincz/frame'
function ProductPage() {
return (
)
}
`
`tsx
import { FrameAPI } from '@bkincz/frame'
// Open a flow (chains to history)
FrameAPI.openFlow('checkout')
FrameAPI.openFlow('checkout', 'payment') // Open at specific step
// Replace current flow (clears history)
FrameAPI.replaceFlow('login')
// Navigate within flow
FrameAPI.nextStep()
FrameAPI.previousStep()
FrameAPI.goBack() // Smart back (previous step or close)
FrameAPI.closeFlow()
// Manage flow history
FrameAPI.clearHistory()
const hasFlowHistory = FrameAPI.hasHistory()
`
Navigate directly to any step while maintaining accurate history:
`tsx
import { FrameAPI } from '@bkincz/frame'
// Skip to any step in the current flow
FrameAPI.goToStep('payment') // Jump from step 1 to payment
FrameAPI.goToStep('confirmation') // Skip ahead
FrameAPI.goToStep('cart') // Jump backward
// Navigate back through your exact path
FrameAPI.goBackInStepHistory() // Returns to previous step in history
// Check and manage step history
if (FrameAPI.hasStepHistory()) {
// Show "undo" or step history back button
}
FrameAPI.clearStepHistory()
`
How Step History Works:
When you skip steps using goToStep(), each visited step is recorded. This allows users to retrace their exact navigation path:
`tsx
// User's navigation: cart → payment → confirmation → cart
FrameAPI.openFlow('checkout', 'cart')
FrameAPI.goToStep('payment') // History: [cart]
FrameAPI.goToStep('confirmation') // History: [cart, payment]
FrameAPI.goToStep('cart') // History: [cart, payment, confirmation]
// Going back retraces the path:
FrameAPI.goBackInStepHistory() // → confirmation
FrameAPI.goBackInStepHistory() // → payment
FrameAPI.goBackInStepHistory() // → cart
`
Step history is automatically cleared when:
- Switching to a different flow
- Closing the frame
- Reopening a closed flow
Define custom layouts directly in your flow or step configuration. This is useful when different flows or steps require completely different visual structures:
`tsx
import type { FlowDefinition, FrameRenderProps } from '@bkincz/frame'
import { DefaultFrameLayout } from '@bkincz/frame'
// Custom layout for a specific flow
const WizardLayout = ({ refs, handlers, state, Frame }: FrameRenderProps) => (
{state.currentStep &&
)
// Minimal layout for confirmation steps
const MinimalLayout = ({ refs, state, Frame }: FrameRenderProps) => (
{state.currentStep &&
)
export const createCheckoutFlow = (): FlowDefinition => ({
flow: {
cart: {
components: [CartStep],
heading: 'Your Cart',
subheading: 'Review your items',
// This step uses the flow-level layout
},
payment: {
components: [PaymentStep],
heading: 'Payment',
// This step uses the flow-level layout
},
confirmation: {
components: [ConfirmationStep],
heading: 'Order Complete',
config: {
layout: MinimalLayout, // Step-specific layout override
},
},
},
config: {
variant: 'modal',
layout: WizardLayout, // Flow-level layout (default for all steps)
},
})
`
Layout Priority:
Layouts are resolved in this order (first match wins):
1. Step-level layout (step.config.layout)flow.config.layout
2. Flow-level layout ()
3. FrameContainer children render prop
4. DefaultFrameLayout (built-in)
This allows you to:
- Set a default layout for an entire flow
- Override specific steps with custom layouts
- Fall back to the FrameContainer's render prop or default layout
Write simple React components for each step:
`tsx
import { Frame } from '@bkincz/frame'
function CartStep() {
return (
<>
{/ Heading and subheading can be set in flow definition or here /}
{/ Your cart UI /}
{/ Navigation automatically manages back/next/close /}
>
)
}
`
Flows can open other flows, maintaining navigation history:
`tsx
function LoginStep() {
return (
<>
{/ Opens nested flow - back button returns to login /}
>
)
}
`
When using modal variant, Frame automatically makes background content non-interactive and hidden from screen readers using the inert attribute and aria-hidden. This ensures proper accessibility and prevents focus from escaping the modal.
Automatic Behavior:
- In modal mode: Background elements become inert (enabled by default)
- In fullscreen mode: No inert management applied
- Frame container is always excluded from inert state
Configuration:
`tsx
// Flow-level configuration
export const createCheckoutFlow = (): FlowDefinition => ({
flow: {
// ... steps
},
config: {
variant: 'modal',
inert: {
enabled: true, // Default: true (only applies in modal mode)
excludeSelectors: [
'#persistent-header', // Keep header interactive
'.always-accessible', // Keep certain elements accessible
'[data-persistent]', // Custom data attributes
],
},
},
})
// Step-level override
export const createCheckoutFlow = (): FlowDefinition => ({
flow: {
payment: {
heading: 'Payment',
components: [PaymentStep],
config: {
variant: 'modal',
inert: {
enabled: true,
excludeSelectors: ['#chat-widget'], // Keep chat widget accessible
},
},
},
},
config: {
variant: 'modal',
},
})
// Disable inert management for a specific step
export const createCheckoutFlow = (): FlowDefinition => ({
flow: {
'special-step': {
heading: 'Special Step',
components: [SpecialStep],
config: {
variant: 'modal',
inert: {
enabled: false, // Disable inert management for this step
},
},
},
},
})
`
Browser Support:
- The inert attribute is natively supported in modern browsers
- For older browsers, consider using a polyfill like wicg-inert
How It Works:
1. When a modal opens, all direct children of
(except the frame) are marked as inert
2. Elements matching excludeSelectors are skipped
3. Script and style elements are automatically excluded
4. When the modal closes or switches to fullscreen, inert state is restored
5. Original inert, aria-hidden, and tabindex values are preserved and restoredFrame Components
$3
-
- Root container
- - Overlay backdrop (modal variant)
- - Content wrapper
- - Close button
- - Step heading
- - Step subheading
- - Two-column grid layout
- - Main content area
- - Sidebar area
- - Navigation container
- - Back button (intelligent auto-hide/close)
- - Next button (intelligent auto-hide/close)
- - Renders step components$3
`tsx
function MyStep() {
return (
Main Content
{/ Your main content /}
{/ Sidebar content (hidden on mobile/tablet, hidden in modal variant) /}
)
}
`$3
`tsx
function MyStep() {
return (
<>
Step Title
{/ Replace default navigation with custom buttons /}
>
)
}
`React Hooks
$3
Get navigation state for custom UI:
`tsx
import { useNavigationState } from '@bkincz/frame'function CustomNavigation() {
const backState = useNavigationState({ direction: 'previous' })
const nextState = useNavigationState({ direction: 'next' })
return (
)
}
`Customization
Frame provides full control over layout structure through render props.
$3
`tsx
`$3
`tsx
{({ refs, handlers, state, Frame }) => (
{state.showOverlay && (
)}
ref={refs.content}
onClick={handlers.stopPropagation}
variant={state.variant}
>
{state.currentStep && (
<>
{state.currentStep.heading && (
{state.currentStep.heading}
)}
>
)}
{/ Custom navigation placement /}
$3
refs - Required element references:
- refs.overlay - Attach to Frame.Overlay (modal variant)
- refs.content - Attach to Frame.Content (required)
- refs.stepWrapper - Attach to Frame.Main (required for transitions)
handlers - Pre-configured event handlers:
- handlers.closeFrame() - Close with animation
- handlers.stopPropagation(event) - Prevent event bubbling
- handlers.handleOverlayClick() - Close on overlay click
state - Current frame state:
- state.isOpen - Whether frame is open
- state.currentFlow - Current flow name
- state.currentStepKey - Current step key
- state.currentStep - Current step definition
- state.variant - Frame variant ('modal' | 'fullscreen')
- state.showOverlay - Whether to show overlay
- state.showSidebar - Whether to show sidebar
Frame - The Frame component with all sub-components$3
Custom layouts must include:
-
- Root container
- - Content container
- - Step wrapper
- - Step renderer
- - Only if state.showOverlay === trueConfiguration
$3
`tsx
interface FrameContainerProps {
debug?: boolean // Enable debug logging (default: false)
children?: FrameRenderFunction // Optional render function for custom layouts
}
`$3
`tsx
interface FlowDefinition {
flow: Record
config?: FlowConfig
onEnter?: (flowName: string) => void
onExit?: (flowName: string) => void
}interface FlowConfig {
variant?: 'modal' | 'fullscreen' // Default: 'fullscreen'
sidebar?: boolean // Default: true (auto-hidden in modal)
layout?: FrameRenderFunction // Custom layout for all steps in flow
inert?: {
enabled?: boolean // Default: true in modal mode
excludeSelectors?: string[] // CSS selectors to exclude from inert
}
}
interface FlowStep {
components: React.ComponentType[]
heading?: string
subheading?: string
config?: {
variant?: 'modal' | 'fullscreen'
sidebar?: boolean
layout?: FrameRenderFunction // Custom layout for this step only
inert?: {
enabled?: boolean // Default: true in modal mode
excludeSelectors?: string[] // CSS selectors to exclude from inert
}
}
onEnter?: (stepKey: string) => void
onExit?: (stepKey: string) => void
}
`$3
`tsx
import {
setFlowRegistry,
registerFlow,
unregisterFlow,
clearFlowRegistry,
getFlowRegistry
} from '@bkincz/frame'// Set entire registry
setFlowRegistry({
checkout: {
factory: createCheckoutFlow,
title: 'Checkout',
description: 'Complete your purchase',
},
login: {
factory: createLoginFlow,
title: 'Login'
},
})
// Add individual flow
registerFlow('signup', {
factory: createSignupFlow,
title: 'Sign Up',
})
// Remove flow
unregisterFlow('signup')
// Clear all
clearFlowRegistry()
// Get current registry
const registry = getFlowRegistry()
`Utility Functions
`tsx
import {
flowExists,
getAvailableFlows,
getFlowMetadata,
getFlowStepKeys,
isValidStepKey,
getFirstStepKey,
getNextStepKey,
getPreviousStepKey,
} from '@bkincz/frame'// Check if flow exists
if (flowExists('checkout')) {
FrameAPI.openFlow('checkout')
}
// Get all flow names
const flows = getAvailableFlows()
// ['checkout', 'login', 'signup']
// Get flow metadata
const metadata = getFlowMetadata('checkout')
// { title: 'Checkout', description: '...' }
// Get step keys for a flow
const steps = getFlowStepKeys('checkout')
// ['cart', 'payment', 'confirmation']
// Validate step
if (isValidStepKey('checkout', 'payment')) {
FrameAPI.openFlow('checkout', 'payment')
}
`Framework Integration
$3
`tsx
// app/layout.tsx
import { FrameContainer, setFlowRegistry } from '@bkincz/frame'
import '@bkincz/frame/styles'
import { createCheckoutFlow } from './flows/checkout'setFlowRegistry({
checkout: {
factory: createCheckoutFlow,
title: 'Checkout',
},
})
export default function RootLayout({ children }) {
return (
{children}
)
}
`$3
`tsx
// pages/_app.tsx
import { FrameContainer, setFlowRegistry } from '@bkincz/frame'
import '@bkincz/frame/styles'
import { createCheckoutFlow } from '../flows/checkout'setFlowRegistry({
checkout: {
factory: createCheckoutFlow,
title: 'Checkout',
},
})
export default function App({ Component, pageProps }) {
return (
<>
>
)
}
`$3
`tsx
// main.tsx or index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { FrameContainer, setFlowRegistry } from '@bkincz/frame'
import '@bkincz/frame/styles'
import { createCheckoutFlow } from './flows/checkout'
import App from './App'setFlowRegistry({
checkout: {
factory: createCheckoutFlow,
title: 'Checkout',
},
})
createRoot(document.getElementById('root')!).render(
)
`TypeScript
Full TypeScript support with comprehensive type definitions:
`tsx
import type {
// Flow types
FlowDefinition,
FlowFactory,
FlowConfig,
FlowStep,
FlowRegistry,
FlowRegistryEntry,
FrameVariant,
// Customization types
FrameRenderProps,
FrameRenderFunction,
FrameRefs,
FrameHandlers,
FrameState,
} from '@bkincz/frame'// Typed flow factory
const createMyFlow: FlowFactory = () => ({
flow: {
step1: {
components: [MyComponent],
heading: 'Step 1',
},
},
config: {
variant: 'modal',
},
})
// Typed custom layout
function CustomLayout(props: FrameRenderProps) {
const { refs, handlers, state, Frame } = props
return (
{state.currentStep && }
)
}
`API Reference
$3
Flow Navigation:
-
openFlow(flowName, stepKey?) - Open flow (chains to history)
- replaceFlow(flowName, stepKey?) - Replace flow (clears history)
- closeFlow() - Close current flow
- goBack() - Smart back navigationStep Navigation:
-
nextStep() - Navigate to next step
- previousStep() - Navigate to previous step
- goToStep(stepKey) - Skip to any step (tracks history)
- goBackInStepHistory() - Go back through step historyHistory Management:
-
hasHistory() - Check if flow history exists
- clearHistory() - Clear flow navigation history
- hasStepHistory() - Check if step history exists
- clearStepHistory() - Clear step navigation history$3
-
setFlowRegistry(registry) - Set entire registry
- registerFlow(name, entry) - Register single flow
- unregisterFlow(name) - Unregister flow
- clearFlowRegistry() - Clear all flows
- getFlowRegistry() - Get current registry$3
-
flowExists(name) - Check if flow exists
- getAvailableFlows() - Get all flow names
- getFlowMetadata(name) - Get flow metadata
- getFlowStepKeys(name) - Get step keys
- isValidStepKey(flowName, stepKey) - Validate step
- getFirstStepKey(flowName) - Get first step
- getNextStepKey(flowName, currentKey) - Get next step
- getPreviousStepKey(flowName, currentKey) - Get previous step
- isFirstStepOfRootFlow() - Check if first step of root
- isLastStepOfLeafFlow()` - Check if last step of leafMIT