---
title: Features
description: Feature sections for explaining what matters clearly.
group: Blocks
order: 23
---

Layouts for walking through the useful parts of a product. Replace the copy, icons, and imagery with your own.

## Three up

Three outlined cards with image placeholders, titles, and short copy. They stack on narrow widths.

```tsx
"use client";

import { cn } from "@/lib/utils";

type Features01Props = {
  className?: string;
};

const FEATURES = [
  {
    title: "Token ready",
    description: "Restyle blocks with your theme tokens.",
    image: "/assets/brand/demos/attachments/attachment-1.png",
  },
  {
    title: "Copy and ship",
    description: "Install a block and drop it into a page.",
    image: "/assets/brand/demos/attachments/attachment-2.png",
  },
  {
    title: "Built to compose",
    description: "Mix sections without fighting the layout.",
    image: "/assets/brand/demos/attachments/attachment-3.png",
  },
] as const;

export function Features01({ className }: Features01Props) {
  return (
    <section className={cn("@container w-full bg-background", className)}>
      <div className="mx-auto w-full max-w-4xl px-6 py-14">
        <ul className="grid w-full gap-4 @[40rem]:grid-cols-3">
          {FEATURES.map((feature) => (
            <li
              key={feature.title}
              className="rounded-xl border border-border p-2"
            >
              <div className="aspect-[16/10] w-full overflow-hidden rounded-sm bg-muted">
                <img
                  src={feature.image}
                  alt=""
                  className="size-full object-cover"
                />
              </div>
              <div className="px-1 pt-3 pb-1">
                <h3 className="text-sm font-medium tracking-wide text-foreground">
                  {feature.title}
                </h3>
                <p className="mt-1.5 text-sm tracking-wide text-muted-foreground">
                  {feature.description}
                </p>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add features-01
```

### Usage

```tsx
import { Features01 } from "@/components/blocks/features/features-01"
```

```tsx
<Features01 />
```

## Accordion split

An accordion on the left and a matching preview on the right. One item always remains open, with plus and minus controls.

```tsx
"use client";

import { useState } from "react";
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { cn } from "@/lib/utils";

type Features02Props = {
  className?: string;
};

const FEATURES = [
  {
    value: "1",
    title: "Token ready",
    description: "Restyle every block with your colors, radii, and type.",
    image: "/assets/brand/demos/attachments/attachment-1.png",
  },
  {
    value: "2",
    title: "Copy and ship",
    description: "Install a block, drop it in a page, and keep moving.",
    image: "/assets/brand/demos/attachments/attachment-2.png",
  },
  {
    value: "3",
    title: "Built to compose",
    description: "Mix heroes, nav, and sections without fighting the layout.",
    image: "/assets/brand/demos/attachments/attachment-3.png",
  },
] as const;

export function Features02({ className }: Features02Props) {
  const [open, setOpen] = useState("1");
  const index = Math.max(
    0,
    FEATURES.findIndex((feature) => feature.value === open),
  );

  return (
    <section className={cn("@container w-full bg-background", className)}>
      <div
        data-slot="block-split"
        className="mx-auto flex w-full max-w-4xl flex-col gap-6 px-6 py-14 lg:flex-row lg:items-stretch lg:gap-10"
      >
        <div className="flex min-w-0 flex-1 flex-col justify-center">
          <Accordion
            type="single"
            collapsible={false}
            value={open}
            onValueChange={(value) => {
              if (typeof value === "string" && value) setOpen(value);
            }}
          >
            {FEATURES.map((feature) => (
              <AccordionItem key={feature.value} value={feature.value}>
                <AccordionTrigger indicator="plus">
                  {feature.title}
                </AccordionTrigger>
                <AccordionContent>{feature.description}</AccordionContent>
              </AccordionItem>
            ))}
          </Accordion>
        </div>

        <div className="relative min-h-[16rem] flex-1 overflow-hidden rounded-md bg-muted lg:min-h-[20rem]">
          <div
            className="absolute inset-x-0 top-0 w-full will-change-transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
            style={{
              height: `${FEATURES.length * 100}%`,
              transform: `translate3d(0, -${(index * 100) / FEATURES.length}%, 0)`,
            }}
          >
            {FEATURES.map((feature) => (
              <div
                key={feature.value}
                className="w-full"
                style={{ height: `${100 / FEATURES.length}%` }}
              >
                <img
                  src={feature.image}
                  alt=""
                  className="size-full object-cover"
                />
              </div>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add features-02
```

### Usage

```tsx
import { Features02 } from "@/components/blocks/features/features-02"
```

```tsx
<Features02 />
```

## Alternating

Image-and-copy rows that alternate sides, then stack on narrow widths.

