# Bento Closer Look

A feature spotlight that pairs an auto-advancing accordion of features with a matching cross-fading image.

Bento Closer Look is an open-source block from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/bento-closer-look

**React**

**Example**

```tsx title="components/blocks/bento-closer-look.tsx"
"use client"

import { PlusIcon } from "@phosphor-icons/react"
import * as React from "react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=1000&q=80&auto=format&fit=crop`

type Feature = {
  title: string
  description: string
  image: string
  alt: string
}

const features: [Feature, ...Feature[]] = [
  {
    title: "Camera Control",
    description:
      "A faster way to take photos. Slide to zoom, press lightly to focus.",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
  },
  {
    title: "Fusion camera",
    description:
      "Three rear cameras capture detail at every angle with 48 MP resolution.",
    image: photo("1604677209244-6569d050557a"),
    alt: "A camera lens lit in red and pink",
  },
  {
    title: "Aluminum body",
    description: "A unibody design that spreads heat better and weighs less.",
    image: photo("1664883410931-2edffc8be5e6"),
    alt: "A silver phone on a light grey background",
  },
  {
    title: "New colors",
    description: "Cosmic Orange, Deep Blue and Silver.",
    image: photo("1633596683562-4a47eb4983c5"),
    alt: "Layered orange paper waves",
  },
]

export function BentoCloserLook() {
  const [active, setActive] = React.useState(features[0].title)
  const [auto, setAuto] = React.useState(true)

  React.useEffect(() => {
    if (!auto) return
    const timer = window.setInterval(() => {
      setActive((current) => {
        const index = features.findIndex((item) => item.title === current)
        return features[(index + 1) % features.length]?.title ?? current
      })
    }, 4000)
    return () => window.clearInterval(timer)
  }, [auto])

  return (
    <section className="flex flex-col gap-6 bg-muted p-5 md:p-8">
      <h2 className="text-4xl font-semibold tracking-tight md:text-5xl">
        Take a closer look.
      </h2>
      <div className="grid overflow-hidden rounded-[1.75rem] bg-background md:min-h-117 md:grid-cols-[minmax(16rem,0.8fr)_1.2fr]">
        <Accordion
          value={[active]}
          onValueChange={(value: string[]) => {
            setAuto(false)
            if (value[0]) setActive(value[0])
          }}
          className="order-2 justify-center gap-1.5 p-4 md:order-1 md:py-7 md:ps-7 md:pe-4.5"
        >
          {features.map((feature) => (
            <AccordionItem
              key={feature.title}
              value={feature.title}
              className="ms-0 rounded-[1.125rem] px-5 not-first:border-t-0 transition-[background-color,padding] duration-400 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:bg-muted data-open:py-2 motion-reduce:transition-none"
            >
              <AccordionTrigger className="justify-start gap-2.5 pe-0 text-lg font-semibold hover:no-underline **:data-[slot=accordion-trigger-icon]:hidden">
                <span className="flex size-6.5 shrink-0 items-center justify-center rounded-full bg-secondary text-secondary-foreground transition-[rotate,background-color,color] duration-350 ease-[cubic-bezier(0.32,0.72,0,1)] group-aria-expanded/accordion-trigger:rotate-45 group-aria-expanded/accordion-trigger:bg-foreground group-aria-expanded/accordion-trigger:text-background motion-reduce:transition-none">
                  <PlusIcon className="size-3.5" />
                </span>
                {feature.title}
              </AccordionTrigger>
              <AccordionContent className="ps-9 pe-0 text-base leading-normal">
                {feature.description}
              </AccordionContent>
            </AccordionItem>
          ))}
        </Accordion>
        <div className="relative order-1 m-4 aspect-4/3 overflow-hidden rounded-[1.25rem] bg-secondary md:order-2 md:aspect-auto">
          {features.map((feature) => (
            <img
              key={feature.title}
              src={feature.image}
              alt={feature.alt}
              aria-hidden={feature.title !== active}
              data-active={feature.title === active ? "" : undefined}
              className="absolute inset-0 size-full scale-104 object-cover opacity-0 transition-[opacity,scale] duration-[700ms,1200ms] ease-[ease,cubic-bezier(0.32,0.72,0,1)] data-active:scale-100 data-active:opacity-100 motion-reduce:transition-none"
            />
          ))}
        </div>
      </div>
    </section>
  )
}
```

A feature spotlight with a list of features on one side and a photo on the other that cross-fades to match the open item. The list advances on its own every four seconds until someone opens an item, then stays put. On mobile the photo moves above the list. Use it to walk through a handful of product features without a carousel.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/bento-closer-look
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add blocks bento-closer-look
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Add the pieces it builds on: [Accordion](https://ui.suiss.app/docs/components/accordion).

3. Copy the source into `components/blocks/bento-closer-look.tsx`:

```tsx title="components/blocks/bento-closer-look.tsx"
"use client"

