Skip to content
Commerce block

Menu Course List

A restaurant menu typeset as a bill of fare rather than a product grid — dotted leaders to the price, dietary marks as abbreviations with a key, and prices as strings so “market price” still fits.

282 linesNo dependenciesAdded 14 Sept 2026
  • menu
  • restaurant
  • food
  • pricing
  • hospitality

What's included

  • components/menu-course-list.tsx
  • No runtime dependencies

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add menu-course-list

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Dinner

Served Wednesday to Sunday, 6pm until 10pm. One kitchen, one menu, changed when the produce changes rather than on a schedule.

To begin

Served until 3pm

  • Devilled eggs, brown crab, lovage(gf) (s)

    9

    Six halves. The crab is picked here on the morning it arrives.

  • Bread and cultured butter(v)

    5

    Sourdough from the Tuesday bake, salted butter churned in house.

  • Chicory, pear, hazelnut, aged ewe’s cheese(v) (gf) (n)

    11

    Bitter, sweet, salt. The pears are from a single orchard in Herefordshire.

  • Potted shrimp on toast(s)

    12

    Morecambe Bay brown shrimp, mace and cayenne, clarified butter.

Larger

  • Hogget shoulder for two, anchovy and rosemary(gf)

    58

    Four hours in the wood oven, carved at the table. Comes with greens and dripping potatoes.

  • Whole plaice, brown butter, capers, sea beet(gf)

    28

    On the bone. Ask and we will take it off for you.

  • Barley, wild mushroom, pickled walnut, curd(v) (n)

    21

    The mushrooms change weekly; ask what came in.

  • Dry-aged rib chop, bone marrow butter(gf)

    market price

    Thirty-five days. Priced by weight on the day.

To finish

Or a cheese plate, £4 a cut, £14 for four

  • Burnt honey and thyme tart(v)

    10

    Crème fraîche. The honey is from the roof of the building next door.

  • Quince, brown sugar, oat cream(ve) (df)

    9

    Poached slowly until it goes the colour of a penny.

  • Salted caramel and malt ice cream(v) (gf)

    5 / 8 / 10

    One scoop, two scoops, or three if the table is sharing.

(gf)
Gluten free
(s)
Contains shellfish
(v)
Vegetarian
(n)
Contains nuts
(ve)
Vegan
(df)
Dairy free

Please tell us about allergies before you order — we cook in one kitchen and cannot promise any dish is free of a trace. A discretionary 12.5% service charge is added to tables of six or more and goes to the people who served you.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/menu-course-list.tsx
/**
 * <MenuCourseList> — a menu typeset as a bill of fare, not as a product grid.
 *
 * The default instinct is to render a restaurant menu as cards: a photo, a
 * title, a price, a button. That is a storefront, and it is wrong for this
 * content in a way that costs the restaurant money. A menu is read in one
 * pass, at a table, by someone deciding between eleven things — so it wants
 * the shape print settled on a century ago: courses as headings, one line
 * per dish, the price on the end edge, and a leader joining the two so the
 * eye can cross a wide column without losing its row.
 *
 * THE LEADER IS THE WHOLE TRICK, and it is a border rather than a row of
 * literal dots. A string of "..." is read out character by character by a
 * screen reader and re-wraps unpredictably when a dish name is long; a
 * dotted bottom border on a flex spacer is decorative by construction,
 * announces nothing, and grows to whatever width is left over.
 *
 * DIETARY MARKS ARE ABBREVIATIONS WITH A KEY, not emoji or colour. A green
 * dot meaning vegetarian fails for the reader who cannot see green and says
 * nothing to the one who can; "(v)" with a key at the foot of the menu is
 * how printed menus have always solved it, and it survives being copied
 * into a text message. Each mark carries a `<abbr title>` so the long form
 * is one hover or one focus away.
 *
 * Prices are plain strings, not numbers. A menu quotes "14" or "14.50" or
 * "market price" or "9 / 16" for two pour sizes, and a numeric type plus a
 * currency formatter cannot express three of those four.
 */