```tsx
"use client";

import { cn } from "@/lib/utils";

type Features03Props = {
  className?: string;
};

const FEATURES = [
  {
    title: "Token ready",
    description: "Restyle every block with your colors, radii, and type.",
    image: "/assets/brand/demos/attachments/attachment-1.png",
  },
  {
    title: "Copy and ship",
    description: "Install a block, drop it in a page, and keep moving.",
    image: "/assets/brand/demos/attachments/attachment-2.png",
  },
  {
    title: "Built to compose",
    description: "Mix heroes, nav, and sections without fighting the layout.",
    image: "/assets/brand/demos/attachments/attachment-3.png",
  },
] as const;

export function Features03({ className }: Features03Props) {
  return (
    <section className={cn("w-full bg-background", className)}>
      <div className="mx-auto flex w-full max-w-4xl flex-col gap-12 px-6 py-14 lg:gap-16">
        {FEATURES.map((feature, index) => {
          const flip = index % 2 === 1;

          return (
            <div
              key={feature.title}
              data-slot="block-split"
              className={cn(
                "flex flex-col gap-6 lg:flex-row lg:items-center lg:gap-10",
                flip && "lg:flex-row-reverse",
              )}
            >
              <div className="min-h-[14rem] flex-1 overflow-hidden rounded-md bg-muted lg:min-h-[18rem]">
                <img
                  src={feature.image}
                  alt=""
                  className="size-full object-cover"
                />
              </div>
              <div className="flex flex-1 flex-col justify-center">
                <h3 className="text-base font-medium tracking-wide text-foreground">
                  {feature.title}
                </h3>
                <p className="mt-2 max-w-sm text-sm tracking-wide text-muted-foreground">
                  {feature.description}
                </p>
              </div>
            </div>
          );
        })}
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add features-03
```

### Usage

```tsx
import { Features03 } from "@/components/blocks/features/features-03"
```

```tsx
<Features03 />
```

## Comparison

A feature table comparing one product mark with a stacked group of logos.

```card
title: Demo brands
description: Demo logos are placeholders. Swap in your own before shipping.
```

```tsx
"use client";

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
  AvatarImage,
} from "@/components/ui/avatar";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";

type Features04Props = {
  className?: string;
};

const ROWS = [
  { feature: "Realtime sync", ours: true, others: false },
  { feature: "Shared workspaces", ours: true, others: true },
  { feature: "Keyboard-first UI", ours: true, others: false },
  { feature: "Guest access", ours: true, others: false },
  { feature: "Audit history", ours: true, others: false },
] as const;

const OTHER_LOGOS = [
  "/assets/icons/logos/slack.svg",
  "/assets/icons/logos/notion.svg",
  "/assets/icons/logos/figma.svg",
] as const;

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path
        d="M5 12.5 10 17.5 19 7.5"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function DashIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
      <path
        d="M6 12h12"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
      />
    </svg>
  );
}

function CellMark({ on }: { on: boolean }) {
  return on ? (
    <CheckIcon className="size-4 text-foreground" />
  ) : (
    <DashIcon className="size-4 text-muted-foreground" />
  );
}

function LogoAvatar({ src }: { src: string }) {
  return (
    <Avatar size="xs">
      <AvatarImage
        src={src}
        alt=""
        className="object-contain p-1 brightness-0 dark:invert"
      />
      <AvatarFallback> </AvatarFallback>
    </Avatar>
  );
}

export function Features04({ className }: Features04Props) {
  return (
    <section className={cn("w-full bg-background", className)}>
      <div className="mx-auto w-full max-w-3xl px-6 py-14">
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead className="w-[46%]">Feature</TableHead>
              <TableHead className="w-[27%]">
                <span className="inline-flex items-center gap-2 text-foreground">
                  <LogoAvatar src="/assets/icons/logos/linear.svg" />
                  Linear
                </span>
              </TableHead>
              <TableHead className="w-[27%]">
                <AvatarGroup overlap="sm">
                  {OTHER_LOGOS.map((src) => (
                    <LogoAvatar key={src} src={src} />
                  ))}
                  <AvatarGroupCount className="size-6 text-[10px]">
                    +4
                  </AvatarGroupCount>
                </AvatarGroup>
              </TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {ROWS.map((row) => (
              <TableRow key={row.feature}>
                <TableCell className="text-foreground">{row.feature}</TableCell>
                <TableCell>
                  <CellMark on={row.ours} />
                </TableCell>
                <TableCell>
                  <CellMark on={row.others} />
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add features-04
```

### Usage

```tsx
import { Features04 } from "@/components/blocks/features/features-04"
```

```tsx
<Features04 />
```
