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