Breadcrumb

This component can be configured based on the max items, icons, items, and render item props. The only animations present are hover animations and the opening of the collapsed menu through the ellipsis since this component primarily serves as navigation, animations are at a minimum here. Given the nature of the ellipsis button opening a menu, that component is only rendered if the length of items is greater than the enforced max items coupled with an ease out animation that feels on par to the feeling of opening something on your phone.

Code

// Breadcrumb.tsx
import React from 'react'
import { ChevronRight, Ellipsis } from 'lucide-react'
import { useState, useEffect, useRef } from 'react'
import CollapsedMenu from './CollapsedMenu'
import type { BreadcrumbItem } from './types'
import { AnimatePresence, motion } from 'motion/react'
import BreadcrumbStandard from './BreadcrumbStandard'
import BreadcrumbStart from './BreadcrumbStart'
import BreadcrumbEnd from './BreadcrumbEnd'

interface BreadCrumbProps {
    items: BreadcrumbItem[]
    maxItems?: number
    separator?: React.ReactNode
    renderItem?:(item:BreadcrumbItem) => React.ReactNode
}

const Breadcrumb = ({items, maxItems = 5, separator, renderItem} : BreadCrumbProps) => {

    const [isExpanded, setIsExpanded] = useState<boolean>(false);
    const elipsisRef = useRef<HTMLButtonElement>(null);

    const enforcedMaxItems = Math.max(maxItems, 3);
    const listItemCount = enforcedMaxItems - 2;

    let visibleStart: BreadcrumbItem[] = [];
    let elipsesItems: BreadcrumbItem[] = [];
    let visibleEnd: BreadcrumbItem[] = [];
    
    if(items.length > enforcedMaxItems) {
      visibleStart = items.slice(0, 1);
      visibleEnd = items.slice(items.length - listItemCount, items.length);
      elipsesItems = items.slice(1, items.length - listItemCount)
    }

    const handleButtonClick = () => {
      setIsExpanded(prev => !prev)
    }

    useEffect(() => {
      const clickObserver = (e:MouseEvent) => {
        if(!isExpanded) return;
        
        if(e.target instanceof Node){
          if(elipsisRef.current && !elipsisRef.current.contains(e.target)){
            setIsExpanded(false)
          }
        }
      }
      window.addEventListener('click', clickObserver)
      return () => {
        window.removeEventListener('click', clickObserver)
      }
    }, [isExpanded])


  return (
    <nav>
      <ul className='flex flex-row flex-wrap items-center gap-1'>
        {items.length <= enforcedMaxItems ? (        
            <BreadcrumbStandard
            items = {items}
            renderItem={renderItem}
            separator = {separator}
            />
        ) : (
          <>
            <BreadcrumbStart
            startItems={visibleStart}
            renderItem = {renderItem}
            separator = {separator}
            />
            <li className = "relative flex flex-row items-center gap-1 text-gray-500 hover:text-gray-700">
              <button
               ref = {elipsisRef}
               aria-label = "Show missing paths"
               onClick = {handleButtonClick}
               className = "p-1 rounded-md hover:bg-gray-200/70"
               >
                <Ellipsis className = "text-gray-500" size = {'16px'}/>
              </button>
              <AnimatePresence mode='popLayout'>
                {isExpanded === true && (
                  <motion.div
                  className = "absolute flex flex-col w-48 gap-1 bg-white border border-gray-200 rounded-md shadow-sm top-7"
                  initial = {{opacity: 0, scale: 0.95, y: -2}}
                  animate = {{opacity: 1, scale: 1, y: 0}}
                  exit = {{opacity: 0, scale: 0.95, y: -2}}
                  transition={{ease: 'easeOut', duration: 0.2}}
                  >
                    <CollapsedMenu
                     collapsedList={elipsesItems}
                     renderItem={renderItem}
                    /> 
                  </motion.div>
                )}
              </AnimatePresence>
              {separator ? separator : <ChevronRight size = {'16px'} className = "text-gray-400"/>}
            </li>
            <BreadcrumbEnd
            endItems = {visibleEnd}
            renderItem={renderItem}
            separator = {separator}
            />
          </>
        )}
      </ul>
    </nav>
  )
}

export default Breadcrumb

// BreadcrumbStart.tsx
import React from 'react'
import type { BreadcrumbItem } from './types'
import { ChevronRight } from 'lucide-react'

interface BreadcrumbStartProps {
    startItems: BreadcrumbItem[]
    renderItem?: (item:BreadcrumbItem) => React.ReactNode
    separator?: React.ReactNode
}

const BreadcrumbStart = ({startItems, renderItem, separator}: BreadcrumbStartProps) => {
  return (
    <>
        {startItems.map((start) =>
              <li key = {start.id} className = "flex flex-row items-center gap-1 text-gray-500 hover:text-gray-700">
                {renderItem ? renderItem(start) : <a href = {start.link}>{start.title}</a>}
                {separator ? separator : <ChevronRight size = {'16px'} className = "text-gray-500"/>}
              </li>
            )}
    </>
  )
}

export default BreadcrumbStart

// BreadcrumbStandard.tsx
import React from 'react'
import type { BreadcrumbItem } from './types'
import { ChevronRight } from 'lucide-react'

interface breadcrumbStandardProps{
    items: BreadcrumbItem[]
    renderItem?: (item:BreadcrumbItem) => React.ReactNode
    separator?: React.ReactNode
}

const BreadcrumbStandard = ({items, renderItem, separator}: breadcrumbStandardProps) => {
  return (
    <>
     {items.map((item, index) => {
            if(index === items.length - 1){
              return(
                <li key = {item.id} aria-current ="page" className = "text-gray-700">
                    {item.title}
                </li>
              )
            }
            return (
              <li key = {item.id} className = "flex flex-row items-center gap-1 text-gray-500 hover:text-gray-700">
                {renderItem ? renderItem(item) : <a href = {item.link}>{item.title}</a>}
                {separator ? separator : <ChevronRight size = {'16px'} className = "text-gray-500"/>}
              </li>
            )
          })}
    </>
  )
}

export default BreadcrumbStandard

// BreadcrumbEnd.tsx
import type { BreadcrumbItem } from "./types"
import { ChevronRight } from "lucide-react"

interface BreadcrumbEndProps {
    endItems: BreadcrumbItem[]
    renderItem?: (item:BreadcrumbItem) => React.ReactNode
    separator?: React.ReactNode
}

const BreadcrumbEnd = ({endItems, renderItem, separator}: BreadcrumbEndProps) => {
  return (
    <>
    {endItems.map((endItem, index) => {
              if (index === endItems.length - 1){
                return(
                  <li key = {endItem.id} aria-current ="page" className = "flex flex-row items-center gap-1 text-gray-700">
                    {endItem.title}
                </li>
                )
              }
              return (
              <li key = {endItem.id}>
                {renderItem ? renderItem(endItem) : <a href = {endItem.link}>{endItem.title}</a>}
                {separator ? separator : <ChevronRight size = {'16px'} className = "text-gray-400"/>}
              </li>
            )
            })}
    </>
  )
}

export default BreadcrumbEnd