# Feature 6

> Numbered service rows, each with a pitch and a checklist (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/feature-6).

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cnippet-blocks/feature-6.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/utils`

## Files

### `registry/default/sections/feature-6/index.tsx`

```tsx
import { Check } from "lucide-react";

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

import type { Surface } from "../_types/common";
import type { FeatureListContent } from "../_types/feature";
import { defaultFeature6Content } from "./data";
import { SectionHeader, surfaceClass, toneFor } from "./section-header";

/** Numbered rows on a hairline rule: the pitch on the left, its checklist
 *  on the right. Each row carries its `id` as an anchor. */
export default function Feature6({
  content = defaultFeature6Content,
  surface = "default",
}: {
  content?: FeatureListContent;
  surface?: Surface;
}) {
  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">
        <SectionHeader
          description={content.description}
          eyebrow={content.eyebrow}
          title={content.title}
          tone={toneFor(surface)}
        />

        <div className="mt-10 grid gap-px overflow-hidden rounded-lg bg-border">
          {content.items.map((item, index) => (
            <article
              // scroll-mt clears a fixed navbar when an anchor link lands here.
              className="grid scroll-mt-24 gap-8 bg-card p-8 lg:grid-cols-12 lg:gap-12 lg:p-12"
              id={item.id}
              key={item.id}
            >
              <div className="lg:col-span-5">
                <p className="font-mono text-muted-foreground/70 text-xs">
                  {String(index + 1).padStart(2, "0")}
                </p>
                <h3 className="mt-4 font-display text-3xl text-primary sm:text-4xl">
                  {item.title}
                </h3>
                <p className="mt-4 text-muted-foreground leading-relaxed">
                  {item.body}
                </p>
              </div>

              <ul className="grid gap-4 border-border border-t pt-8 lg:col-span-7 lg:border-t-0 lg:border-l lg:pt-0 lg:pl-12">
                {item.points.map((point) => (
                  <li
                    className="flex items-start gap-3 text-muted-foreground text-sm leading-relaxed"
                    key={point}
                  >
                    <span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-sm border border-input text-primary">
                      <Check className="size-3" />
                    </span>
                    {point}
                  </li>
                ))}
              </ul>
            </article>
          ))}
        </div>
      </div>
    </section>
  );
}
```

### `registry/default/sections/feature-6/data.ts`

```ts
import type { FeatureListContent } from "../_types/feature";

export const defaultFeature6Content: FeatureListContent = {
  description:
    "If a job does not appear here, we do not do it — and we will tell you who does.",
  eyebrow: "The list",
  items: [
    {
      body: "We work for one buyer at a time in each district, which is why the list of homes we show you is short. You get the off-market stock, the honest opinion on a street, and one advisor who answers the phone.",
      id: "buying",
      points: [
        "Access to roughly a third of stock that is never advertised",
        "Comparables from sales we handled, not portal estimates",
        "We attend the survey and read it with you",
        "Offer strategy and negotiation on your behalf",
      ],
      title: "Buyer advisory",
    },
    {
      body: "Commission is 1.4% plus VAT on completion, and it includes everything: staging, photography, floor plans, the film, and the whole campaign. There are no marketing add-ons and no upfront fee.",
      id: "selling",
      points: [
        "In-house staging, photography, and film",
        "Guide price backed by three comparable sales",
        "Private list release before any public advertising",
        "Weekly written feedback, not a portal dashboard",
      ],
      title: "Seller representation",
    },
    {
      body: "A forty-minute visit and a written range within two working days, free and without obligation. Bram writes every formal valuation that leaves this office and has not missed a guide by more than four percent since 2019.",
      id: "valuation",
      points: [
        "Free, no obligation, no follow-up campaign",
        "Written range with the three comparables behind it",
        "Formal valuations for probate, tax, and lending",
        "Turnaround in two working days",
      ],
      title: "Free valuation",
    },
    {
      body: "We manage 260 tenancies in-house. Referencing, deposits, inspections, and maintenance all go through one named contact, and the monthly report is written by a person who has seen the property.",
      id: "management",
      points: [
        "1.4% void across the managed portfolio last year",
        "One named contact, not a call centre",
        "Vetted trades, fixed the week they are reported",
        "Monthly written reporting",
      ],
      title: "Property management",
    },
    {
      body: "For buyers arriving from abroad: sequencing, schools, banking, and the twenty small questions nobody thinks to ask until week three. Elin runs this desk and has placed families from eleven countries.",
      id: "relocation",
      points: [
        "Sequenced plan from bank account to completion",
        "School places researched and applied for in parallel",
        "Video viewings walked room by room, at your hours",
        "We meet the surveyor and the solicitor for you",
      ],
      title: "Relocation",
    },
  ],
  title: "Everything the practice does",
};
```

### `registry/default/sections/feature-6/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/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;
};
```

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

```ts
import type { SectionIntro, Stat } from "./common";

/** Numbered rows, each with a short pitch and a checklist. */
export type FeatureListContent = SectionIntro & {
  items: {
    body: string;
    /** Used as the anchor id, so links can land on a row. */
    id: string;
    points: string[];
    title: string;
  }[];
};

/** A header (and optional paragraph) beside a checklist. */
export type FeatureChecklistContent = SectionIntro & {
  body?: string;
  points: string[];
  pointsLabel?: string;
  stats?: Stat[];
};
```
