# Grid Lineup

> A product lineup comparing models side by side, each with finish swatches, a price, and buy actions.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/grid-lineup).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/grid-lineup
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/grid-lineup-light-be1c63795b9d0d95.png
- Registry item: https://suiss.app/ui/r/new-york-v4/grid-lineup.json
- Categories: blocks, grid
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/grid-lineup.json
```

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

## Dependencies

- Registry items: `@esuiss/button`, `@esuiss/toggle-group`

## Files

### `bases/radix/components/blocks/grid-lineup.tsx`

```tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

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

type Finish = { label: string; color: string }

const models: {
  name: string
  tagline: string
  price: string
  kicker?: string
  image: string
  alt: string
  finishes: [Finish, ...Finish[]]
}[] = [
  {
    name: "Phone Pro",
    tagline: "Pro at its very core.",
    price: "$1,099",
    kicker: "New",
    image: photo("1714972384975-fbd4f19b1bb3"),
    alt: "An orange phone on a warm gradient",
    finishes: [
      { label: "Cosmic Orange", color: "oklch(0.68 0.16 50)" },
      { label: "Deep Blue", color: "oklch(0.38 0.07 262)" },
      { label: "Silver", color: "oklch(0.9 0.006 260)" },
    ],
  },
  {
    name: "Phone Air",
    tagline: "The thinnest phone ever.",
    price: "$999",
    kicker: "New",
    image: photo("1664883410931-2edffc8be5e6"),
    alt: "A silver phone on a light grey background",
    finishes: [
      { label: "Sky Blue", color: "oklch(0.92 0.01 230)" },
      { label: "Light Gold", color: "oklch(0.86 0.04 80)" },
      { label: "Space Black", color: "oklch(0.3 0.01 260)" },
    ],
  },
  {
    name: "Phone",
    tagline: "Better in every way.",
    price: "$799",
    image: photo("1714578187196-29775454aa39"),
    alt: "A phone floating among blue spheres",
    finishes: [
      { label: "Lavender", color: "oklch(0.82 0.06 290)" },
      { label: "Sage", color: "oklch(0.85 0.07 160)" },
      { label: "Mist Blue", color: "oklch(0.88 0.04 240)" },
      { label: "Black", color: "oklch(0.3 0.01 260)" },
    ],
  },
  {
    name: "Phone e",
    tagline: "Speed. Battery. Value.",
    price: "$599",
    image: photo("1703676311066-34caf2cbee33"),
    alt: "A phone on a pink and purple backdrop",
    finishes: [
      { label: "Black", color: "oklch(0.3 0.01 260)" },
      { label: "White", color: "oklch(0.97 0 0)" },
    ],
  },
]

function LineupModel({ model }: { model: (typeof models)[number] }) {
  const [finish, setFinish] = React.useState(model.finishes[0].label)

  return (
    <div className="flex flex-col items-center text-center">
      <img
        src={model.image}
        alt={model.alt}
        className="aspect-[0.85] w-full rounded-2xl bg-secondary object-cover"
      />
      <ToggleGroup
        type="single"
        value={finish}
        onValueChange={(value: string) => {
          if (value) setFinish(value)
        }}
        aria-label={`${model.name} finish`}
        spacing={2}
        className="mt-5.5"
      >
        {model.finishes.map((item) => (
          <ToggleGroupItem
            key={item.label}
            value={item.label}
            aria-label={item.label}
            style={{ "--swatch": item.color } as React.CSSProperties}
            className="relative size-3 min-w-0 rounded-full bg-(--swatch) p-0 shadow-[inset_0_0_0_1px_--alpha(var(--color-foreground)/15%)] transition-shadow duration-200 after:absolute after:-inset-2 after:content-[''] hover:bg-(--swatch) data-[state=on]:bg-(--swatch) data-[state=on]:ring-[1.5px] data-[state=on]:ring-muted-foreground data-[state=on]:ring-offset-[1.5px] data-[state=on]:ring-offset-background data-[state=on]:hover:bg-(--swatch)"
          />
        ))}
      </ToggleGroup>
      <span className="mt-4.5 min-h-4 text-xs font-semibold text-[color-mix(in_oklab,var(--color-orange-500),var(--foreground)_45%)] dark:text-orange-500">
        {model.kicker}
      </span>
      <h3 className="mt-1 text-2xl font-semibold tracking-tight">
        {model.name}
      </h3>
      <p className="mt-2 min-h-[2lh] text-lg leading-snug text-balance">
        {model.tagline}
      </p>
      <p className="mt-2 text-sm text-muted-foreground">From {model.price}</p>
      <div className="mt-4.5 flex flex-wrap items-center justify-center gap-x-4.5 gap-y-3">
        <Button size="sm">Buy</Button>
        <a href="#learn" className="text-sm text-primary hover:underline">
          Learn more ›
        </a>
      </div>
    </div>
  )
}

export function GridLineup() {
  return (
    <section className="bg-background px-5 py-10 md:px-10">
      <h2 className="mb-9 text-center text-4xl font-semibold tracking-tight text-balance md:text-5xl">
        Which phone is right for you?
      </h2>
      <div className="grid grid-cols-2 gap-x-4 gap-y-10 lg:grid-cols-4 lg:gap-6">
        {models.map((model) => (
          <LineupModel key={model.name} model={model} />
        ))}
      </div>
    </section>
  )
}
```
