Animations - nself-org/nchat GitHub Wiki
This document describes the comprehensive animation system implemented in nself-chat using Framer Motion.
The animation system provides:
- Consistent UX: Reusable animation variants for common patterns
- Premium Feel: Smooth, spring-based physics animations
- Accessibility: Respects user's motion preferences
- Performance: Optimized for 60fps rendering
- Type Safety: Full TypeScript support
Location: /src/lib/animations.ts
Message Entry - Slide in from bottom with fade
import { messageEntry } from '@/lib/animations'
<motion.div variants={messageEntry}>
{/* Message content */}
</motion.div>Message Hover - Background color transition
import { messageHover } from '@/lib/animations'
<motion.div variants={messageHover} initial="rest" whileHover="hover">
{/* Message */}
</motion.div>Reaction Burst - Emoji reaction with burst effect
import { reactionBurst } from '@/lib/animations'
<motion.button variants={reactionBurst}>
👍
</motion.button>Reaction Pill Hover - Subtle scale on hover
import { reactionPillHover } from '@/lib/animations'
<motion.div variants={reactionPillHover} whileHover="hover" whileTap="tap">
{/* Reaction pill */}
</motion.div>Modal Overlay - Fade in backdrop
import { modalOverlay, modalContent } from '@/lib/animations'
<motion.div variants={modalOverlay}>
<motion.div variants={modalContent}>
{/* Modal content */}
</motion.div>
</motion.div>Sheet Slide - Side drawer animation
import { sheetSlide } from '@/lib/animations'
<motion.div variants={sheetSlide('right')}>
{/* Drawer content */}
</motion.div>Page Transition - Fade and slide
import { PageTransition } from '@/components/ui/page-transition'
<PageTransition mode="slide">
{children}
</PageTransition>Channel Switch - Optimized for channel navigation
import { ChannelTransition } from '@/components/ui/page-transition'
<ChannelTransition channelId={channelId}>
{children}
</ChannelTransition>Sidebar Toggle - Expand/collapse animation
import { sidebarToggle } from '@/lib/animations'
<motion.div
variants={sidebarToggle}
animate={isExpanded ? 'expanded' : 'collapsed'}
>
{/* Sidebar */}
</motion.div>Skeleton Pulse - Loading placeholder
import { skeletonPulse } from '@/lib/animations'
<motion.div variants={skeletonPulse} />Shimmer Effect - Loading shimmer
import { shimmer } from '@/lib/animations'
<motion.div animate={shimmer.animate} />Staggered Items - Sequential reveal
import { staggerContainer, staggerItem } from '@/lib/animations'
<motion.div variants={staggerContainer}>
{items.map(item => (
<motion.div key={item.id} variants={staggerItem}>
{item.content}
</motion.div>
))}
</motion.div>Button Press - Tactile button feedback
import { Button } from '@/components/ui/button'
<Button animated>Click me</Button>Tooltip - Fade and slide
import { tooltip } from '@/lib/animations'
<motion.div variants={tooltip}>
{/* Tooltip content */}
</motion.div>Dropdown Menu - Cascade animation
import { dropdownMenu, dropdownItem } from '@/lib/animations'
<motion.div variants={dropdownMenu}>
{items.map(item => (
<motion.div key={item.id} variants={dropdownItem}>
{item.label}
</motion.div>
))}
</motion.div>Badge Bounce - Notification badge
import { badgeBounce } from '@/lib/animations'
<motion.div variants={badgeBounce}>
{count}
</motion.div>FAB Float - Floating action button
import { fabFloat } from '@/lib/animations'
<motion.button variants={fabFloat} whileHover="hover">
<Plus />
</motion.button>Toast Slide - Slide from top
import { toastSlide } from '@/lib/animations'
<motion.div variants={toastSlide}>
{/* Toast content */}
</motion.div>Notification Pulse - Attention-grabbing pulse
import { notificationPulse } from '@/lib/animations'
<motion.div variants={notificationPulse}>
{/* Notification badge */}
</motion.div>Input Focus - Border highlight
import { Input } from '@/components/ui/input'
<Input error={!!error} success={isValid} />Error Shake - Validation feedback
import { errorShake } from '@/lib/animations'
<motion.div variants={errorShake} animate={hasError ? 'animate' : 'initial'}>
{/* Input field */}
</motion.div>Success Checkmark - Confirmation animation
import { successCheckmark } from '@/lib/animations'
<motion.div variants={successCheckmark}>
<CheckCircle />
</motion.div>Scroll Reveal - Fade in on scroll
import { ScrollReveal } from '@/components/ui/scroll-reveal'
<ScrollReveal>
{/* Content */}
</ScrollReveal>Staggered Scroll Reveal - Sequential reveal
import { StaggeredScrollReveal } from '@/components/ui/scroll-reveal'
<StaggeredScrollReveal>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</StaggeredScrollReveal>Fade In On Scroll - Directional fade
import { FadeInOnScroll } from '@/components/ui/scroll-reveal'
<FadeInOnScroll direction="up">
{/* Content */}
</FadeInOnScroll>Pull to Refresh - Mobile refresh gesture
import { PullToRefresh } from '@/components/ui/pull-to-refresh'
<PullToRefresh onRefresh={async () => {
await refetchData()
}}>
{/* Scrollable content */}
</PullToRefresh>Swipe to Dismiss - Swipeable items
import { swipeToDismiss } from '@/lib/animations'
<motion.div variants={swipeToDismiss('right')}>
{/* Swipeable item */}
</motion.div>Drag Reorder - Drag and drop
import { dragReorder } from '@/lib/animations'
<motion.div
drag
variants={dragReorder}
whileDrag="dragging"
>
{/* Draggable item */}
</motion.div>import { spring, springSmooth, springBouncy } from '@/lib/animations'
// Standard spring (400 stiffness, 30 damping)
<motion.div transition={spring} />
// Smooth spring (300 stiffness, 25 damping)
<motion.div transition={springSmooth} />
// Bouncy spring (500 stiffness, 20 damping)
<motion.div transition={springBouncy} />import { easeOut, easeInOut, easeFast, easeSlow } from '@/lib/animations'
// Fast ease out (0.2s)
<motion.div transition={easeOut} />
// Ease in-out (0.3s)
<motion.div transition={easeInOut} />
// Very fast (0.15s)
<motion.div transition={easeFast} />
// Slow (0.5s)
<motion.div transition={easeSlow} />Create custom animation variants:
import { fade, slide, scale, combine } from '@/lib/animations'
// Fade animation
const fadeVariant = fade(0.3) // 0.3s duration
// Slide animation
const slideVariant = slide('up', 20) // slide up 20px
// Scale animation
const scaleVariant = scale(0.8, 1) // from 0.8 to 1
// Combine multiple variants
const combinedVariant = combine(fadeVariant, scaleVariant)Pre-built skeleton components with animations:
import {
MessageSkeleton,
MessageListSkeleton,
ChannelSidebarSkeleton,
MemberListSkeleton,
ChatHeaderSkeleton,
ChatLayoutSkeleton,
} from '@/components/ui/loading-skeletons'
// Single message skeleton
<MessageSkeleton grouped={false} />
// Full message list
<MessageListSkeleton count={10} />
// Complete chat layout
<ChatLayoutSkeleton />Buttons include automatic press animations:
import { Button } from '@/components/ui/button'
// Animated by default
<Button>Click me</Button>
// Disable animation
<Button animated={false}>No animation</Button>Inputs include validation feedback animations:
import { Input } from '@/components/ui/input'
// Error state with shake animation
<Input error="Invalid email" />
// Success state with checkmark
<Input success />Dialogs include overlay fade and content scale:
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Modal Title</DialogTitle>
</DialogHeader>
{/* Content animates in automatically */}
</DialogContent>
</Dialog>Detect when elements enter viewport:
import { useScrollAnimation } from '@/hooks/use-scroll-animation'
function Component() {
const { ref, isInView } = useScrollAnimation({
threshold: 0.2,
once: true,
})
return (
<div ref={ref}>
{isInView ? 'Visible!' : 'Hidden'}
</div>
)
}Create parallax scrolling effects:
import { useParallax } from '@/hooks/use-scroll-animation'
function Component() {
const { ref, y } = useParallax(50)
return (
<motion.div ref={ref} style={{ y }}>
{/* Parallax content */}
</motion.div>
)
}Track scroll direction:
import { useScrollDirection } from '@/hooks/use-scroll-animation'
function Component() {
const direction = useScrollDirection()
return (
<div>
Scrolling {direction}
</div>
)
}Auto-hide elements on scroll:
import { useAutoHideOnScroll } from '@/hooks/use-scroll-animation'
function Header() {
const isVisible = useAutoHideOnScroll(100)
return (
<motion.header
animate={{ y: isVisible ? 0 : -100 }}
>
{/* Header content */}
</motion.header>
)
}The theme context includes smooth transitions:
import { useTheme } from '@/contexts/theme-context'
function ThemeToggle() {
const { theme, setTheme, isTransitioning } = useTheme()
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{isTransitioning ? 'Transitioning...' : 'Toggle Theme'}
</button>
)
}-
Use
layoutprop sparingly - Only for necessary layout animations -
Prefer transforms over layout changes -
x,y,scale,rotate -
Use
AnimatePresencefor exit animations - Ensures smooth removal -
Leverage
will-changeCSS - Applied automatically by Framer Motion -
Reduce motion for accessibility - Use
useReducedMotionhook
All animations respect the user's motion preferences:
import { useReducedMotion } from '@/lib/accessibility/use-reduced-motion'
function Component() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={shouldReduceMotion ? {} : { scale: 1.2 }}
>
{/* Content */}
</motion.div>
)
}import { motion, AnimatePresence } from 'framer-motion'
import { messageEntry, staggerContainer, staggerItem } from '@/lib/animations'
function MessageList({ messages }) {
return (
<motion.div variants={staggerContainer} initial="initial" animate="animate">
<AnimatePresence mode="popLayout">
{messages.map((message) => (
<motion.div
key={message.id}
variants={messageEntry}
layout
>
<MessageItem message={message} />
</motion.div>
))}
</AnimatePresence>
</motion.div>
)
}import { motion } from 'framer-motion'
import { sidebarToggle, staggerContainer, staggerItem } from '@/lib/animations'
function Sidebar({ isOpen }) {
return (
<motion.aside
variants={sidebarToggle}
animate={isOpen ? 'expanded' : 'collapsed'}
>
<motion.div variants={staggerContainer} initial="initial" animate="animate">
{channels.map((channel) => (
<motion.div key={channel.id} variants={staggerItem}>
{channel.name}
</motion.div>
))}
</motion.div>
</motion.aside>
)
}import { motion } from 'framer-motion'
import { Input } from '@/components/ui/input'
import { errorShake, successCheckmark } from '@/lib/animations'
function FormField({ value, error, isValid }) {
return (
<div>
<Input
value={value}
error={error}
success={isValid}
/>
{error && (
<motion.p
variants={errorShake}
animate="animate"
className="text-destructive text-sm"
>
{error}
</motion.p>
)}
</div>
)
}When adding new animations:
- Add variant to
/src/lib/animations.ts - Use semantic naming (describe what it does, not how)
- Include TypeScript types
- Add documentation to this file
- Ensure accessibility compliance
- Test on multiple devices and browsers