Template · React · HTML
Shop landing page
Eine Startseite für einen Wohnaccessoire-Shop: Hero, Produkte, Kollektionen, Geschichte, Bewertungen, FAQ und Newsletter.
Vorschau
React
Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/shop-landing.jsonnpx shadcn@latest add @manniche/shop-landingshop-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
Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.
Die Farbe in deiner eigenen App
Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel 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);
}Installieren
npx shadcn@latest add https://mikkelmanniche.dk/lab/r/shop-landing.json