Leave a star on GitHub! ⭐


Docs
Features

Features

These components are used to build the features section of the website.

Get Started Effortlessly

Three simple steps to bring your ideas to life

    1. Choose Your Component

    Select the component that best suits your needs from Eldora UI's versatile collection, designed to simplify and enhance your development process.

    2. Add Utility Helpers

    Enhance functionality by incorporating utility helpers that align with the selected component, ensuring seamless integration and customization.

    3. Copy and Paste the Code

    Simply copy and paste the provided code into your project, making the process quick and hassle-free. You're now ready to see the magic in action!

Installation

pnpm add framer-motion @radix-ui/react-accordion lucide-react

Copy and paste the following code of header into your project.

components/eldoraui/features.tsx

"use client";
 
import * as Accordion from "@radix-ui/react-accordion";
import { motion, useInView } from "framer-motion";
import type { ReactNode } from "react";
import React, { forwardRef, useEffect, useRef, useState } from "react";
 
import { cn } from "@/lib/utils";
 
type AccordionItemProps = {
  children: React.ReactNode;
  className?: string;
} & Accordion.AccordionItemProps;
 
const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
  ({ children, className, ...props }, forwardedRef) => (
    <Accordion.Item
      className={cn(
        "mt-px overflow-hidden focus-within:relative focus-within:z-10",
        className,
      )}
      {...props}
      ref={forwardedRef}
    >
      {children}
    </Accordion.Item>
  ),
);
AccordionItem.displayName = "AccordionItem";
 
interface AccordionTriggerProps {
  children: React.ReactNode;
  className?: string;
}
 
const AccordionTrigger = forwardRef<HTMLButtonElement, AccordionTriggerProps>(
  ({ children, className, ...props }, forwardedRef) => (
    <Accordion.Header className="flex">
      <Accordion.Trigger
        className={cn(
          "group flex flex-1 cursor-pointer items-center justify-between px-5 text-[15px] leading-none outline-none",
          className,
        )}
        {...props}
        ref={forwardedRef}
      >
        {children}
      </Accordion.Trigger>
    </Accordion.Header>
  ),
);
AccordionTrigger.displayName = "AccordionTrigger";
type AccordionContentProps = {
  children: ReactNode;
  className?: string;
} & Accordion.AccordionContentProps;
 
const AccordionContent = forwardRef<HTMLDivElement, AccordionContentProps>(
  ({ children, className, ...props }, forwardedRef) => (
    <Accordion.Content
      className={cn(
        "data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down overflow-hidden text-[15px] font-medium",
        className,
      )}
      {...props}
      ref={forwardedRef}
    >
      <div className="px-5 py-2">{children}</div>
    </Accordion.Content>
  ),
);
AccordionContent.displayName = "AccordionContent";
 
export interface FeaturesDataProps {
  id: number;
  title: string;
  content: string;
  image?: string;
  video?: string;
  icon?: React.ReactNode;
}
 
export interface FeaturesProps {
  collapseDelay?: number;
  ltr?: boolean;
  linePosition?: "left" | "right" | "top" | "bottom";
  data: FeaturesDataProps[];
}
 
