cult-ui
Layout
Animation

expandable

Expandable Card primitive to easily condense and expand details.

animated
card
effect
flex
grid
hover
motion
text
transition

Source Code

Files
product-showcase-card
1"use client"
2 
3import React, { useState } from "react"
4import {
5  Battery,
6  Bluetooth,
7  Calendar,
8  Clock,
9  Cloud,
10  Droplets,
11  Fingerprint,
12  MapPin,
13  MessageSquare,
14  Mic,
15  ShoppingCart,
16  Star,
17  Sun,
18  Users,
19  Video,
20  Wind,
21} from "lucide-react"
22import { AnimatePresence, motion } from "motion/react"
23import { toast } from "sonner"
24 
25import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
26import { Badge } from "@/components/ui/badge"
27import { Button } from "@/components/ui/button"
28import {
29  Tooltip,
30  TooltipContent,
31  TooltipProvider,
32  TooltipTrigger,
33} from "@/components/ui/tooltip"
34import {
35  Expandable,
36  ExpandableCard,
37  ExpandableCardContent,
38  ExpandableCardFooter,
39  ExpandableCardHeader,
40  ExpandableContent,
41  ExpandableTrigger,
42} from "@/components/ui/expandable"
43 
44// _____________________EXAMPLES______________________
45function DesignSyncExample() {
46  return (
47    <Expandable
48      expandDirection="both"
49      expandBehavior="replace"
50      initialDelay={0.2}
51      onExpandStart={() => console.log("Expanding meeting card...")}
52      onExpandEnd={() => console.log("Meeting card expanded!")}
53    >
54      {({ isExpanded }) => (
55        <ExpandableTrigger>
56          <ExpandableCard
57            className="w-full relative"
58            collapsedSize={{ width: 320, height: 240 }}
59            expandedSize={{ width: 420, height: 480 }}
60            hoverToExpand={false}
61            expandDelay={200}
62            collapseDelay={500}
63          >
64            <ExpandableCardHeader>
65              <div className="flex justify-between items-start w-full">
66                <div>
67                  <Badge
68                    variant="secondary"
69                    className="bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-100 mb-2"
70                  >
71                    In 15 mins
72                  </Badge>
73                  <h3 className="font-semibold text-xl text-gray-800 dark:text-white">
74                    Design Sync
75                  </h3>
76                </div>
77                <TooltipProvider>
78                  <Tooltip>
79                    <TooltipTrigger asChild>
80                      <Button size="icon" variant="outline" className="h-8 w-8">
81                        <Calendar className="h-4 w-4" />
82                      </Button>
83                    </TooltipTrigger>
84                    <TooltipContent>
85                      <p>Add to Calendar</p>
86                    </TooltipContent>
87                  </Tooltip>
88                </TooltipProvider>
89              </div>
90            </ExpandableCardHeader>
91 
92            <ExpandableCardContent>
93              <div className="flex flex-col items-start justify-between mb-4">
94                <div className="flex items-center text-sm text-gray-600 dark:text-gray-300">
95                  <Clock className="h-4 w-4 mr-1" />
96                  <span>1:30PM → 2:30PM</span>
97                </div>
98 
99                <ExpandableContent preset="blur-md">
100                  <div className="flex items-center text-sm text-gray-600 dark:text-gray-300">
101                    <MapPin className="h-4 w-4 mr-1" />
102                    <span>Conference Room A</span>
103                  </div>
104                </ExpandableContent>
105              </div>
106              <ExpandableContent preset="blur-md" stagger staggerChildren={0.2}>
107                <p className="text-sm text-gray-700 dark:text-gray-200 mb-4">
108                  Weekly design sync to discuss ongoing projects, share updates,
109                  and address any design-related challenges.
110                </p>
111                <div className="mb-4">
112                  <h4 className="font-medium text-sm text-gray-800 dark:text-gray-100 mb-2 flex items-center">
113                    <Users className="h-4 w-4 mr-2" />
114                    Attendees:
115                  </h4>
116                  <div className="flex -space-x-2 overflow-hidden">
117                    {["Alice", "Bob", "Charlie", "David"].map((name, index) => (
118                      <TooltipProvider key={index}>
119                        <Tooltip>
120                          <TooltipTrigger asChild>
121                            <Avatar className="border-2 border-white dark:border-gray-800">
122                              <AvatarImage
123                                src={`/placeholder.svg?height=32&width=32&text=${name[0]}`}
124                                alt={name}
125                              />
126                              <AvatarFallback>{name[0]}</AvatarFallback>
127                            </Avatar>
128                          </TooltipTrigger>
129                          <TooltipContent>
130                            <p>{name}</p>
131                          </TooltipContent>
132                        </Tooltip>
133                      </TooltipProvider>
134                    ))}
135                  </div>
136                </div>
137                <div className="space-y-2">
138                  <Button className="w-full bg-red-600 hover:bg-red-700 text-white">
139                    <Video className="h-4 w-4 mr-2" />
140                    Join Meeting
141                  </Button>
142                  {isExpanded && (
143                    <Button variant="outline" className="w-full">
144                      <MessageSquare className="h-4 w-4 mr-2" />
145                      Open Chat
146                    </Button>
147                  )}
148                </div>
149              </ExpandableContent>
150            </ExpandableCardContent>
151            <ExpandableContent preset="slide-up">
152              <ExpandableCardFooter>
153                <div className="flex items-center justify-between w-full text-sm text-gray-600 dark:text-gray-300">
154                  <span>Weekly</span>
155                  <span>Next: Mon, 10:00 AM</span>
156                </div>
157              </ExpandableCardFooter>
158            </ExpandableContent>
159          </ExpandableCard>
160        </ExpandableTrigger>
161      )}
162    </Expandable>
163  )
164}
165 
166export function ProductShowcaseCard() {
167  return (
168    <Expandable
169      expandDirection="both"
170      expandBehavior="replace"
171      onExpandStart={() => console.log("Expanding product card...")}
172      onExpandEnd={() => console.log("Product card expanded!")}
173    >
174      {({ isExpanded }) => (
175        <ExpandableTrigger>
176          <ExpandableCard
177            className="w-full relative"
178            collapsedSize={{ width: 330, height: 220 }}
179            expandedSize={{ width: 500, height: 520 }}
180            hoverToExpand={false}
181            expandDelay={500}
182            collapseDelay={700}
183          >
184            <ExpandableCardHeader>
185              <div className="flex justify-between items-center">
186                <Badge
187                  variant="secondary"
188                  className="bg-blue-100 text-blue-800"
189                >
190                  New Arrival
191                </Badge>
192                <Badge variant="outline" className="ml-2">
193                  $129.99
194                </Badge>
195              </div>
196            </ExpandableCardHeader>
197 
198            <ExpandableCardContent>
199              <div className="flex items-start mb-4">
200                <img
201                  src="https://pisces.bbystatic.com/image2/BestBuy_US/images/products/6505/6505727_rd.jpg;maxHeight=640;maxWidth=550;format=webp"
202                  alt="Product"
203                  className="object-cover rounded-md mr-4"
204                  style={{
205                    width: isExpanded ? "120px" : "80px",
206                    height: isExpanded ? "120px" : "80px",
207                    transition: "width 0.3s, height 0.3s",
208                  }}
209                />
210                <div className="flex-1">
211                  <h3
212                    className="font-medium text-gray-800 dark:text-white tracking-tight transition-all duration-300"
213                    style={{
214                      fontSize: isExpanded ? "24px" : "18px",
215                      fontWeight: isExpanded ? "700" : "400",
216                    }}
217                  >
218                    Sony Headphones
219                  </h3>
220                  <div className="flex items-center mt-1">
221                    {[1, 2, 3, 4, 5].map((star) => (
222                      <Star
223                        key={star}
224                        className="w-4 h-4 text-yellow-400 fill-current"
225                      />
226                    ))}
227                    <AnimatePresence mode="wait">
228                      {isExpanded ? (
229                        <motion.span
230                          key="expanded"
231                          initial={{ opacity: 0, width: 0 }}
232                          animate={{ opacity: 1, width: "auto" }}
233                          exit={{ opacity: 0, width: 0 }}
234                          transition={{ duration: 0.2 }}
235                          className="ml-2 text-sm text-gray-600 dark:text-gray-400 overflow-hidden whitespace-nowrap"
236                        >
237                          (128 reviews)
238                        </motion.span>
239                      ) : (
240                        <motion.span
241                          key="collapsed"
242                          initial={{ opacity: 0, width: 0 }}
243                          animate={{ opacity: 1, width: "auto" }}
244                          exit={{ opacity: 0, width: 0 }}
245                          transition={{ duration: 0.2 }}
246                          className="ml-2 text-sm text-gray-600 dark:text-gray-400 overflow-hidden whitespace-nowrap"
247                        >
248                          (128)
249                        </motion.span>
250                      )}
251                    </AnimatePresence>
252                  </div>
253                </div>
254              </div>
255              <ExpandableContent
256                preset="fade"
257                keepMounted={false}
258                animateIn={{
259                  initial: { opacity: 0, y: 20 },
260                  animate: { opacity: 1, y: 0 },
261                  transition: { type: "spring", stiffness: 300, damping: 20 },
262                }}
263              >
264                <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-xs">
265                  Experience crystal-clear audio with our latest
266                  noise-cancelling technology. Perfect for work, travel, or
267                  relaxation.
268                </p>
269 
270                <div className="space-y-4">
271                  {[
272                    { icon: Battery, text: "30-hour battery life" },
273                    { icon: Bluetooth, text: "Bluetooth 5.0" },
274                    { icon: Fingerprint, text: "Touch controls" },
275                    { icon: Mic, text: "Voice assistant compatible" },
276                  ].map((feature, index) => (
277                    <div
278                      key={index}
279                      className="flex items-center text-sm text-gray-600 dark:text-gray-400"
280                    >
281                      <feature.icon className="w-4 h-4 mr-2" />
282                      <span>{feature.text}</span>
283                    </div>
284                  ))}
285 
286                  <Button className="w-full bg-blue-600 hover:bg-blue-700 text-white">
287                    <ShoppingCart className="w-4 h-4 mr-2" />
288                    Add to Cart
289                  </Button>
290                </div>
291              </ExpandableContent>
292            </ExpandableCardContent>
293            <ExpandableContent preset="slide-up">
294              <ExpandableCardFooter>
295                <div className="flex justify-between text-sm text-gray-600 dark:text-gray-400 w-full">
296                  <span>Free shipping</span>
297                  <span>30-day return policy</span>
298                </div>
299              </ExpandableCardFooter>
300            </ExpandableContent>
301          </ExpandableCard>
302        </ExpandableTrigger>
303      )}
304    </Expandable>
305  )
306}
307 
308export function WeatherForecastCard() {
309  return (
310    <Expandable expandDirection="both" expandBehavior="replace">
311      <ExpandableTrigger>
312        <ExpandableCard
313          collapsedSize={{ width: 300, height: 220 }}
314          expandedSize={{ width: 500, height: 420 }}
315          hoverToExpand={false}
316          expandDelay={100}
317          collapseDelay={400}
318        >
319          <ExpandableCardHeader>
320            <div className="flex items-center justify-between">
321              <div className="flex items-center">
322                <Sun className="w-8 h-8 text-yellow-400 mr-2" />
323                <ExpandableContent preset="blur-sm" keepMounted={true}>
324                  <h3 className="font-medium text-lg">Today's Weather</h3>
325                  <Badge
326                    variant="secondary"
327                    className="bg-blue-100 text-blue-800"
328                  >
329                    72°F
330                  </Badge>
331                </ExpandableContent>
332              </div>
333            </div>
334          </ExpandableCardHeader>
335 
336          <ExpandableCardContent>
337            <div className="flex justify-between items-center mb-4">
338              <div>
339                <p className="text-2xl font-bold">72°F</p>
340                <p className="text-sm text-gray-600 dark:text-gray-400">
341                  Feels like 75°F
342                </p>
343              </div>
344              <div className="text-right">
345                <p className="font-medium">Sunny</p>
346                <ExpandableContent
347                  preset="blur-sm"
348                  stagger
349                  staggerChildren={0.1}
350                  keepMounted={true}
351                  animateIn={{
352                    initial: { opacity: 0, y: 20, rotate: -5 },
353                    animate: { opacity: 1, y: 0, rotate: 0 },
354                    transition: { type: "spring", stiffness: 300, damping: 20 },
355                  }}
356                >
357                  <p className="text-sm text-gray-600 dark:text-gray-400">
358                    High 78° / Low 65°
359                  </p>
360                </ExpandableContent>
361              </div>
362            </div>
363            <ExpandableContent
364              preset="blur-sm"
365              stagger
366              staggerChildren={0.1}
367              keepMounted={true}
368              animateIn={{
369                initial: { opacity: 0, y: 20, rotate: -5 },
370                animate: { opacity: 1, y: 0, rotate: 0 },
371                transition: { type: "spring", stiffness: 300, damping: 20 },
372              }}
373            >
374              <div className="space-y-2 mb-4">
375                <div className="flex justify-between items-center">
376                  <div className="flex items-center">
377                    <Cloud className="w-5 h-5 mr-2 text-gray-400" />
378                    <span>Humidity</span>
379                  </div>
380                  <span>45%</span>
381                </div>
382                <div className="flex justify-between items-center">
383                  <div className="flex items-center">
384                    <Wind className="w-5 h-5 mr-2 text-gray-400" />
385                    <span>Wind</span>
386                  </div>
387                  <span>8 mph</span>
388                </div>
389                <div className="flex justify-between items-center">
390                  <div className="flex items-center">
391                    <Droplets className="w-5 h-5 mr-2 text-gray-400" />
392                    <span>Precipitation</span>
393                  </div>
394                  <span>0%</span>
395                </div>
396              </div>
397              <div className="space-y-2">
398                <h4 className="font-medium">5-Day Forecast</h4>
399                {["Mon", "Tue", "Wed", "Thu", "Fri"].map((day, index) => (
400                  <div key={day} className="flex justify-between items-center">
401                    <span>{day}</span>
402                    <div className="flex items-center">
403                      <Sun className="w-4 h-4 text-yellow-400 mr-2" />
404                      <span>{70 + index}°F</span>
405                    </div>
406                  </div>
407                ))}
408              </div>
409            </ExpandableContent>
410          </ExpandableCardContent>
411          <ExpandableCardFooter>
412            <p className="text-xs text-gray-500 dark:text-gray-400">
413              Last updated: 5 minutes ago
414            </p>
415          </ExpandableCardFooter>
416        </ExpandableCard>
417      </ExpandableTrigger>
418    </Expandable>
419  )
420}
421 
422function ControlledExpandableCard() {
423  const [isExpanded, setIsExpanded] = useState(false)
424 
425  const handleToggle = () => {
426    setIsExpanded((prev) => !prev)
427  }
428 
429  return (
430    <div className="space-y-4">
431      <Button onClick={handleToggle} className="mb-4">
432        {isExpanded ? "Collapse" : "Expand"}
433      </Button>
434 
435      <Expandable
436        expanded={isExpanded}
437        onToggle={handleToggle}
438        expandDirection="vertical"
439        expandBehavior="push"
440        onExpandStart={() => toast.info("Expanding controlled card...")}
441        onExpandEnd={() => toast.info("Controlled card expanded!")}
442      >
443        <ExpandableCard
444          collapsedSize={{ width: 300, height: 100 }}
445          expandedSize={{ width: 300, height: 300 }}
446        >
447          <ExpandableTrigger>
448            <ExpandableCardHeader>
449              <h3 className="text-lg font-semibold">
450                Controlled Expandable Card
451              </h3>
452              <Badge variant="secondary">
453                {isExpanded ? "Expanded" : "Collapsed"}
454              </Badge>
455            </ExpandableCardHeader>
456          </ExpandableTrigger>
457          <ExpandableCardContent>
458            <p className="mb-4">
459              This card's expanded state is controlled externally.
460            </p>
461            <ExpandableContent preset="fade" stagger staggerChildren={0.1}>
462              <p className="mb-2">This content fades in when expanded.</p>
463              <p className="mb-2">
464                It uses staggered animation for child elements.
465              </p>
466              <p>The expansion is controlled by the button above.</p>
467            </ExpandableContent>
468          </ExpandableCardContent>
469          <ExpandableCardFooter>
470            <ExpandableContent preset="slide-up">
471              <p className="text-sm text-gray-500">
472                Footer content slides up when expanded
473              </p>
474            </ExpandableContent>
475          </ExpandableCardFooter>
476        </ExpandableCard>
477      </Expandable>
478    </div>
479  )
480}
481 
482export function ExpandableCardExamples() {
483  return (
484    <div className="p-8 w-full max-w-7xl mx-auto space-y-12">
485      <div className="flex flex-col items-center space-y-24">
486        <div className="min-h-[480px]">
487          <DesignSyncExample />
488        </div>
489        <div className="flex gap-24 min-h-[600px]">
490          <ProductShowcaseCard />
491          <WeatherForecastCard />
492        </div>
493        {/* <div>
494        </div> */}
495        {/* <div>
496          <h2 className="text-xl font-semibold mb-4">Controlled Expandable</h2>
497          <ControlledExpandableCard />
498        </div> */}
499      </div>
500    </div>
501  )
502}