Animate basket price and item changes

Reflow remaining basket lines with framer-motion when an item is
removed, slide the per-line price when it changes, and tween the
basket total up/down to its new amount instead of snapping to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 11:05:24 +02:00
co-authored by Claude Sonnet 5
parent 3d736769e0
commit 7f3728b5bf
3 changed files with 115 additions and 50 deletions
+14 -4
View File
@@ -5,6 +5,8 @@ import styles from './AnimatedNumber.module.scss'
type AnimatedNumberProps = {
value: number
format?: (value: number) => string
align?: 'center' | 'start'
className?: string
}
@@ -23,7 +25,12 @@ const variants = {
// in the render body — mutating a ref here would already reflect the new
// value by the time `exit`'s "custom" is resolved for the outgoing digit,
// so every transition — increment or decrement — read as "forward".
export const AnimatedNumber = ({ value, className }: AnimatedNumberProps) => {
export const AnimatedNumber = ({
value,
format = String,
align = 'center',
className,
}: AnimatedNumberProps) => {
const reduceMotion = useReducedMotion()
const [previousValue, setPreviousValue] = useState(value)
const [direction, setDirection] = useState(1)
@@ -34,11 +41,14 @@ export const AnimatedNumber = ({ value, className }: AnimatedNumberProps) => {
}
if (reduceMotion) {
return <span className={className}>{value}</span>
return <span className={className}>{format(value)}</span>
}
return (
<span className={`${styles.wrapper} ${className ?? ''}`}>
<span
className={`${styles.wrapper} ${className ?? ''}`}
style={{ justifyItems: align }}
>
<AnimatePresence mode="popLayout" initial={false} custom={direction}>
<motion.span
key={value}
@@ -50,7 +60,7 @@ export const AnimatedNumber = ({ value, className }: AnimatedNumberProps) => {
transition={{ duration: 0.15, ease: 'easeOut' }}
className={styles.digit}
>
{value}
{format(value)}
</motion.span>
</AnimatePresence>
</span>