import * as React from 'react'

/** Long forms for the marks, so the key and the abbr cannot disagree. */
const MARK_MEANINGS: Record<string, string> = {
  v: 'Vegetarian',
  ve: 'Vegan',
  gf: 'Gluten free',
  df: 'Dairy free',
  n: 'Contains nuts',
  s: 'Contains shellfish',
}

export interface MenuDish {
  name: string
  /** One line. Ingredients in the order they are tasted, not alphabetically. */
  description?: string
  /** A string, not a number — "14", "9 / 16" and "market price" are all valid. */
  price: string
  /** Keys of `MARK_MEANINGS`, e.g. `['v', 'gf']`. */
  marks?: string[]
  /** Renders the dish in the house's emphasis style. Use sparingly. */
  signature?: boolean
}

export interface MenuCourse {
  name: string
  /** A note under the course heading — a serving rule, a time limit, a price. */
  note?: string
  dishes: MenuDish[]
}

export interface MenuCourseListProps {
  heading?: string
  /** The line under the heading: service times, a set-price note, a source. */
  standfirst?: string
  courses?: MenuCourse[]
  /** Shown at the foot, above the key. Allergen and service-charge wording. */
  footnote?: string
  className?: string
}

const DEFAULT_COURSES: MenuCourse[] = [
  {
    name: 'To begin',
    note: 'Served until 3pm',
    dishes: [
      {
        name: 'Devilled eggs, brown crab, lovage',
        description: 'Six halves. The crab is picked here on the morning it arrives.',
        price: '9',
        marks: ['gf', 's'],
      },
      {
        name: 'Bread and cultured butter',
        description: 'Sourdough from the Tuesday bake, salted butter churned in house.',
        price: '5',
        marks: ['v'],
      },
      {
        name: 'Chicory, pear, hazelnut, aged ewe’s cheese',
        description: 'Bitter, sweet, salt. The pears are from a single orchard in Herefordshire.',
        price: '11',
        marks: ['v', 'gf', 'n'],
      },
      {
        name: 'Potted shrimp on toast',
        description: 'Morecambe Bay brown shrimp, mace and cayenne, clarified butter.',
        price: '12',
        marks: ['s'],
      },
    ],
  },
  {
    name: 'Larger',
    dishes: [
      {
        name: 'Hogget shoulder for two, anchovy and rosemary',
        description:
          'Four hours in the wood oven, carved at the table. Comes with greens and dripping potatoes.',
        price: '58',
        marks: ['gf'],
        signature: true,
      },
      {
        name: 'Whole plaice, brown butter, capers, sea beet',
        description: 'On the bone. Ask and we will take it off for you.',
        price: '28',
        marks: ['gf'],
      },
      {
        name: 'Barley, wild mushroom, pickled walnut, curd',
        description: 'The mushrooms change weekly; ask what came in.',
        price: '21',
        marks: ['v', 'n'],
      },
      {
        name: 'Dry-aged rib chop, bone marrow butter',
        description: 'Thirty-five days. Priced by weight on the day.',
        price: 'market price',
        marks: ['gf'],
      },
    ],
  },
  {
    name: 'To finish',
    note: 'Or a cheese plate, £4 a cut, £14 for four',
    dishes: [
      {
        name: 'Burnt honey and thyme tart',
        description: 'Crème fraîche. The honey is from the roof of the building next door.',
        price: '10',
        marks: ['v'],
      },
      {
        name: 'Quince, brown sugar, oat cream',
        description: 'Poached slowly until it goes the colour of a penny.',
        price: '9',
        marks: ['ve', 'df'],
      },
      {
        name: 'Salted caramel and malt ice cream',
        description: 'One scoop, two scoops, or three if the table is sharing.',
        price: '5 / 8 / 10',
        marks: ['v', 'gf'],
      },
    ],
  },
]

