Componentes
[comunidad] · animate-ui / embla-carousel / motion

MotionCarousel

Carrusel con Embla y animaciones de Motion: dots animados tipo pill, escala del slide activo y navegación.

animate-uiembla-carouselmotion
@/components/animate-ui/components/community/motion-carousel
1'use client';;
2import * as React from 'react';
3import { motion } from 'motion/react';
4import useEmblaCarousel from 'embla-carousel-react';
5import { Button } from '@/components/animate-ui/components/buttons/button';
6import { ChevronRight, ChevronLeft } from 'lucide-react';
7 
8const transition = {
9 type: 'spring',
10 stiffness: 240,
11 damping: 24,
12 mass: 1,
13};
14 
15const useEmblaControls = emblaApi => {
16 const [selectedIndex, setSelectedIndex] = React.useState(0);
17 const [scrollSnaps, setScrollSnaps] = React.useState([]);
18 const [prevDisabled, setPrevDisabled] = React.useState(true);
19 const [nextDisabled, setNextDisabled] = React.useState(true);
20 
21 const onDotClick = React.useCallback((index) => emblaApi?.scrollTo(index), [emblaApi]);
22 
23 const onPrev = React.useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
24 const onNext = React.useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
25 
26 const updateSelectionState = (api) => {
27 setSelectedIndex(api.selectedScrollSnap());
28 setPrevDisabled(!api.canScrollPrev());
29 setNextDisabled(!api.canScrollNext());
30 };
31 
32 const onInit = React.useCallback((api) => {
33 setScrollSnaps(api.scrollSnapList());
34 updateSelectionState(api);
35 }, []);
36 
37 const onSelect = React.useCallback((api) => {
38 updateSelectionState(api);
39 }, []);
40 
41 React.useEffect(() => {
42 if (!emblaApi) return;
43 
44 onInit(emblaApi);
45 emblaApi.on('reInit', onInit).on('select', onSelect);
46 
47 return () => {
48 emblaApi.off('reInit', onInit).off('select', onSelect);
49 };
50 }, [emblaApi, onInit, onSelect]);
51 
52 return {
53 selectedIndex,
54 scrollSnaps,
55 prevDisabled,
56 nextDisabled,
57 onDotClick,
58 onPrev,
59 onNext,
60 };
61};
62 
63function MotionCarousel(props) {
64 const { slides, options } = props;
65 const [emblaRef, emblaApi] = useEmblaCarousel(options);
66 const {
67 selectedIndex,
68 scrollSnaps,
69 prevDisabled,
70 nextDisabled,
71 onDotClick,
72 onPrev,
73 onNext,
74 } = useEmblaControls(emblaApi);
75 
76 return (
77 <div
78 className="w-full space-y-4 [--slide-height:9rem] sm:[--slide-height:13rem] md:[--slide-height:18rem] [--slide-spacing:1.5rem] [--slide-size:55%]">
79 <div className="overflow-hidden" ref={emblaRef}>
80 <div className="flex touch-pan-y touch-pinch-zoom">
81 {slides.map((index) => {
82 const isActive = index === selectedIndex;
83 
84 return (
85 <motion.div
86 key={index}
87 className="h-[var(--slide-height)] mr-[var(--slide-spacing)] basis-[var(--slide-size)] flex-none flex min-w-0">
88 <motion.div
89 className="size-full flex items-center justify-center text-3xl md:text-5xl font-semibold select-none border-4 rounded-xl"
90 initial={false}
91 animate={{
92 scale: isActive ? 1 : 0.9,
93 }}
94 transition={transition}>
95 {index + 1}
96 </motion.div>
97 </motion.div>
98 );
99 })}
100 </div>
101 </div>
102 
103 <div className="flex justify-between">
104 <Button size="icon" onClick={onPrev} disabled={prevDisabled}>
105 <ChevronLeft className="size-5" />
106 </Button>
107 
108 <div className="flex flex-wrap justify-end items-center gap-2">
109 {scrollSnaps.map((_, index) => (
110 <DotButton
111 key={index}
112 label={`Slide ${index + 1}`}
113 selected={index === selectedIndex}
114 onClick={() => onDotClick(index)} />
115 ))}
116 </div>
117 
118 <Button size="icon" onClick={onNext} disabled={nextDisabled}>
119 <ChevronRight className="size-5" />
120 </Button>
121 </div>
122 </div>
123 );
124}
125 
126function DotButton({
127 selected = false,
128 label,
129 onClick
130}) {
131 return (
132 <motion.button
133 type="button"
134 onClick={onClick}
135 layout
136 initial={false}
137 className="flex cursor-pointer select-none items-center justify-center rounded-full border-none bg-primary text-primary-foreground text-sm"
138 animate={{
139 width: selected ? 68 : 12,
140 height: selected ? 28 : 12,
141 }}
142 transition={transition}>
143 <motion.span
144 layout
145 initial={false}
146 className="block whitespace-nowrap px-3 py-1"
147 animate={{
148 opacity: selected ? 1 : 0,
149 scale: selected ? 1 : 0,
150 filter: selected ? 'blur(0)' : 'blur(4px)',
151 }}
152 transition={transition}>
153 {label}
154 </motion.span>
155 </motion.button>
156 );
157}
158 
159export { MotionCarousel };
160 

Uso

Props

PropTipoDefaultDescripción
slidesReactNode[]—Contenido de cada slide.
optionsEmblaOptions—Opciones de Embla (loop, align…).

Acciones del componente

Search for a command to run...

Potenciá tu negocio con SFESS

Software a medida, IA integrada y ciberseguridad avanzada para empresas.

0años de ingeniería
0proyectos entregados
0% uptime
Servicios destacados
  • Consultoría SEO Avanzado & Analítica

    Auditoría técnica profunda para posicionar tu negocio en los primeros resultados de Google.

    SEO TécnicoGA4
  • Páginas Web Corporativas Complejas

    Plataformas empresariales masivas con múltiples secciones y animaciones fluidas.

    Framer MotionMulti-page
  • Integración de Inteligencia Artificial en Negocios

    Automatizamos la atención, las reservas y las ventas de tu negocio con agentes de IA.

    AI AgentsChatbots
Nuestro ecosistema
WORKENTA_OSSFESS_SOLUTIONSPRANALUZ_YOGAREMY_AI
Ver todos los servicios