Загрузка...
Анимированный степпер для React.UI-компонент с плавными переходами, индикаторами прогресса и настраиваемым контентом шагов. Минималистичный дизайн.
# Animated Stepper
You are given a task to integrate an existing React component in the codebase
~~~/README.md
# AnimatedStepper
A production-grade, minimalist stepper component with fluid Framer Motion transitions and dynamic height adjustment.
## Features
- **Fluid Motion**: Uses custom Power3 easing for professional entrance and slide transitions.
- **Dynamic Height**: The container smoothly adjusts its height to fit the content of the current step.
- **Progress Tracking**: Interactive or disabled step indicators with completion states.
- **Customizable**: Built with Tailwind CSS variables for easy theming.
## Dependencies
- `framer-motion`: ^11.0.0
- `lucide-react`: ^0.344.0
## Props
### AnimatedStepper
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Multiple `<Step>` components |
| `initialStep` | `number` | `1` | The starting step index |
| `onStepChange` | `(step: number) => void` | - | Callback when step changes |
| `onFinalStepCompleted` | `() => void` | - | Callback after clicking 'Complete' on last step |
| `backButtonText` | `string` | `"Back"` | Label for the back button |
| `nextButtonText` | `string` | `"Continue"` | Label for the next button |
| `disableStepIndicators` | `boolean` | `false` | If true, user cannot click indicators to jump steps |
### Step
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Optional title displayed at the top of the step |
| `children` | `ReactNode` | Content of the step |
## Usage
```tsx
import { AnimatedStepper, Step } from '@/sd-components/59f1e3dc-3550-45c2-aa02-048138d93ebe';
export default function MyWizard() {
return (
<AnimatedStepper onFinalStepCompleted={() => console.log('Done!')}>
<Step title="Account Setup">
<p>Your account information goes here...</p>
</Step>
<Step title="Preferences">
<p>Choose your notification settings...</p>
</Step>
</AnimatedStepper>
);
}
```
~~~
~~~/src/App.tsx
import React, { useState } from 'react';
import { AnimatedStepper, Step } from './Component';
export default function App() {
const [name, setName] = useState('');
return (
<div className="min-h-screen w-full bg-background flex items-center justify-center font-sans">
<AnimatedStepper
onFinalStepCompleted={() => alert('Wizard Completed!')}
>
<Step title="Step 1: Introduction">
<p className="mb-4">Welcome to the minimalist stepper showcase. This component is built for production-grade interfaces with a focus on fluid motion.</p>
<div className="h-32 w-full rounded-2xl bg-secondary/50 flex items-center justify-center">
<span className="text-muted-foreground text-sm font-medium">Custom visual content here</span>
</div>
</Step>
<Step title="Step 2: Interactive Input">
<p className="mb-4">Forms and inputs integrate seamlessly with the dynamic height adjustment of the stepper container.</p>
<div className="space-y-4">
<label className="text-sm font-medium text-foreground block">Your Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name..."
className="w-full h-12 px-4 rounded-xl border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all"
/>
</div>
</Step>
<Step title="Step 3: Final Review">
<p className="mb-2 text-lg">Almost there, {name || 'Explorer'}!</p>
<p className="text-muted-foreground">Confirm your choices and complete the setup. The animations use high-end Power3 easing for that premium feel.</p>
<div className="mt-6 p-4 rounded-2xl bg-primary/5 border border-primary/10">
<div className="flex justify-between items-center text-sm">
<span className="font-medium">User Status</span>
<span className="text-primary font-bold">Ready</span>
</div>
</div>
</Step>
</AnimatedStepper>
</div>
);
}
~~~
~~~/package.json
{
"name": "animated-stepper",
"description": "A minimalist, production-grade animated stepper component",
"dependencies": {
"framer-motion": "^11.0.0",
"lucide-react": "^0.344.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.1"
}
}
~~~
~~~/src/Component.tsx
/**
* AnimatedStepper Component
* A reusable multi-step indicator and content container with smooth Framer Motion transitions.
*
* Features:
* - Slide transitions between steps
* - Dynamic height adjustment
* - Progress indicators with completion states
* - Fully customizable step content via <Step> sub-component
* - Responsive design following minimalist principles
*/
import React, { useState, Children, useRef, useLayoutEffect, HTMLAttributes, ReactNode } from 'react';
import { motion, AnimatePresence, Variants } from 'framer-motion';
import { Check } from 'lucide-react';
interface StepperProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
initialStep?: number;
onStepChange?: (step: number) => void;
onFinalStepCompleted?: () => void;
stepCircleContainerClassName?: string;
stepContainerClassName?: string;
contentClassName?: string;
footerClassName?: string;
backButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
nextButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
backButtonText?: string;
nextButtonText?: string;
disableStepIndicators?: boolean;
renderStepIndicator?: (props: {
step: number;
currentStep: number;
onStepClick: (clicked: number) => void;
}) => ReactNode;
}
export function AnimatedStepper({
children,
initialStep = 1,
onStepChange = () => {},
onFinalStepCompleted = () => {},
stepCircleContainerClassName = '',
stepContainerClassName = '',
contentClassName = '',
footerClassName = '',
backButtonProps = {},
nextButtonProps = {},
backButtonText = 'Back',
nextButtonText = 'Continue',
disableStepIndicators = false,
renderStepIndicator,
...rest
}: StepperProps) {
const [currentStep, setCurrentStep] = useState<number>(initialStep);
const [direction, setDirection] = useState<number>(0);
const stepsArray = Children.toArray(children);
const totalSteps = stepsArray.length;
const isCompleted = currentStep > totalSteps;
const isLastStep = currentStep === totalSteps;
const updateStep = (newStep: number) => {
setCurrentStep(newStep);
if (newStep > totalSteps) {
onFinalStepCompleted();
} else {
onStepChange(newStep);
}
};
const handleBack = () => {
if (currentStep > 1) {
setDirection(-1);
updateStep(currentStep - 1);
}
};
const handleNext = () => {
if (!isLastStep) {
setDirection(1);
updateStep(currentStep + 1);
}
};
const handleComplete = () => {
setDirection(1);
updateStep(totalSteps + 1);
};
return (
<div
className={`flex min-h-[400px] w-full flex-col items-center justify-center p-4 sm:p-8 ${rest.className || ''}`}
{...rest}
>
<div
className={`mx-auto w-full max-w-lg overflow-hidden rounded-[2.5rem] bg-card border border-border shadow-[0_40px_100px_-20px_rgba(0,0,0,0.05)] ${stepCircleContainerClassName}`}
>
{/* Indicators */}
<div className={`flex w-full items-center p-8 pb-4 ${stepContainerClassName}`}>
{stepsArray.map((_, index) => {
const stepNumber = index + 1;
const isNotLastStep = index < totalSteps - 1;
return (
<React.Fragment key={stepNumber}>
{renderStepIndicator ? (
renderStepIndicator({
step: stepNumber,
currentStep,
onStepClick: clicked => {
setDirection(clicked > currentStep ? 1 : -1);
updateStep(clicked);
}
})
) : (
<StepIndicator
step={stepNumber}
disableStepIndicators={disableStepIndicators}
currentStep={currentStep}
onClickStep={clicked => {
setDirection(clicked > currentStep ? 1 : -1);
updateStep(clicked);
}}
/>
)}
{isNotLastStep && <StepConnector isComplete={currentStep > stepNumber} />}
</React.Fragment>
);
})}
</div>
{/* Content Area */}
<StepContentWrapper
isCompleted={isCompleted}
currentStep={currentStep}
direction={direction}
className={`space-y-4 px-8 ${contentClassName}`}
>
{stepsArray[currentStep - 1]}
</StepContentWrapper>
{/* Footer Actions */}
{!isCompleted && (
<div className={`px-8 pb-8 pt-4 ${footerClassName}`}>
<div className={`flex items-center ${currentStep !== 1 ? 'justify-between' : 'justify-end'}`}>
{currentStep !== 1 && (
<button
onClick={handleBack}
className={`text-sm font-medium transition-colors duration-300 hover:text-foreground/80 text-muted-foreground ${
currentStep === 1 ? 'pointer-events-none opacity-0' : 'opacity-100'
}`}
{...backButtonProps}
>
{backButtonText}
</button>
)}
<button
onClick={isLastStep ? handleComplete : handleNext}
className="inline-flex h-11 items-center justify-center rounded-full bg-primary px-8 text-sm font-semibold tracking-tight text-primary-foreground transition-all duration-300 hover:opacity-90 active:scale-95"
{...nextButtonProps}
>
{isLastStep ? 'Complete' : nextButtonText}
</button>
</div>
</div>
)}
</div>
</div>
);
}
/**
* Step Content Wrapper with dynamic height and slide animation
*/
function StepContentWrapper({
isCompleted,
currentStep,
direction,
children,
className = ''
}: {
isCompleted: boolean;
currentStep: number;
direction: number;
children: ReactNode;
className?: string;
}) {
const [parentHeight, setParentHeight] = useState<number>(0);
return (
<motion.div
style={{ position: 'relative', overflow: 'hidden' }}
animate={{ height: isCompleted ? 0 : parentHeight || 'auto' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className={className}
>
<AnimatePresence initial={false} mode="wait" custom={direction}>
{!isCompleted && (
<SlideTransition key={currentStep} direction={direction} onHeightReady={h => setParentHeight(h)}>
{children}
</SlideTransition>
)}
</AnimatePresence>
</motion.div>
);
}
function SlideTransition({ children, direction, onHeightReady }: {
children: ReactNode;
direction: number;
onHeightReady: (height: number) => void;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
if (containerRef.current) {
onHeightReady(containerRef.current.offsetHeight);
}
}, [children, onHeightReady]);
return (
<motion.div
ref={containerRef}
custom={direction}
variants={stepVariants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: 'spring', stiffness: 300, damping: 30 },
opacity: { duration: 0.2 }
}}
className="w-full"
>
{children}
</motion.div>
);
}
const stepVariants: Variants = {
enter: (dir: number) => ({
x: dir >= 0 ? 20 : -20,
opacity: 0
}),
center: {
x: 0,
opacity: 1
},
exit: (dir: number) => ({
x: dir >= 0 ? -20 : 20,
opacity: 0
})
};
/**
* Step Sub-component for individual step content
*/
export function Step({ children, title }: { children: ReactNode; title?: string }) {
return (
<div className="py-4">
{title && <h2 className="mb-4 text-2xl font-bold tracking-tight text-foreground">{title}</h2>}
<div className="text-muted-foreground leading-relaxed">{children}</div>
</div>
);
}
/**
* Step Indicator Circle
*/
function StepIndicator({
step,
currentStep,
onClickStep,
disableStepIndicators = false
}: {
step: number;
currentStep: number;
onClickStep: (clicked: number) => void;
disableStepIndicators?: boolean;
}) {
const status = currentStep === step ? 'active' : currentStep < step ? 'inactive' : 'complete';
return (
<motion.div
onClick={() => !disableStepIndicators && onClickStep(step)}
className={`relative flex items-center justify-center ${!disableStepIndicators ? 'cursor-pointer' : ''}`}
animate={status}
>
<motion.div
variants={{
inactive: {
scale: 1,
backgroundColor: 'hsl(var(--secondary))',
color: 'hsl(var(--muted-foreground))',
borderColor: 'hsl(var(--border))'
},
active: {
scale: 1,
backgroundColor: 'hsl(var(--background))',
color: 'hsl(var(--primary))',
borderColor: 'hsl(var(--primary))'
},
complete: {
scale: 1,
backgroundColor: 'hsl(var(--primary))',
color: 'hsl(var(--primary-foreground))',
borderColor: 'hsl(var(--primary))'
}
}}
className="flex h-10 w-10 items-center justify-center rounded-full border-2 font-semibold transition-colors duration-300"
>
{status === 'complete' ? (
<Check className="h-5 w-5" />
) : (
<span className="text-sm">{step}</span>
)}
</motion.div>
{status === 'active' && (
<motion.div
layoutId="active-glow"
className="absolute -inset-1 rounded-full bg-primary/20 blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</motion.div>
);
}
/**
* Connector line between indicators
*/
function StepConnector({ isComplete }: { isComplete: boolean }) {
return (
<div className="relative mx-4 h-[2px] flex-1 overflow-hidden rounded-full bg-border">
<motion.div
className="absolute inset-0 bg-primary origin-left"
initial={{ scaleX: 0 }}
animate={{ scaleX: isComplete ? 1 : 0 }}
transition={{ duration: 0.5, ease: [0.33, 1, 0.68, 1] }}
/>
</div>
);
}
export default AnimatedStepper;
~~~
Implementation Guidelines
1. Analyze the component structure, styling, animation implementations
2. Review the component's arguments and state
3. Think through what is the best place to adopt this component/style into the design we are doing
4. Then adopt the component/design to our current system
Help me integrate this into my design