/** One dish: name and marks on the start edge, price on the end, leader between. */
function Dish({ dish }: { dish: MenuDish }) {
  return (
    <li className="pt-4 first:pt-0">
      {/* `items-baseline` and not `items-center`: the leader must sit on the
          same baseline as the two things it joins, or it reads as a rule
          under the name rather than as a line across to the price. */}
      <div className="flex items-baseline gap-2">
        <h4
          data-stress-ignore
          className={`min-w-0 text-[0.95rem] leading-snug ${
            dish.signature ? 'font-semibold text-foreground' : 'font-medium text-card-foreground'
          }`}
        >
          {dish.name}
          {dish.marks?.length ? (
            <span className="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground">
              {dish.marks.map((mark, i) => (
                <React.Fragment key={mark}>
                  {i > 0 ? ' ' : null}
                  <abbr
                    title={MARK_MEANINGS[mark] ?? mark}
                    className="no-underline decoration-dotted underline-offset-2 hover:underline"
                  >
                    ({mark})
                  </abbr>
                </React.Fragment>
              ))}
            </span>
          ) : null}
        </h4>

        {/* The leader. Decorative by construction — a border, not text — so
            it is announced by nothing and wraps to whatever is left. */}
        <span
          aria-hidden
          className="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"
        />

        <span className="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">
          {dish.price}
        </span>
      </div>

      {dish.description ? (
        <p className="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">
          {dish.description}
        </p>
      ) : null}
    </li>
  )
}

