Product Header 6
edge_to_edgeImage/Video LeftText Align Left2 ColumnsMultiple ImagesImage LightboxButtons

Code
relume-product-header6.tsx
import React, { useState } from "react";
import clsx from "clsx";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button, type ButtonProps } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { StarFull, StarHalf, Star } from "relume-icons";
type ImageProps = {
src: string;
alt?: string;
};
type BreadcrumbProps = {
url: string;
title: string;
};
type GalleryProps = {
images: ImageProps[];
};
type RatingProps = {
review: number;
starsNumber: number;
};
type TabProps = {
value: string;
trigger: string;
description: string;
};
type SelectVariant = {
value: string;
label: string;
};
type Props = {
breadcrumbs: BreadcrumbProps[];
heading: string;
images: ImageProps[];
description: string;
price: string;
rating: RatingProps;
addToCartbutton: ButtonProps;
buyNowButtons: ButtonProps;
options: (ButtonProps & {
url: string;
})[];
quantityInputPlaceholder: string;
freeShipping: string;
defaultTabValue: string;
tabs: TabProps[];
selectVariants: SelectVariant[];
};
export type ProductHeader6Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
export const ProductHeader6 = (props: ProductHeader6Props) => {
const {
breadcrumbs,
heading,
images,
price,
description,
rating,
addToCartbutton,
buyNowButtons,
options,
quantityInputPlaceholder,
freeShipping,
tabs,
selectVariants,
} = {
...ProductHeader6Defaults,
...props,
};
const [variantInput, setVariantInput] = useState("");
const [optionInput, setOptionInput] = useState("");
const [quantityInput, setQuantityInput] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log({
variantInput,
optionInput,
quantityInput,
});
};
return (
<header>
<div className="grid grid-cols-1 lg:grid-cols-2">
<Gallery images={images} />
<div>
<div className="px-[5%] pt-8 pb-20 md:pt-12 lg:sticky lg:top-0 lg:py-20 lg:pr-[5vw] lg:pl-20">
<div className="lg:max-w-md">
<Breadcrumb className="mb-6 flex flex-wrap items-center text-small">
<BreadcrumbList>
{breadcrumbs.map((item, index) => (
<React.Fragment key={index}>
<BreadcrumbItem>
<BreadcrumbLink href={item.url}>{item.title}</BreadcrumbLink>
</BreadcrumbItem>
{index < breadcrumbs.length - 1 && <BreadcrumbSeparator />}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
<div>
<h1 className="mb-5 text-h3 font-bold md:mb-6">{heading}</h1>
<div className="mb-5 flex flex-col flex-wrap sm:flex-row sm:items-center md:mb-6">
<h5 className="text-h5 font-bold">{price}</h5>
<div className="mx-4 hidden w-px self-stretch bg-scheme-border sm:block"></div>
<div className="flex flex-wrap items-center gap-3">
<StarRating rating={rating.starsNumber} />
<p className="text-small">{`(${rating.starsNumber} stars) • ${rating.review} reviews`}</p>
</div>
</div>
<p className="mb-5 md:mb-6">{description}</p>
<form onSubmit={handleSubmit} className="mb-8">
<div className="grid grid-cols-1 gap-6">
<div className="flex flex-col">
<Label className="mb-2">Variant</Label>
<Select onValueChange={setVariantInput}>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
{selectVariants.map((item, index) => (
<SelectItem key={index} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col">
<Label className="mb-2">Variant</Label>
<div className="flex flex-wrap gap-4">
{options.map((option, index) => (
<Button
key={index}
className="px-4 py-2"
asChild
onClick={() => setOptionInput(option.title || "")}
{...option}
>
<a
href={option.url}
className={clsx({
"pointer-events-none opacity-25": option.disabled,
})}
>
{option.title}
</a>
</Button>
))}
</div>
</div>
</div>
<div className="mt-8">
<div className="grid grid-cols-[4rem_1fr] gap-x-4">
<div className="flex flex-col">
<Input
type="number"
placeholder={quantityInputPlaceholder}
value={quantityInput}
onChange={(e) => setQuantityInput(e.target.value)}
/>
</div>
<Button {...addToCartbutton}>{addToCartbutton.title}</Button>
</div>
<div className="my-4">
<Button {...buyNowButtons} className="w-full">
{buyNowButtons.title}
</Button>
</div>
<p className="text-center text-tiny">{freeShipping}</p>
</div>
</form>
<InformationTabs tabs={tabs} />
</div>
</div>
</div>
</div>
</div>
</header>
);
};
const StarRating = ({ rating }: { rating: number }) => {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 !== 0;
return (
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => {
const isFullStar = i < fullStars;
const isHalfStar = hasHalfStar && i === fullStars;
return (
<div key={i}>
{isFullStar ? (
<StarFull className="text-scheme-text" />
) : isHalfStar ? (
<StarHalf className="text-scheme-text" />
) : (
<Star className="text-scheme-text" />
)}
</div>
);
})}
</div>
);
};
const Gallery = ({ images }: GalleryProps) => {
return (
<React.Fragment>
<div className="relative block w-full pt-[120%] lg:hidden">
<img
src={images[0].src}
alt={images[0].alt}
className="absolute inset-0 size-full object-cover"
/>
</div>
<div className="hidden lg:flex lg:flex-col">
{images.map((slide, index) => (
<div key={index} className="relative lg:h-screen lg:max-h-[60rem]">
<img
src={slide.src}
alt={slide.alt}
className="absolute inset-0 size-full object-cover"
/>
</div>
))}
</div>
</React.Fragment>
);
};
const InformationTabs = ({ tabs }: { tabs: TabProps[] }) => {
return (
<Tabs defaultValue={tabs[0].value}>
<TabsList className="mb-5 flex-wrap items-center gap-6 md:mb-6">
{tabs.map((tab, index) => (
<TabsTrigger
key={index}
value={tab.value}
className="rounded-none border-0 border-b-[1.5px] bg-transparent px-0 py-2 data-[state=active]:bg-transparent data-[state=inactive]:border-transparent"
>
{tab.trigger}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab, index) => (
<TabsContent key={index} value={tab.value} className="data-[state=active]:animate-tabs">
<p>{tab.description}</p>
</TabsContent>
))}
</Tabs>
);
};
export const ProductHeader6Defaults: Props = {
breadcrumbs: [
{ url: "#", title: "Shop all" },
{ url: "#", title: "Category" },
{ url: "#", title: "Product name" },
],
heading: "Product name",
price: "$55",
rating: {
review: 10,
starsNumber: 3.5,
},
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat.",
selectVariants: [
{ value: "first-choice", label: "Option One" },
{ value: "second-choice", label: "Option Two" },
{ value: "third-choice", label: "Option Three" },
],
images: [
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 1",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 2",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 3",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 4",
},
{
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
alt: "Relume placeholder image 5",
},
],
addToCartbutton: { title: "Add to cart" },
buyNowButtons: { title: "Buy now", variant: "secondary" },
options: [
{
title: "Option one",
url: "#",
},
{ title: "Option two", url: "#", variant: "secondary" },
{ title: "Option three", url: "#", variant: "secondary", disabled: true },
],
quantityInputPlaceholder: "1",
freeShipping: "Free shipping over $50",
defaultTabValue: "tab-details",
tabs: [
{
value: "tab-details",
trigger: "Details",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.",
},
{
value: "tab-shipping",
trigger: "Shipping",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.",
},
{
value: "tab-returns",
trigger: "Returns",
description:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.",
},
],
};npm i @radix-ui/react-accordion @radix-ui/react-dialog @radix-ui/react-label @radix-ui/react-select @radix-ui/react-slot @radix-ui/react-tabs class-variance-authority@0.7.1 clsx@^2.1.1 embla-carousel-react@^8.5.2 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2