export function Features({
  collapseDelay = 5000,
  ltr = false,
  linePosition = "left",
  data = [],
}: FeaturesProps) {
  const [currentIndex, setCurrentIndex] = useState<number>(-1);
  const carouselRef = useRef<HTMLUListElement>(null);
  const ref = useRef(null);
  const isInView = useInView(ref, {
    once: true,
    amount: 0.5,
  });
 
  useEffect(() => {
    const timer = setTimeout(() => {
      if (isInView) {
        setCurrentIndex(0);
      } else {
        setCurrentIndex(-1);
      }
    }, 100);
 
    return () => clearTimeout(timer);
  }, [isInView]);
 
  const scrollToIndex = (index: number) => {
    if (carouselRef.current) {
      const card = carouselRef.current.querySelectorAll(".card")[index];
      if (card) {
        const cardRect = card.getBoundingClientRect();
        const carouselRect = carouselRef.current.getBoundingClientRect();
        const offset =
          cardRect.left -
          carouselRect.left -
          (carouselRect.width - cardRect.width) / 2;
 
        carouselRef.current.scrollTo({
          left: carouselRef.current.scrollLeft + offset,
          behavior: "smooth",
        });
      }
    }
  };
 
  useEffect(() => {
    const timer = setInterval(() => {
      setCurrentIndex((prevIndex) =>
        prevIndex !== undefined ? (prevIndex + 1) % data.length : 0,
      );
    }, collapseDelay);
 
    return () => clearInterval(timer);
  }, [collapseDelay, currentIndex, data.length]);
 
  useEffect(() => {
    const handleAutoScroll = () => {
      const nextIndex =
        (currentIndex !== undefined ? currentIndex + 1 : 0) % data.length;
      scrollToIndex(nextIndex);
    };
 
    const autoScrollTimer = setInterval(handleAutoScroll, collapseDelay);
 
    return () => clearInterval(autoScrollTimer);
  }, [collapseDelay, currentIndex, data.length]);
 
  useEffect(() => {
    const carousel = carouselRef.current;
    if (carousel) {
      const handleScroll = () => {
        const scrollLeft = carousel.scrollLeft;
        const cardWidth = carousel.querySelector(".card")?.clientWidth || 0;
        const newIndex = Math.min(
          Math.floor(scrollLeft / cardWidth),
          data.length - 1,
        );
        setCurrentIndex(newIndex);
      };
 
      carousel.addEventListener("scroll", handleScroll);
      return () => carousel.removeEventListener("scroll", handleScroll);
    }
  }, [data.length]);
 
  return (
    <section ref={ref} id="features">
      <div className="container">
        <div className="mx-auto max-w-6xl">
          <div className="mx-auto my-12 grid h-full items-center gap-10 lg:grid-cols-2">
            <div
              className={` order-1 hidden lg:order-none lg:flex ${
                ltr ? "lg:order-2 lg:justify-end" : "justify-start"
              }`}
            >
              <Accordion.Root
                className=""
                type="single"
                defaultValue={`item-${currentIndex}`}
                value={`item-${currentIndex}`}
                onValueChange={(value) =>
                  setCurrentIndex(Number(value.split("-")[1]))
                }
              >
                {data.map((item, index) => (
                  <AccordionItem
                    key={item.id}
                    className="relative mb-8 last:mb-0"
                    value={`item-${index}`}
                  >
                    {linePosition === "left" || linePosition === "right" ? (
                      <div
                        className={`absolute inset-y-0 h-full w-0.5 overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
                          linePosition === "right"
                            ? "left-auto right-0"
                            : "left-0 right-auto"
                        }`}
                      >
                        <div
                          className={`absolute left-0 top-0 w-full ${
                            currentIndex === index ? "h-full" : "h-0"
                          } origin-top bg-primary transition-all ease-linear dark:bg-white`}
                          style={{
                            transitionDuration:
                              currentIndex === index
                                ? `${collapseDelay}ms`
                                : "0s",
                          }}
                        ></div>
                      </div>
                    ) : null}
 
                    {linePosition === "top" || linePosition === "bottom" ? (
                      <div
                        className={`absolute inset-x-0 h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30 ${
                          linePosition === "bottom" ? "bottom-0" : "top-0"
                        }`}
                      >
                        <div
                          className={`absolute left-0 ${
                            linePosition === "bottom" ? "bottom-0" : "top-0"
                          } h-full ${
                            currentIndex === index ? "w-full" : "w-0"
                          } origin-left bg-primary transition-all ease-linear dark:bg-white`}
                          style={{
                            transitionDuration:
                              currentIndex === index
                                ? `${collapseDelay}ms`
                                : "0s",
                          }}
                        ></div>
                      </div>
                    ) : null}
 
                    <div className="relative flex items-center">
                      <div className="item-box mx-2 flex size-12 shrink-0 items-center justify-center rounded-full bg-primary/10 sm:mx-6">
                        {item.icon}
                      </div>
 
                      <div>
                        <AccordionTrigger className="pl-0 text-xl font-bold">
                          {item.title}
                        </AccordionTrigger>
 
                        <AccordionTrigger className="justify-start pl-0 text-left text-[16px] leading-4">
                          {item.content}
                        </AccordionTrigger>
                      </div>
                    </div>
                  </AccordionItem>
                ))}
              </Accordion.Root>
            </div>
            <div
              className={`h-[350px] min-h-[200px] w-auto  ${
                ltr && "lg:order-1"
              }`}
            >
              {data[currentIndex]?.image ? (
                <motion.img
                  key={currentIndex}
                  src={data[currentIndex].image}
                  alt="feature"
                  className="aspect-auto size-full rounded-xl border border-neutral-300/50 object-cover object-left-top p-1 shadow-lg"
                  initial={{ opacity: 0, scale: 0.98 }}
                  animate={{ opacity: 1, scale: 1 }}
                  exit={{ opacity: 0, scale: 0.98 }}
                  transition={{ duration: 0.25, ease: "easeOut" }}
                />
              ) : data[currentIndex]?.video ? (
                <video
                  preload="auto"
                  src={data[currentIndex].video}
                  className="aspect-auto size-full rounded-lg object-cover shadow-lg"
                  autoPlay
                  loop
                  muted
                />
              ) : (
                <div className="aspect-auto size-full rounded-xl border border-neutral-300/50 bg-gray-200 p-1"></div>
              )}
            </div>
 
            <ul
              ref={carouselRef}
              className=" flex h-full snap-x snap-mandatory flex-nowrap overflow-x-auto py-10 [-ms-overflow-style:none] [-webkit-mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [mask-image:linear-gradient(90deg,transparent,black_20%,white_80%,transparent)] [scrollbar-width:none] lg:hidden [&::-webkit-scrollbar]:hidden"
              style={{
                padding: "50px calc(50%)",
              }}
            >
              {data.map((item, index) => (
                <div
                  key={item.id}
                  className="card relative mr-8 grid h-full max-w-60 shrink-0 items-start justify-center py-4 last:mr-0"
                  onClick={() => setCurrentIndex(index)}
                  style={{
                    scrollSnapAlign: "center",
                  }}
                >
                  <div className="absolute inset-y-0 left-0 right-auto h-0.5 w-full overflow-hidden rounded-lg bg-neutral-300/50 dark:bg-neutral-300/30">
                    <div
                      className={`absolute left-0 top-0 h-full ${
                        currentIndex === index ? "w-full" : "w-0"
                      } origin-top bg-primary transition-all ease-linear`}
                      style={{
                        transitionDuration:
                          currentIndex === index ? `${collapseDelay}ms` : "0s",
                      }}
                    ></div>
                  </div>
                  <h2 className="text-xl font-bold">{item.title}</h2>
                  <p className="mx-0 max-w-sm text-balance text-sm">
                    {item.content}
                  </p>
                </div>
              ))}
            </ul>
          </div>
        </div>
      </div>
    </section>
  );
}