export function MenuCourseList({
  heading = 'Dinner',
  standfirst = 'Served Wednesday to Sunday, 6pm until 10pm. One kitchen, one menu, changed when the produce changes rather than on a schedule.',
  courses = DEFAULT_COURSES,
  footnote = 'Please tell us about allergies before you order — we cook in one kitchen and cannot promise any dish is free of a trace. A discretionary 12.5% service charge is added to tables of six or more and goes to the people who served you.',
  className = '',
}: MenuCourseListProps) {
  /* Only the marks actually used, so the key never lists an abbreviation
     that appears nowhere on the menu above it. */
  const usedMarks = [...new Set(courses.flatMap((c) => c.dishes.flatMap((d) => d.marks ?? [])))]

  return (
    <section
      aria-label={heading}
      className={`mx-auto w-full max-w-3xl px-6 py-16 sm:py-20 ${className}`}
    >
      <header className="text-center">
        <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">{heading}</h2>
        {standfirst ? (
          <p className="mx-auto mt-3 max-w-xl text-pretty text-sm leading-relaxed text-muted-foreground">
            {standfirst}
          </p>
        ) : null}
      </header>

      <div className="mt-12 space-y-12">
        {courses.map((course) => (
          <div key={course.name}>
            <div className="flex items-baseline justify-between gap-4 border-b border-border pb-2">
              {course.name ? (
                <h3
                  data-stress-ignore
                  className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground"
                >
                  {course.name}
                </h3>
              ) : null}
              {course.note ? (
                <p className="text-xs text-muted-foreground/80">{course.note}</p>
              ) : null}
            </div>

            <ul className="mt-5 divide-y divide-border/50">
              {course.dishes.map((dish) => (
                <Dish key={dish.name} dish={dish} />
              ))}
            </ul>
          </div>
        ))}
      </div>

      {usedMarks.length ? (
        <dl className="mt-12 flex flex-wrap justify-center gap-x-5 gap-y-2 border-t border-border pt-6 text-xs text-muted-foreground">
          {usedMarks.map((mark) => (
            <div key={mark} className="flex items-center gap-1.5">
              <dt className="font-semibold">({mark})</dt>
              <dd>{MARK_MEANINGS[mark] ?? mark}</dd>
            </div>
          ))}
        </dl>
      ) : null}

      {footnote ? (
        <p className="mx-auto mt-6 max-w-xl text-center text-xs leading-relaxed text-muted-foreground/80">
          {footnote}
        </p>
      ) : null}
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Nothing to install. No component library, no icon package.
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/menu-course-list.tsx and import it where you need the section:

import { MenuCourseList } from '@/components/menu-course-list'

Customize

1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
headingstring'Dinner'
standfirstThe line under the heading: service times, a set-price note, a source.string—
coursesMenuCourse[]DEFAULT_COURSES
footnoteShown at the foot, above the key. Allergen and service-charge wording.string—
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

menu-course-list.html
<!--
  Menu Course List — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<section aria-label="Dinner" class="mx-auto w-full max-w-3xl px-6 py-16 sm:py-20 ">
  <header class="text-center">
    <h2 class="text-3xl font-semibold tracking-tight sm:text-4xl">Dinner</h2>
    <p class="mx-auto mt-3 max-w-xl text-pretty text-sm leading-relaxed text-muted-foreground">Served Wednesday to Sunday, 6pm until 10pm. One kitchen, one menu, changed when the produce changes rather than on a schedule.</p>
  </header>
  <div class="mt-12 space-y-12">
    <div>
      <div class="flex items-baseline justify-between gap-4 border-b border-border pb-2">
        <h3 data-stress-ignore="true" class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">To begin</h3>
        <p class="text-xs text-muted-foreground/80">Served until 3pm</p>
      </div>
      <ul class="mt-5 divide-y divide-border/50">
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Devilled eggs, brown crab, lovage<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Gluten free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(gf)</abbr> <abbr title="Contains shellfish" class="no-underline decoration-dotted underline-offset-2 hover:underline">(s)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">9</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Six halves. The crab is picked here on the morning it arrives.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Bread and cultured butter<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Vegetarian" class="no-underline decoration-dotted underline-offset-2 hover:underline">(v)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">5</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Sourdough from the Tuesday bake, salted butter churned in house.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Chicory, pear, hazelnut, aged ewe’s cheese<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Vegetarian" class="no-underline decoration-dotted underline-offset-2 hover:underline">(v)</abbr> <abbr title="Gluten free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(gf)</abbr> <abbr title="Contains nuts" class="no-underline decoration-dotted underline-offset-2 hover:underline">(n)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">11</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Bitter, sweet, salt. The pears are from a single orchard in Herefordshire.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Potted shrimp on toast<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Contains shellfish" class="no-underline decoration-dotted underline-offset-2 hover:underline">(s)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">12</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Morecambe Bay brown shrimp, mace and cayenne, clarified butter.</p>
        </li>
      </ul>
    </div>
    <div>
      <div class="flex items-baseline justify-between gap-4 border-b border-border pb-2">
        <h3 data-stress-ignore="true" class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">Larger</h3>
      </div>
      <ul class="mt-5 divide-y divide-border/50">
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-semibold text-foreground">Hogget shoulder for two, anchovy and rosemary<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Gluten free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(gf)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">58</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Four hours in the wood oven, carved at the table. Comes with greens and dripping potatoes.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Whole plaice, brown butter, capers, sea beet<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Gluten free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(gf)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">28</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">On the bone. Ask and we will take it off for you.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Barley, wild mushroom, pickled walnut, curd<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Vegetarian" class="no-underline decoration-dotted underline-offset-2 hover:underline">(v)</abbr> <abbr title="Contains nuts" class="no-underline decoration-dotted underline-offset-2 hover:underline">(n)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">21</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">The mushrooms change weekly; ask what came in.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Dry-aged rib chop, bone marrow butter<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Gluten free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(gf)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">market price</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Thirty-five days. Priced by weight on the day.</p>
        </li>
      </ul>
    </div>
    <div>
      <div class="flex items-baseline justify-between gap-4 border-b border-border pb-2">
        <h3 data-stress-ignore="true" class="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">To finish</h3>
        <p class="text-xs text-muted-foreground/80">Or a cheese plate, £4 a cut, £14 for four</p>
      </div>
      <ul class="mt-5 divide-y divide-border/50">
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Burnt honey and thyme tart<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Vegetarian" class="no-underline decoration-dotted underline-offset-2 hover:underline">(v)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">10</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Crème fraîche. The honey is from the roof of the building next door.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Quince, brown sugar, oat cream<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Vegan" class="no-underline decoration-dotted underline-offset-2 hover:underline">(ve)</abbr> <abbr title="Dairy free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(df)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">9</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">Poached slowly until it goes the colour of a penny.</p>
        </li>
        <li class="pt-4 first:pt-0">
          <div class="flex items-baseline gap-2">
            <h4 data-stress-ignore="true" class="min-w-0 text-[0.95rem] leading-snug font-medium text-card-foreground">Salted caramel and malt ice cream<span class="ms-2 whitespace-nowrap text-xs font-normal text-muted-foreground"><abbr title="Vegetarian" class="no-underline decoration-dotted underline-offset-2 hover:underline">(v)</abbr> <abbr title="Gluten free" class="no-underline decoration-dotted underline-offset-2 hover:underline">(gf)</abbr></span></h4>
            <span aria-hidden="true" class="min-w-4 flex-1 translate-y-[-0.3rem] border-b border-dotted border-border"></span>
            <span class="shrink-0 text-[0.95rem] font-medium tabular-nums text-card-foreground">5 / 8 / 10</span>
          </div>
          <p class="mt-1 max-w-prose text-sm leading-relaxed text-muted-foreground">One scoop, two scoops, or three if the table is sharing.</p>
        </li>
      </ul>
    </div>
  </div>
  <dl class="mt-12 flex flex-wrap justify-center gap-x-5 gap-y-2 border-t border-border pt-6 text-xs text-muted-foreground">
    <div class="flex items-center gap-1.5">
      <dt class="font-semibold">(gf)</dt>
      <dd>Gluten free</dd>
    </div>
    <div class="flex items-center gap-1.5">
      <dt class="font-semibold">(s)</dt>
      <dd>Contains shellfish</dd>
    </div>
    <div class="flex items-center gap-1.5">
      <dt class="font-semibold">(v)</dt>
      <dd>Vegetarian</dd>
    </div>
    <div class="flex items-center gap-1.5">
      <dt class="font-semibold">(n)</dt>
      <dd>Contains nuts</dd>
    </div>
    <div class="flex items-center gap-1.5">
      <dt class="font-semibold">(ve)</dt>
      <dd>Vegan</dd>
    </div>
    <div class="flex items-center gap-1.5">
      <dt class="font-semibold">(df)</dt>
      <dd>Dairy free</dd>
    </div>
  </dl>
  <p class="mx-auto mt-6 max-w-xl text-center text-xs leading-relaxed text-muted-foreground/80">Please tell us about allergies before you order — we cook in one kitchen and cannot promise any dish is free of a trace. A discretionary 12.5% service charge is added to tables of six or more and goes to the people who served you.</p>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

8 more blocks in Product Listings

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Product Listings

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Product Listings blocks

View category
Open the full page for this block

Product Card Grid

Responsive product cards with sale badges, ratings and a wishlist button layered above the card link so it stays clickable.

Product Listings142 lines1 dep
Open the full page for this block

Faceted Filter Sidebar

Category, size, colour and price facets as proper fieldsets, with colour swatches that carry their name as well as their hue.

Product Listings220 lines1 dep
Open the full page for this block

Collection Sort Toolbar

Result count, sort and density switch — with the count in an aria-live region, since it is the only feedback a filter change gives some users.

Product Listings131 lines1 dep
Open the full page for this block

Product Comparison Table

Three products side by side with the row labels pinned through a horizontal scroll, an "only show differences" toggle that says how many rows it hid, and "best" marked only where better means something.

Product Listings341 lines1 dep
Open the full page for this block

Stock Availability List

Availability per variant stated before the cart, because discovering it at checkout is where the basket gets abandoned.

Product Listings149 linesNo deps
Open the full page for this block

Collection Story Split

The editorial header above a collection grid, carrying the four facts that decide a considered purchase.

Product Listings157 linesNo deps

Map and Results Split

Results on one side, a drawn map on the other, scrolling independently. The map is explicitly decorative and out of the tab order — the list is the content, not its companion.

Product Listings329 lines1 dep