# About 1

> Overlapping photo pair with credentials beside a numbered accordion of reasons (from Marbrook)

A shadcn/ui block from the [@cnippet-blocks](https://blocks.cnippet.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet-blocks/about-1).

- Page, with a live preview: https://shadcn.exchange/r/cnippet-blocks/about-1
- Screenshot: https://shadcn.exchange/shot/cnippet-blocks/about-1/light.png
- Registry item: https://blocks.cnippet.dev/r/about-1.json
- Categories: about
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cnippet-blocks/about-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `@cnippet/accordion`, `@cnippet/button`, `@cnippet/utils`

## Files

### `registry/default/sections/about-1/index.tsx`

```tsx
"use client";

import { ArrowUpRight, Minus, Plus } from "lucide-react";
import Image from "next/image";
import Link from "next/link";

import {
  Accordion,
  AccordionItem,
  AccordionPanel,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

import type { AboutContent } from "../_types/about";
import type { Surface } from "../_types/common";
import { defaultAbout1Content } from "./data";
import { SectionHeader, surfaceClass, toneFor } from "./section-header";

/** Text tones for each band. On `primary` the copy reads off the inverted
 *  foreground; everywhere else it uses the regular muted scale. */
const tones = {
  dark: {
    border: "border-primary-foreground/10",
    faint: "text-primary-foreground/35",
    icon: "border-primary-foreground/20 text-secondary",
    muted: "text-primary-foreground/60",
    strong: "text-primary-foreground",
    value: "text-secondary",
  },
  light: {
    border: "border-border",
    faint: "text-muted-foreground/70",
    icon: "border-input text-primary",
    muted: "text-muted-foreground",
    strong: "text-primary",
    value: "text-primary",
  },
};

/** An overlapping pair of photos with credentials, beside a numbered
 *  accordion of reasons and two calls to action. */
export default function About1({
  content = defaultAbout1Content,
  surface = "primary",
}: {
  content?: AboutContent;
  surface?: Surface;
}) {
  const t = surface === "primary" ? tones.dark : tones.light;
  const [main, inset] = content.images;
  const [primaryAction, secondaryAction] = content.actions ?? [];

  return (
    <section className={cn("py-20 sm:py-28", surfaceClass[surface])}>
      <div className="mx-auto max-w-7xl px-5 sm:px-8 lg:px-12">
        <div className="grid gap-14 lg:grid-cols-12 lg:gap-16">
          <div className="lg:col-span-5">
            <div className="relative aspect-4/5 overflow-hidden rounded-lg">
              <Image
                alt={main.alt}
                className="object-cover"
                fill
                sizes="(min-width: 1024px) 40vw, 100vw"
                src={main.src}
              />
            </div>

            {inset && (
              <div
                className={cn(
                  "relative -mt-16 ml-auto hidden w-3/5 rounded-lg border-8 sm:block",
                  surface === "primary"
                    ? "border-primary"
                    : "border-background",
                  surface === "muted" && "border-muted",
                )}
              >
                <div className="relative aspect-4/3 overflow-hidden rounded-sm">
                  <Image
                    alt={inset.alt}
                    className="object-cover"
                    fill
                    sizes="30vw"
                    src={inset.src}
                  />
                </div>
              </div>
            )}

            {content.credentials && content.credentials.length > 0 && (
              <div
                className={cn(
                  "mt-8 grid grid-cols-3 gap-6 border-t pt-6 sm:mt-4",
                  t.border,
                )}
              >
                {content.credentials.map((credential) => (
                  <div key={credential.label}>
                    <p className={cn("font-display text-2xl", t.value)}>
                      {credential.value}
                    </p>
                    <p className={cn("mt-1 text-xs leading-snug", t.muted)}>
                      {credential.label}
                    </p>
                  </div>
                ))}
              </div>
            )}
          </div>

          <div className="lg:col-span-7">
            <SectionHeader
              description={content.description}
              eyebrow={content.eyebrow}
              title={content.title}
              tone={toneFor(surface)}
            />

            {content.items && content.items.length > 0 ? (
              <Accordion
                className={cn("mt-10 border-t", t.border)}
                defaultValue={[content.items[0].id]}
              >
                {content.items.map((item) => (
                  <AccordionItem
                    className={cn("border-b last:border-b-0", t.border)}
                    key={item.id}
                    value={item.id}
                  >
                    <AccordionTrigger
                      className="items-center gap-6 py-6 text-left [&:not([data-panel-open])_[data-icon=minus]]:hidden [&[data-panel-open]_[data-icon=plus]]:hidden"
                      icon={
                        <span
                          className={cn(
                            "flex size-8 shrink-0 items-center justify-center rounded-md border",
                            t.icon,
                          )}
                        >
                          <Plus className="size-4" data-icon="plus" />
                          <Minus className="size-4" data-icon="minus" />
                        </span>
                      }
                    >
                      <span className="flex items-baseline gap-5">
                        <span className={cn("font-mono text-xs", t.faint)}>
                          {item.id}
                        </span>
                        <span className={cn("font-display text-2xl", t.strong)}>
                          {item.title}
                        </span>
                      </span>
                    </AccordionTrigger>
                    <AccordionPanel
                      className={cn(
                        "max-w-xl pb-7 pl-11 text-sm leading-relaxed",
                        t.muted,
                      )}
                    >
                      {item.body}
                    </AccordionPanel>
                  </AccordionItem>
                ))}
              </Accordion>
            ) : (
              <div className={cn("mt-8 space-y-5 leading-relaxed", t.muted)}>
                {content.body?.map((paragraph) => (
                  <p key={paragraph.slice(0, 40)}>{paragraph}</p>
                ))}
              </div>
            )}

            {(primaryAction || secondaryAction) && (
              <div className="mt-10 flex flex-wrap items-center gap-4">
                {primaryAction && (
                  <Button
                    className="border-none bg-secondary px-7 text-secondary-foreground shadow-none hover:bg-secondary/90"
                    render={<Link href={primaryAction.href} />}
                    size="xl"
                  >
                    {primaryAction.label}
                  </Button>
                )}
                {secondaryAction && (
                  <Link
                    className={cn(
                      "group inline-flex items-center gap-2 text-sm underline-offset-4 transition-colors hover:underline",
                      surface === "primary"
                        ? "text-primary-foreground/70 hover:text-secondary"
                        : "text-primary",
                    )}
                    href={secondaryAction.href}
                  >
                    {secondaryAction.label}
                    <ArrowUpRight className="size-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
                  </Link>
                )}
              </div>
            )}
          </div>
        </div>
      </div>
    </section>
  );
}
```

### `registry/default/sections/about-1/data.ts`

```ts
import type { AboutContent } from "../_types/about";

export const defaultAbout1Content: AboutContent = {
  actions: [
    {
      href: "/about",
      label: "Our story",
    },
    {
      href: "/team",
      label: "Meet the advisors",
    },
  ],
  credentials: [
    {
      label: "Licensed since",
      value: "1994",
    },
    {
      label: "Advisors on staff",
      value: "24",
    },
    {
      label: "Repeat & referred",
      value: "68%",
    },
  ],
  description:
    "Marbrook was founded in 1994 above a chandlery on Rowan Street. We still work the same way: fewer listings, longer relationships, and an honest number even when it is not the one you hoped for.",
  eyebrow: "Why Marbrook",
  images: [
    {
      alt: "Marbrook advisors reviewing a floor plan on site",
      src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786954292/templates/estate/marbrook/cndn_marbrook014.jpg",
    },
    {
      alt: "Interior of a listed harbour residence",
      src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786954292/templates/estate/marbrook/cndn_marbrook016.jpg",
    },
  ],
  items: [
    {
      body: "We only list what we would buy. Each home is inspected, surveyed, and priced against comparable sales we handled ourselves — not an algorithm's guess.",
      id: "01",
      title: "Every listing is vetted in person",
    },
    {
      body: "Roughly a third of what we sell never reaches a portal. Registered buyers see it first, privately, which keeps negotiations calm and prices honest.",
      id: "02",
      title: "A quiet, off-market network",
    },
    {
      body: "One advisor stays with you from first viewing to handover — coordinating surveys, solicitors, finance, and the removal van.",
      id: "03",
      title: "One advisor, start to finish",
    },
    {
      body: "Our in-house team stages, photographs, and films every property. Good rooms deserve better than a wide-angle lens at noon.",
      id: "04",
      title: "Photography that sells the room",
    },
  ],
  title: "A small practice, deliberately.",
};
```

### `registry/default/sections/about-1/section-header.tsx`

```tsx
import type React from "react";

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

import type { Surface } from "../_types/common";

/** The small uppercase label that opens most headers. */
export const eyebrowClass =
  "inline-flex items-center gap-2 font-medium text-[0.6875rem] uppercase tracking-[0.22em]";

export const surfaceClass: Record<Surface, string> = {
  default: "bg-background text-foreground",
  muted: "bg-muted text-foreground",
  primary: "bg-primary text-primary-foreground",
  secondary: "bg-secondary text-secondary-foreground",
};

/**
 * Which band the header sits on. `photo` keeps literal white, because
 * `--primary-foreground` inverts in dark mode and would be unreadable over a
 * photograph.
 */
export type SectionTone = "light" | "photo" | "primary" | "secondary";

export function toneFor(surface: Surface): SectionTone {
  if (surface === "primary") return "primary";
  if (surface === "secondary") return "secondary";
  return "light";
}

const toneClasses: Record<
  SectionTone,
  { eyebrow: string; heading: string; lede: string; tick: string }
> = {
  light: {
    eyebrow: "text-muted-foreground/70",
    heading: "text-primary",
    lede: "text-muted-foreground",
    tick: "bg-secondary",
  },
  photo: {
    eyebrow: "text-white/50",
    heading: "text-white",
    lede: "text-white/65",
    tick: "bg-secondary",
  },
  primary: {
    eyebrow: "text-primary-foreground/40",
    heading: "text-primary-foreground",
    lede: "text-primary-foreground/60",
    tick: "bg-secondary",
  },
  secondary: {
    eyebrow: "text-secondary-foreground/50",
    heading: "text-secondary-foreground",
    lede: "text-secondary-foreground/70",
    tick: "bg-primary",
  },
};

export function SectionHeader({
  action,
  actionAt = "sm",
  className,
  description,
  eyebrow,
  size = "default",
  title,
  tone = "light",
}: {
  /** Rendered beside the heading block once `actionAt` is reached. */
  action?: React.ReactNode;
  /** A text link fits from `sm`; a row of filter tabs needs `lg`. */
  actionAt?: "lg" | "sm";
  className?: string;
  description?: React.ReactNode;
  eyebrow: string;
  /** `sm` is the narrow-sidebar size used by 4/8 splits. */
  size?: "default" | "sm";
  title: React.ReactNode;
  tone?: SectionTone;
}): React.ReactElement {
  const tones = toneClasses[tone];

  return (
    <div
      className={cn(
        action && "flex flex-col gap-6",
        action &&
          (actionAt === "lg"
            ? "gap-8 lg:flex-row lg:items-end lg:justify-between"
            : "sm:flex-row sm:items-end sm:justify-between"),
        className,
      )}
    >
      <div className={cn(action && "max-w-xl")}>
        <p className={cn(eyebrowClass, tones.eyebrow)}>
          <span className={cn("size-1", tones.tick)} />
          {eyebrow}
        </p>

        <h2
          className={cn(
            "mt-5 font-display",
            tones.heading,
            size === "sm"
              ? "text-3xl leading-[1.1] sm:text-4xl"
              : "text-4xl leading-[1.05] sm:text-5xl",
          )}
        >
          {title}
        </h2>

        {description && (
          <p className={cn("mt-4 max-w-xl leading-relaxed", tones.lede)}>
            {description}
          </p>
        )}
      </div>

      {action}
    </div>
  );
}
```

### `registry/default/sections/_types/about.ts`

```ts
import type { Highlight, ImageAsset, LinkAction, SectionIntro } from "./common";

export type AboutContent = SectionIntro & {
  actions?: LinkAction[];
  /** Prose paragraphs. */
  body?: string[];
  credentials?: Highlight[];
  /** Main image first, optional inset image second. */
  images: [ImageAsset, ImageAsset?];
  /** Numbered points. */
  items?: { body: string; id: string; title: string }[];
};
```

### `registry/default/sections/_types/common.ts`

```ts
/**
 * Shapes shared by every token-based section. Each section ships this file
 * alongside its own category types, so installing several sections simply
 * rewrites the same file.
 */

/** Which band a section sits on. A style choice, never part of `content`. */
export type Surface = "default" | "muted" | "primary" | "secondary";

export type ImageAsset = {
  alt: string;
  src: string;
};

export type LinkAction = {
  href: string;
  label: string;
};

/** Eyebrow + heading + optional lede, the header most sections open with. */
export type SectionIntro = {
  description?: string;
  eyebrow: string;
  title: string;
};

/** A figure that counts up when it scrolls into view. */
export type Stat = {
  decimals?: number;
  label: string;
  prefix?: string;
  suffix?: string;
  value: number;
};

/** A figure that is displayed as written ("1,240+", "4.9/5"). */
export type Highlight = {
  label: string;
  value: string;
};

export type FormField = {
  label: string;
  name: string;
  /** Only for `type: "select"`. The first option is the default. */
  options?: string[];
  placeholder?: string;
  required?: boolean;
  /** `full` spans both columns of a two-column form. */
  span?: "full" | "half";
  type?: "email" | "select" | "tel" | "text" | "textarea";
};

export type FormContent = {
  description?: string;
  fields: FormField[];
  note?: string;
  submitLabel: string;
  success: {
    description: string;
    resetLabel: string;
    title: string;
  };
  title: string;
};
```
