ProductCard
Product Card usage blocks.
ProductCard Vertical
The most popular option for presenting detailed product information in a group.
import { SfButton, SfRating, SfCounter, SfLink, SfIconShoppingCart, SfIconFavorite } from '@storefront-ui/react';
export default function ProductCardVertical() {
return (
<div className="border border-neutral-200 rounded-md hover:shadow-lg max-w-[300px]">
<div className="relative">
<SfLink href="#" className="block">
<img
src="https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/sneakers.png"
alt="Great product"
className="object-cover h-auto rounded-md aspect-square"
width="300"
height="300"
/>
</SfLink>
<SfButton
variant="tertiary"
size="sm"
square
className="absolute bottom-0 right-0 mr-2 mb-2 bg-white ring-1 ring-inset ring-neutral-200 !rounded-full"
aria-label="Add to wishlist"
>
<SfIconFavorite size="sm" />
</SfButton>
</div>
<div className="p-4 border-t border-neutral-200">
<SfLink href="#" variant="secondary" className="no-underline">
Athletic mens walking sneakers
</SfLink>
<div className="flex items-center pt-1">
<SfRating size="xs" value={5} max={5} />
<SfLink href="#" variant="secondary" className="pl-1 no-underline">
<SfCounter size="xs">{123}</SfCounter>
</SfLink>
</div>
<p className="block py-2 font-normal typography-text-sm text-neutral-700">
Lightweight • Non slip • Flexible outsole • Easy to wear on and off
</p>
<span className="block pb-2 font-bold typography-text-lg">$2345,99</span>
<SfButton size="sm" slotPrefix={<SfIconShoppingCart size="sm" />}>
Add to cart
</SfButton>
</div>
</div>
);
}
ProductCard Horizontal
Present products in checkout summary in a way that customers can have an easy access to increase, decrease and remove particular product.
import { SfButton, SfIconRemove, SfLink, SfIconAdd, SfIconSell, SfIconDelete } from '@storefront-ui/react';
import { useCounter } from 'react-use';
import { useId, ChangeEvent } from 'react';
import { clamp } from '@storefront-ui/shared';
export default function ProductCardHorizontal() {
const inputId = useId();
const min = 1;
const max = 10;
const [value, { inc, dec, set }] = useCounter(min);
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
const { value: currentValue } = event.target;
const nextValue = parseFloat(currentValue);
set(Number(clamp(nextValue, min, max)));
}
return (
<div className="relative flex border-b-[1px] border-neutral-200 hover:shadow-lg min-w-[320px] max-w-[640px] p-4">
<div className="relative overflow-hidden rounded-md w-[100px] sm:w-[176px]">
<SfLink href="#">
<img
className="w-full h-auto border rounded-md border-neutral-200"
src="https://storage.googleapis.com/sfui_docs_artifacts_bucket_public/production/smartwatch.png"
alt="alt"
width="300"
height="300"
/>
</SfLink>
<div className="absolute top-0 left-0 text-white bg-secondary-600 py-1 pl-1.5 pr-2 text-xs font-medium">
<SfIconSell size="xs" className="mr-1" />
Sale
</div>
</div>
<div className="flex flex-col pl-4 min-w-[180px] flex-1">
<SfLink href="#" variant="secondary" className="no-underline typography-text-sm sm:typography-text-lg">
Smartwatch Fitness Tracker
</SfLink>
<div className="my-2 sm:mb-0">
<ul className="text-xs font-normal leading-5 sm:typography-text-sm text-neutral-700">
<li>
<span className="mr-1">Size:</span>
<span className="font-medium">6.5</span>
</li>
<li>
<span className="mr-1">Color:</span>
<span className="font-medium">Red</span>
</li>
</ul>
</div>
<div className="items-center sm:mt-auto sm:flex">
<span className="font-bold sm:ml-auto sm:order-1 typography-text-sm sm:typography-text-lg">$2,345.99</span>
<div className="flex items-center justify-between mt-4 sm:mt-0">
<div className="flex border border-neutral-300 rounded-md">
<SfButton
variant="tertiary"
square
className="rounded-r-none"
disabled={value <= min}
aria-controls={inputId}
aria-label="Decrease value"
onClick={() => dec()}
>
<SfIconRemove />
</SfButton>
<input
id={inputId}
type="number"
role="spinbutton"
className="appearance-none mx-2 w-8 text-center bg-transparent font-medium [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-inner-spin-button]:display-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-outer-spin-button]:display-none [&::-webkit-outer-spin-button]:m-0 [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none disabled:placeholder-disabled-900 focus-visible:outline focus-visible:outline-offset focus-visible:rounded-sm"
min={min}
max={max}
value={value}
onChange={handleOnChange}
/>
<SfButton
variant="tertiary"
square
className="rounded-l-none"
disabled={value >= max}
aria-controls={inputId}
aria-label="Increase value"
onClick={() => inc()}
>
<SfIconAdd />
</SfButton>
</div>
<button
aria-label="Remove"
type="button"
className="text-neutral-500 text-xs font-light ml-auto flex items-center px-3 py-1.5"
>
<SfIconDelete />
<span className="hidden ml-1.5 sm:block"> Remove </span>
</button>
</div>
</div>
</div>
</div>
);
}
Details
ProductCard details can be used in product details page or to build component used for listing products of many categories.
import {
SfRating,
SfButton,
SfLink,
SfCounter,
SfIconShoppingCart,
SfIconCompareArrows,
SfIconFavorite,
SfIconSell,
SfIconPackage,
SfIconRemove,
SfIconAdd,
SfIconWarehouse,
SfIconSafetyCheck,
SfIconShoppingCartCheckout,
} from '@storefront-ui/react';
import { useCounter } from 'react-use';
import { useId, ChangeEvent } from 'react';
import { clamp } from '@storefront-ui/shared';
export default function ProductDetails() {
const inputId = useId();
const min = 1;
const max = 999;
const [value, { inc, dec, set }] = useCounter(min);
function handleOnChange(event: ChangeEvent<HTMLInputElement>) {
const { value: currentValue } = event.target;
const nextValue = parseFloat(currentValue);
set(Number(clamp(nextValue, min, max)));
}
return (
<section className="md:max-w-[640px]">
<div className="inline-flex items-center justify-center text-sm font-medium text-white bg-secondary-600 py-1.5 px-3 mb-4">
<SfIconSell size="sm" className="mr-1.5" />
Sale
</div>
<h1 className="mb-1 font-bold typography-headline-4">
Mini Foldable Drone with HD Camera FPV Wifi RC Quadcopter
</h1>
<strong className="block font-bold typography-headline-3">$2,345.99</strong>
<div className="inline-flex items-center mt-4 mb-2">
<SfRating size="xs" value={3} max={5} />
<SfCounter className="ml-1" size="xs">
123
</SfCounter>
<SfLink href="#" variant="secondary" className="ml-2 text-xs text-neutral-500">
123 reviews
</SfLink>
</div>
<ul className="mb-4 font-normal typography-text-sm">
<li>HD Pictures & Videos and FPV Function</li>
<li>Intelligent Voice Control</li>
<li>Multiple Fun Flights</li>
<li>Easy to Use</li>
<li>Foldable Design & Double Flight Time</li>
</ul>
<div className="py-4 mb-4 border-gray-200 border-y">
<div className="bg-primary-100 text-primary-700 flex justify-center gap-1.5 py-1.5 typography-text-sm items-center mb-4 rounded-md">
<SfIconShoppingCartCheckout />1 in cart
</div>
<div className="items-start xs:flex">
<div className="flex flex-col items-stretch xs:items-center xs:inline-flex">
<div className="flex border border-neutral-300 rounded-md">
<SfButton
variant="tertiary"
square
className="rounded-r-none p-3"
disabled={value <= min}
aria-controls={inputId}
aria-label="Decrease value"
onClick={() => dec()}
>
<SfIconRemove />
</SfButton>
<input
id={inputId}
type="number"
role="spinbutton"
className="grow appearance-none mx-2 w-8 text-center bg-transparent font-medium [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-inner-spin-button]:display-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-outer-spin-button]:display-none [&::-webkit-outer-spin-button]:m-0 [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none disabled:placeholder-disabled-900 focus-visible:outline focus-visible:outline-offset focus-visible:rounded-sm"
min={min}
max={max}
value={value}
onChange={handleOnChange}
/>
<SfButton
variant="tertiary"
square
className="rounded-l-none p-3"
disabled={value >= max}
aria-controls={inputId}
aria-label="Increase value"
onClick={() => inc()}
>
<SfIconAdd />
</SfButton>
</div>
<p className="self-center mt-1 mb-4 text-xs text-neutral-500 xs:mb-0">
<strong className="text-neutral-900">{max}</strong> in stock
</p>
</div>
<SfButton size="lg" className="w-full xs:ml-4" slotPrefix={<SfIconShoppingCart size="sm" />}>
Add to cart
</SfButton>
</div>
<div className="flex justify-center mt-4 gap-x-4">
<SfButton size="sm" variant="tertiary" slotPrefix={<SfIconCompareArrows size="sm" />}>
Compare
</SfButton>
<SfButton size="sm" variant="tertiary" slotPrefix={<SfIconFavorite size="sm" />}>
Add to list
</SfButton>
</div>
</div>
<div className="flex first:mt-4">
<SfIconPackage size="sm" className="flex-shrink-0 mr-1 text-neutral-500" />
<p className="text-sm">
Free shipping, arrives by Thu, Apr 7. Want it faster?
<SfLink href="#" variant="secondary" className="mx-1">
Add an address
</SfLink>
to see options
</p>
</div>
<div className="flex mt-4">
<SfIconWarehouse size="sm" className="flex-shrink-0 mr-1 text-neutral-500" />
<p className="text-sm">
Pickup not available at your shop.
<SfLink href="#" variant="secondary" className="ml-1">
Check availability nearby
</SfLink>
</p>
</div>
<div className="flex mt-4">
<SfIconSafetyCheck size="sm" className="flex-shrink-0 mr-1 text-neutral-500" />
<p className="text-sm">
Free 30-days returns.
<SfLink href="#" variant="secondary" className="ml-1">
Details
</SfLink>
</p>
</div>
</section>
);
}