Mikkel Manniche
Dansk English Deutsch

Template · React · HTML

Shop landing page

A landing page for a homeware shop: hero, products, collections, story, reviews, FAQ and newsletter.

Preview

React

Install the template with shadcn. It lands in your project as ordinary components that you edit yourself.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/shop-landing.json
npx shadcn@latest add @manniche/shop-landing
shop-landing.tsx
// A full landing page for a small shop. Every interaction is plain HTML and CSS (details, :hover, :focus-within),
// so the same markup works as React and as a static page. Colours come from the theme tokens.
import { ArrowRight, Leaf, Menu, RotateCcw, Search, ShieldCheck, ShoppingBag, Star, Truck, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { ProductArt, type ProductArtKind } from './product-art'

const nav = ['Shop', 'Collections', 'Journal', 'About']

const products: { name: string; price: string; kind: ProductArtKind; colours: string[]; badge?: string }[] = [
  { name: 'Morning mug', price: '€24', kind: 'mug', colours: ['bg-primary', 'bg-foreground/70', 'bg-muted-foreground/40'], badge: 'New' },
  { name: 'Tall vase', price: '€58', kind: 'vase', colours: ['bg-primary', 'bg-foreground/70'] },
  { name: 'Serving bowl', price: '€42', kind: 'bowl', colours: ['bg-primary', 'bg-muted-foreground/40'] },
  { name: 'Water carafe', price: '€36', kind: 'carafe', colours: ['bg-primary/40'], badge: 'Back in stock' },
]

const promises = [
  { icon: Truck, title: 'Free delivery', text: 'On orders over €60' },
  { icon: RotateCcw, title: '30-day returns', text: 'Free and no questions' },
  { icon: Leaf, title: 'Small batches', text: 'From 12 workshops' },
  { icon: ShieldCheck, title: '2-year guarantee', text: 'On every piece' },
]

const reviews = [
  { quote: 'The mugs keep coffee warm for ages and they look even better in person.', name: 'Ana R.', item: 'Morning mug, set of 4' },
  { quote: 'Arrived in two days, packed in paper and straw. Not a single chip.', name: 'Jonas P.', item: 'Serving bowl' },
  { quote: 'I bought one plate to try. I now own twelve. Send help.', name: 'Mei L.', item: 'Dinner plates' },
]

const faq = [
  ['Are the pieces dishwasher safe?', 'Yes. Everything is glazed and fired at 1,240 °C, so it goes in the dishwasher and the microwave.'],
  ['How long does delivery take?', 'We pack within 48 hours. Most orders arrive two to four working days later.'],
  ['Can I return something?', 'Within 30 days, for free. Print the label from your order page and drop it at any parcel shop.'],
  ['Why do the colours vary a little?', 'Every piece is glazed by hand, so no two are quite the same. That is the point.'],
]

function Logo() {
  return (
    <a href="#top" className="flex items-center gap-2 font-semibold tracking-tight">
      <svg viewBox="0 0 24 24" className="size-7" aria-hidden>
        <circle cx="12" cy="12" r="11" className="fill-primary" />
        <path d="M12 5c4 3 4 11 0 14-4-3-4-11 0-14Z" className="fill-primary-foreground" />
      </svg>
      Linden
    </a>
  )
}

function Stars({ className }: { className?: string }) {
  return (
    <span className={cn('flex text-primary', className)} aria-hidden>
      {Array.from({ length: 5 }, (_, i) => (
        <Star key={i} className="size-4 fill-current" />
      ))}
    </span>
  )
}

/** Landing page for a homeware shop: hero, promises, products, collections, story, reviews, FAQ and newsletter. */
export function ShopLanding() {
  return (
    <div id="top" className="min-h-dvh bg-background font-sans text-foreground antialiased">
      <p className="bg-foreground px-4 py-2 text-center text-xs text-background sm:text-sm">Free delivery over €60 · Returns within 30 days</p>

      <header className="sticky top-0 z-20 border-b bg-background/85 backdrop-blur">
        <div className="mx-auto flex h-16 max-w-6xl items-center gap-6 px-4 sm:px-6">
          <Logo />
          <nav aria-label="Main" className="hidden md:block">
            <ul className="flex gap-6 text-sm text-muted-foreground">
              {nav.map((n) => (
                <li key={n}>
                  <a href={`#${n.toLowerCase()}`} className="transition-colors duration-150 hover:text-foreground">
                    {n}
                  </a>
                </li>
              ))}
            </ul>
          </nav>
          <div className="ml-auto flex items-center gap-1">
            <a href="#search" aria-label="Search" className="grid size-11 place-items-center rounded-full transition-colors hover:bg-muted">
              <Search className="size-5" aria-hidden />
            </a>
            <a href="#bag" aria-label="Bag, 2 items" className="relative grid size-11 place-items-center rounded-full transition-colors hover:bg-muted">
              <ShoppingBag className="size-5" aria-hidden />
              <span className="absolute top-1.5 right-1.5 grid size-4 place-items-center rounded-full bg-primary text-[10px] font-semibold text-primary-foreground">
                2
              </span>
            </a>
            <details className="group relative md:hidden">
              <summary className="grid size-11 cursor-pointer list-none place-items-center rounded-full hover:bg-muted [&::-webkit-details-marker]:hidden">
                <Menu className="size-5 group-open:hidden" aria-hidden />
                <X className="hidden size-5 group-open:block" aria-hidden />
                <span className="sr-only">Menu</span>
              </summary>
              <nav aria-label="Mobile" className="absolute top-12 right-0 w-56 rounded-2xl border bg-card p-2 shadow-lg">
                {nav.map((n) => (
                  <a key={n} href={`#${n.toLowerCase()}`} className="block rounded-xl px-3 py-3 text-sm hover:bg-muted">
                    {n}
                  </a>
                ))}
              </nav>
            </details>
          </div>
        </div>
      </header>

      <main>
        {/* Hero */}
        <section className="mx-auto grid max-w-6xl items-center gap-12 px-4 pt-14 pb-20 sm:px-6 md:grid-cols-[1.05fr_1fr] md:pt-20">
          <div className="transition duration-700 ease-out starting:translate-y-3 starting:opacity-0 motion-reduce:transition-none">
            <a href="#shop" className="inline-flex items-center gap-2 rounded-full border bg-card px-3 py-1 text-sm shadow-sm transition-colors hover:bg-muted">
              <span className="rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground">New</span>
              The spring edit is here
              <ArrowRight className="size-3.5" aria-hidden />
            </a>
            <h1 className="mt-6 font-serif text-5xl leading-[1.05] tracking-tight text-balance sm:text-6xl">Things for slow mornings and long tables.</h1>
            <p className="mt-5 max-w-md text-lg text-pretty text-muted-foreground">
              Stoneware, glass and linen from small workshops. Made to be used every day and handed down after.
            </p>
            <div className="mt-8 flex flex-wrap gap-3">
              <a
                href="#shop"
                className="inline-flex min-h-12 items-center gap-2 rounded-full bg-primary px-6 font-medium text-primary-foreground shadow-sm transition hover:opacity-90 active:scale-[0.98]"
              >
                Shop the edit <ArrowRight className="size-4" aria-hidden />
              </a>
              <a
                href="#collections"
                className="inline-flex min-h-12 items-center rounded-full border bg-card px-6 font-medium transition-colors hover:bg-muted"
              >
                See collections
              </a>
            </div>
            <div className="mt-10 flex items-center gap-3 text-sm text-muted-foreground">
              <Stars />
              <span>
                <b className="font-semibold text-foreground">4.9</b> from 2,300 reviews
              </span>
            </div>
          </div>

          <div className="relative grid aspect-square grid-cols-6 grid-rows-6 gap-3 transition delay-150 duration-700 ease-out starting:scale-95 starting:opacity-0 motion-reduce:transition-none">
            <div className="col-span-4 row-span-4 rounded-[2rem] bg-accent p-8">
              <ProductArt kind="vase" />
            </div>
            <div className="col-span-2 row-span-3 rounded-[2rem] bg-muted p-4">
              <ProductArt kind="mug" />
            </div>
            <div className="col-span-2 row-span-3 rounded-[2rem] bg-primary/15 p-4">
              <ProductArt kind="carafe" />
            </div>
            <div className="col-span-4 row-span-2 flex items-center gap-4 rounded-[2rem] border bg-card p-4 shadow-sm">
              <div className="size-16 shrink-0 rounded-2xl bg-muted p-1">
                <ProductArt kind="bowl" />
              </div>
              <div className="min-w-0">
                <p className="text-sm text-muted-foreground">Bestseller</p>
                <p className="truncate font-medium">Serving bowl, sand</p>
              </div>
              <span className="ml-auto font-semibold">€42</span>
            </div>
          </div>
        </section>

        {/* Promises */}
        <section aria-label="Our promises" className="border-y bg-card">
          <ul className="mx-auto grid max-w-6xl grid-cols-2 gap-px px-4 sm:px-6 lg:grid-cols-4">
            {promises.map(({ icon: Icon, title, text }) => (
              <li key={title} className="flex items-center gap-3 py-6">
                <span className="grid size-10 shrink-0 place-items-center rounded-full bg-accent text-accent-foreground">
                  <Icon className="size-5" aria-hidden />
                </span>
                <span>
                  <span className="block text-sm font-medium">{title}</span>
                  <span className="block text-sm text-muted-foreground">{text}</span>
                </span>
              </li>
            ))}
          </ul>
        </section>

        {/* Products */}
        <section id="shop" className="mx-auto max-w-6xl scroll-mt-20 px-4 py-20 sm:px-6">
          <div className="flex items-end justify-between gap-4">
            <div>
              <p className="text-sm font-medium text-primary">Most loved</p>
              <h2 className="mt-1 font-serif text-3xl tracking-tight sm:text-4xl">This week’s favourites</h2>
            </div>
            <a href="#shop" className="hidden items-center gap-1 text-sm font-medium hover:underline sm:inline-flex">
              View all <ArrowRight className="size-4" aria-hidden />
            </a>
          </div>
          <ul className="mt-10 grid grid-cols-2 gap-x-4 gap-y-10 lg:grid-cols-4">
            {products.map((p) => (
              <li key={p.name} className="group relative">
                <div className="relative aspect-[4/5] overflow-hidden rounded-3xl bg-muted p-6 transition duration-300 group-hover:-translate-y-1 group-hover:shadow-lg">
                  <div className="h-full transition duration-500 group-hover:scale-105">
                    <ProductArt kind={p.kind} />
                  </div>
                  {p.badge && <span className="absolute top-3 left-3 rounded-full bg-card px-2.5 py-1 text-xs font-medium shadow-sm">{p.badge}</span>}
                  <span className="absolute inset-x-3 bottom-3 translate-y-2 rounded-full bg-foreground py-2.5 text-center text-sm font-medium text-background opacity-0 transition duration-200 group-focus-within:translate-y-0 group-focus-within:opacity-100 group-hover:translate-y-0 group-hover:opacity-100">
                    Add to bag
                  </span>
                </div>
                <div className="mt-4 flex items-start justify-between gap-2">
                  <h3 className="font-medium">
                    <a href="#shop" className="after:absolute after:inset-0">
                      {p.name}
                    </a>
                  </h3>
                  <span className="font-medium">{p.price}</span>
                </div>
                <p className="mt-2 flex gap-1.5" aria-label={`${p.colours.length} colours`}>
                  {p.colours.map((c) => (
                    <span key={c} className={cn('size-3.5 rounded-full ring-1 ring-border', c)} />
                  ))}
                </p>
              </li>
            ))}
          </ul>
        </section>

        {/* Collections */}
        <section id="collections" className="mx-auto max-w-6xl scroll-mt-20 px-4 pb-20 sm:px-6">
          <h2 className="font-serif text-3xl tracking-tight sm:text-4xl">Shop by collection</h2>
          <div className="mt-10 grid gap-4 md:grid-cols-3 md:grid-rows-2">
            {[
              { title: 'Tableware', count: '48 pieces', kind: 'plate' as const, cls: 'md:col-span-2 md:row-span-2 bg-accent', art: 'md:h-80' },
              { title: 'Glass', count: '16 pieces', kind: 'carafe' as const, cls: 'bg-muted', art: '' },
              { title: 'Linen', count: '22 pieces', kind: 'linen' as const, cls: 'bg-primary/15', art: '' },
            ].map((c) => (
              <a
                key={c.title}
                href="#collections"
                className={cn('group flex flex-col justify-between overflow-hidden rounded-[2rem] p-6 transition hover:shadow-lg', c.cls)}
              >
                <div className={cn('mx-auto h-40 w-full max-w-xs transition duration-500 group-hover:scale-105', c.art)}>
                  <ProductArt kind={c.kind} />
                </div>
                <div className="mt-6 flex items-end justify-between">
                  <div>
                    <h3 className="text-xl font-semibold">{c.title}</h3>
                    <p className="text-sm text-muted-foreground">{c.count}</p>
                  </div>
                  <span className="grid size-11 place-items-center rounded-full bg-card shadow-sm transition group-hover:translate-x-1">
                    <ArrowRight className="size-4" aria-hidden />
                  </span>
                </div>
              </a>
            ))}
          </div>
        </section>

        {/* Story */}
        <section id="about" className="scroll-mt-20 bg-foreground text-background">
          <div className="mx-auto grid max-w-6xl gap-12 px-4 py-20 sm:px-6 md:grid-cols-2">
            <div>
              <p className="text-sm font-medium opacity-70">Our story</p>
              <h2 className="mt-2 font-serif text-3xl tracking-tight text-balance sm:text-4xl">Made slowly, by people who sign their work.</h2>
              <p className="mt-5 max-w-md opacity-75">
                We started with one potter and a kiln in a garage. Today we work with twelve workshops, and every piece still carries the maker’s mark on the
                base.
              </p>
              <a
                href="#journal"
                className="mt-8 inline-flex min-h-12 items-center gap-2 rounded-full bg-background px-6 font-medium text-foreground transition hover:opacity-90"
              >
                Meet the makers <ArrowRight className="size-4" aria-hidden />
              </a>
            </div>
            <dl className="grid grid-cols-2 gap-px self-end overflow-hidden rounded-3xl bg-background/15">
              {[
                ['12', 'workshops'],
                ['48 h', 'to dispatch'],
                ['31,000', 'orders sent'],
                ['4.9 / 5', 'average rating'],
              ].map(([v, l]) => (
                <div key={l} className="bg-foreground p-6">
                  <dt className="text-sm opacity-70">{l}</dt>
                  <dd className="mt-1 font-serif text-3xl">{v}</dd>
                </div>
              ))}
            </dl>
          </div>
        </section>

        {/* Reviews */}
        <section id="journal" className="mx-auto max-w-6xl scroll-mt-20 px-4 py-20 sm:px-6">
          <h2 className="font-serif text-3xl tracking-tight sm:text-4xl">Kind words</h2>
          <ul className="mt-10 grid gap-4 md:grid-cols-3">
            {reviews.map((r) => (
              <li key={r.name}>
                <figure className="flex h-full flex-col rounded-3xl border bg-card p-6 shadow-sm">
                  <Stars />
                  <blockquote className="mt-4 flex-1 text-lg text-pretty">“{r.quote}”</blockquote>
                  <figcaption className="mt-6 flex items-center gap-3 text-sm">
                    <span className="grid size-10 place-items-center rounded-full bg-accent font-semibold text-accent-foreground">{r.name[0]}</span>
                    <span>
                      <span className="block font-medium">{r.name}</span>
                      <span className="block text-muted-foreground">{r.item}</span>
                    </span>
                  </figcaption>
                </figure>
              </li>
            ))}
          </ul>
        </section>

        {/* FAQ */}
        <section className="mx-auto grid max-w-6xl gap-10 px-4 pb-20 sm:px-6 md:grid-cols-[1fr_1.6fr]">
          <div>
            <h2 className="font-serif text-3xl tracking-tight sm:text-4xl">Questions</h2>
            <p className="mt-3 text-muted-foreground">
              Something else? Write to{' '}
              <a href="mailto:hello@example.com" className="font-medium text-foreground underline underline-offset-4">
                hello@example.com
              </a>
              .
            </p>
          </div>
          <div className="divide-y rounded-3xl border bg-card px-6">
            {faq.map(([q, a]) => (
              <details key={q} className="group py-2">
                <summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-4 font-medium [&::-webkit-details-marker]:hidden">
                  {q}
                  <span
                    className="grid size-8 shrink-0 place-items-center rounded-full bg-muted transition-transform duration-200 group-open:rotate-45"
                    aria-hidden
                  >
                    +
                  </span>
                </summary>
                <p className="pb-4 text-muted-foreground">{a}</p>
              </details>
            ))}
          </div>
        </section>

        {/* Newsletter */}
        <section className="mx-auto max-w-6xl px-4 pb-20 sm:px-6">
          <div className="relative overflow-hidden rounded-[2rem] bg-primary px-6 py-14 text-center text-primary-foreground sm:px-12">
            <div className="pointer-events-none absolute -top-24 -right-24 size-72 rounded-full bg-primary-foreground/10" aria-hidden />
            <div className="pointer-events-none absolute -bottom-32 -left-16 size-80 rounded-full bg-primary-foreground/10" aria-hidden />
            <h2 className="relative font-serif text-3xl tracking-tight sm:text-4xl">10% off your first order</h2>
            <p className="relative mx-auto mt-3 max-w-md opacity-85">One letter a month with new pieces and the stories behind them. No spam.</p>
            <form className="relative mx-auto mt-8 flex max-w-md flex-col gap-2 sm:flex-row" action="#">
              <label htmlFor="nl-email" className="sr-only">
                Email
              </label>
              <input
                id="nl-email"
                type="email"
                required
                placeholder="you@example.com"
                className="min-h-12 flex-1 rounded-full border-0 bg-primary-foreground px-5 text-foreground placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-foreground"
              />
              <button
                type="submit"
                className="min-h-12 rounded-full bg-foreground px-6 font-medium text-background transition hover:opacity-90 active:scale-[0.98]"
              >
                Subscribe
              </button>
            </form>
          </div>
        </section>
      </main>

      <footer className="border-t bg-card">
        <div className="mx-auto grid max-w-6xl gap-10 px-4 py-14 sm:px-6 md:grid-cols-[1.5fr_repeat(3,1fr)]">
          <div>
            <Logo />
            <p className="mt-3 max-w-xs text-sm text-muted-foreground">Everyday things from small workshops, since 2016.</p>
          </div>
          {[
            ['Shop', ['Tableware', 'Glass', 'Linen', 'Gift cards']],
            ['Help', ['Delivery', 'Returns', 'Care guide', 'Contact']],
            ['Company', ['About', 'Makers', 'Journal', 'Wholesale']],
          ].map(([title, links]) => (
            <div key={title as string}>
              <h3 className="text-sm font-semibold">{title}</h3>
              <ul className="mt-3 space-y-2 text-sm text-muted-foreground">
                {(links as string[]).map((l) => (
                  <li key={l}>
                    <a href="#top" className="transition-colors hover:text-foreground">
                      {l}
                    </a>
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </div>
        <p className="mx-auto max-w-6xl border-t px-4 py-6 text-xs text-muted-foreground sm:px-6">© 2026 Linden. A template from Manniche UI.</p>
      </footer>
    </div>
  )
}
product-art.tsx
import { cn } from '@/lib/utils'

export type ProductArtKind = 'mug' | 'vase' | 'bowl' | 'carafe' | 'plate' | 'linen'

/** Flat drawings of tableware, coloured from the theme, so the template needs no photos. Swap them for your own images. */
export function ProductArt({ kind, className }: { kind: ProductArtKind; className?: string }) {
  return (
    <svg viewBox="0 0 200 200" className={cn('h-full w-full', className)} aria-hidden>
      <ellipse cx="100" cy="176" rx="62" ry="7" className="fill-foreground/10" />
      {kind === 'mug' && (
        <g>
          <path d="M128 92h10a20 20 0 0 1 0 40h-10" fill="none" strokeWidth="10" className="stroke-primary/70" />
          <path d="M58 70h76v78a26 26 0 0 1-26 26H84a26 26 0 0 1-26-26Z" className="fill-primary" />
          <path d="M58 70h76v12H58Z" className="fill-primary-foreground/25" />
          <path d="M84 44c0 8 8 8 8 16M102 40c0 8 8 8 8 16" fill="none" strokeWidth="4" strokeLinecap="round" className="stroke-foreground/25" />
        </g>
      )}
      {kind === 'vase' && (
        <g>
          <path d="M86 30h28v16c0 10 30 26 30 66 0 36-20 62-44 62s-44-26-44-62c0-40 30-56 30-66Z" className="fill-primary" />
          <path d="M66 110c10 8 58 8 68 0" fill="none" strokeWidth="5" className="stroke-primary-foreground/30" />
          <path d="M100 30c-6-14 4-22 10-26M100 30c8-10 22-8 28-2" fill="none" strokeWidth="3" strokeLinecap="round" className="stroke-success" />
        </g>
      )}
      {kind === 'bowl' && (
        <g>
          <path d="M34 100h132c0 40-30 68-66 68s-66-28-66-68Z" className="fill-primary" />
          <ellipse cx="100" cy="100" rx="66" ry="12" className="fill-primary-foreground/30" />
          <circle cx="82" cy="94" r="10" className="fill-foreground/20" />
          <circle cx="104" cy="90" r="12" className="fill-foreground/15" />
          <circle cx="124" cy="96" r="9" className="fill-foreground/25" />
        </g>
      )}
      {kind === 'carafe' && (
        <g>
          <path d="M88 24h24v34c0 8 34 24 34 70 0 28-20 46-46 46s-46-18-46-46c0-46 34-62 34-70Z" className="fill-primary/25 stroke-primary" strokeWidth="4" />
          <path d="M60 116c26 10 54 10 80 0v12c0 26-18 40-40 40s-40-14-40-40Z" className="fill-primary/60" />
          <rect x="84" y="18" width="32" height="10" rx="4" className="fill-primary" />
        </g>
      )}
      {kind === 'plate' && (
        <g>
          <ellipse cx="100" cy="128" rx="78" ry="34" className="fill-primary" />
          <ellipse cx="100" cy="124" rx="56" ry="22" className="fill-primary-foreground/25" />
          <ellipse cx="100" cy="96" rx="58" ry="24" className="fill-primary/60" />
          <ellipse cx="100" cy="93" rx="40" ry="15" className="fill-primary-foreground/30" />
        </g>
      )}
      {kind === 'linen' && (
        <g>
          <path d="M40 60h120l-8 106H48Z" className="fill-primary/80" />
          <path d="M40 60h120v18H40Z" className="fill-primary" />
          {[60, 80, 100, 120, 140].map((x) => (
            <path key={x} d={`M${x} 80v84`} strokeWidth="2" className="stroke-primary-foreground/20" />
          ))}
          <path d="M40 104h118M44 132h112" strokeWidth="2" className="stroke-primary-foreground/20" />
        </g>
      )}
    </svg>
  )
}
shop-landing-demo.tsx
import { ShopLanding } from '@/components/shop-landing'

export default function ShopLandingDemo() {
  return <ShopLanding />
}

HTML

The whole page as one file with all CSS built in and no JavaScript. Toggles, tabs and filters work with HTML and CSS alone.

Open in a new tab

The colour in your own app

If you already have shadcn set up, these variables give the same colour. Light in :root, dark in .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Install

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/shop-landing.json

Dependencies

Files

  • shop-landing.tsx
  • product-art.tsx
  • shop-landing.html

Manniche Lab · open source

Free tools for AI agents and web development.