AccordionItem Props

Prop NameTypeDefaultDescription
childrenReact.ReactNode-The content to be displayed inside the accordion item.
classNamestring-Additional CSS classes to style the accordion item.
...propsAccordion.AccordionItemProps-Props inherited from Radix UI's AccordionItem.

AccordionTrigger Props

Prop NameTypeDefaultDescription
childrenReact.ReactNode-The trigger content (e.g., title or heading) for the accordion item.
classNamestring-Additional CSS classes for styling the trigger.
...propsAccordion.AccordionTriggerProps-Props inherited from Radix UI's AccordionTrigger.

AccordionContent Props

Prop NameTypeDefaultDescription
childrenReact.ReactNode-The content to be displayed within the accordion.
classNamestring-Additional CSS classes for styling the accordion content.
...propsAccordion.AccordionContentProps-Props inherited from Radix UI's AccordionContent.

FeaturesDataProps (Used in Features)

Prop NameTypeDefaultDescription
idnumber-Unique identifier for each feature item.
titlestring-The title of the feature.
contentstring-The description or content of the feature.
imagestring (optional)-URL of the image for the feature.
videostring (optional)-URL of the video for the feature.
iconReact.ReactNode (optional)-Icon or visual representation for the feature.

Features Props

Prop NameTypeDefaultDescription
collapseDelaynumber5000Time (in milliseconds) to auto-collapse an accordion item.
ltrbooleanfalseDefines if the layout should switch to left-to-right.
linePosition"left" | "right" | "top" | "bottom""left"Specifies the position of the progress line indicator.
dataFeaturesDataProps[][]Array of feature objects containing title, content, image, video, etc.