import { PlusIcon } from "@phosphor-icons/react"
import * as React from "react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=1000&q=80&auto=format&fit=crop`

type Feature = {
  title: string
  description: string
  image: string
  alt: string
}

const features: [Feature, ...Feature[]] = [
  {
    title: "Camera Control",
    description:
      "A faster way to take photos. Slide to zoom, press lightly to focus.",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
  },
  {
    title: "Fusion camera",
    description:
      "Three rear cameras capture detail at every angle with 48 MP resolution.",
    image: photo("1604677209244-6569d050557a"),
    alt: "A camera lens lit in red and pink",
  },
  {
    title: "Aluminum body",
    description: "A unibody design that spreads heat better and weighs less.",
    image: photo("1664883410931-2edffc8be5e6"),
    alt: "A silver phone on a light grey background",
  },
  {
    title: "New colors",
    description: "Cosmic Orange, Deep Blue and Silver.",
    image: photo("1633596683562-4a47eb4983c5"),
    alt: "Layered orange paper waves",
  },
]

export function BentoCloserLook() {
  const [active, setActive] = React.useState(features[0].title)
  const [auto, setAuto] = React.useState(true)

  React.useEffect(() => {
    if (!auto) return
    const timer = window.setInterval(() => {
      setActive((current) => {
        const index = features.findIndex((item) => item.title === current)
        return features[(index + 1) % features.length]?.title ?? current
      })
    }, 4000)
    return () => window.clearInterval(timer)
  }, [auto])

  return (
    <section className="flex flex-col gap-6 bg-muted p-5 md:p-8">
      <h2 className="text-4xl font-semibold tracking-tight md:text-5xl">
        Take a closer look.
      </h2>
      <div className="grid overflow-hidden rounded-[1.75rem] bg-background md:min-h-117 md:grid-cols-[minmax(16rem,0.8fr)_1.2fr]">
        <Accordion
          value={[active]}
          onValueChange={(value: string[]) => {
            setAuto(false)
            if (value[0]) setActive(value[0])
          }}
          className="order-2 justify-center gap-1.5 p-4 md:order-1 md:py-7 md:ps-7 md:pe-4.5"
        >
          {features.map((feature) => (
            <AccordionItem
              key={feature.title}
              value={feature.title}
              className="ms-0 rounded-[1.125rem] px-5 not-first:border-t-0 transition-[background-color,padding] duration-400 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:bg-muted data-open:py-2 motion-reduce:transition-none"
            >
              <AccordionTrigger className="justify-start gap-2.5 pe-0 text-lg font-semibold hover:no-underline **:data-[slot=accordion-trigger-icon]:hidden">
                <span className="flex size-6.5 shrink-0 items-center justify-center rounded-full bg-secondary text-secondary-foreground transition-[rotate,background-color,color] duration-350 ease-[cubic-bezier(0.32,0.72,0,1)] group-aria-expanded/accordion-trigger:rotate-45 group-aria-expanded/accordion-trigger:bg-foreground group-aria-expanded/accordion-trigger:text-background motion-reduce:transition-none">
                  <PlusIcon className="size-3.5" />
                </span>
                {feature.title}
              </AccordionTrigger>
              <AccordionContent className="ps-9 pe-0 text-base leading-normal">
                {feature.description}
              </AccordionContent>
            </AccordionItem>
          ))}
        </Accordion>
        <div className="relative order-1 m-4 aspect-4/3 overflow-hidden rounded-[1.25rem] bg-secondary md:order-2 md:aspect-auto">
          {features.map((feature) => (
            <img
              key={feature.title}
              src={feature.image}
              alt={feature.alt}
              aria-hidden={feature.title !== active}
              data-active={feature.title === active ? "" : undefined}
              className="absolute inset-0 size-full scale-104 object-cover opacity-0 transition-[opacity,scale] duration-[700ms,1200ms] ease-[ease,cubic-bezier(0.32,0.72,0,1)] data-active:scale-100 data-active:opacity-100 motion-reduce:transition-none"
            />
          ))}
        </div>
      </div>
    </section>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { BentoCloserLook } from "@/components/blocks/bento-closer-look"
```

```tsx
<BentoCloserLook />
```

Edit the `features` array; each entry has a title, description, image and alt text. Change the 4000 ms interval or the heading in the component. Nothing needs wiring to a backend.

## Notes

The automatic rotation keeps running under reduced motion until the user opens an item, so drop the interval if that matters for your audience. Photos are Unsplash placeholders.

