Product Buy Box
Bloque de compra de la ficha: precio, ahorro, color y talla, cantidad, añadir al carrito, comprar ya y detalles desplegables.
Vista Previa en Vivo
Audio
BestsellerAurora Wireless Headphones
★★★★★★★★★★4.8(312)$189.00Was $229.00Save 17%
Adaptive noise cancelling, 40-hour battery and memory-foam cushions that disappear after five minutes. Tuned in-house for a warm, detailed sound.
In stock — ships in 24 hours
- Free shippingOver $75
- 30-day returnsFree & easy
Features
Hybrid ANC with transparency mode · Bluetooth 5.4 multipoint · USB-C fast charge (10 min = 5 h) · Foldable aluminium frame.
Shipping & returns
Free standard shipping over $75, delivered in 2–4 business days. Return anything within 30 days for a full refund.
Warranty
Two-year limited warranty covering manufacturing defects, with free repair or replacement.
Uso
Copia y pega este código directamente en tu componente React:
import { ProductBuyBox } from 'react-component-kit-ui/ecommerce';
<ProductBuyBox product={product} checkoutHref="/checkout/" />Instalación Manual
Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/ProductBuyBox.tsx + ProductBuyBox.css:
'use client';
import { useEffect, useRef } from 'react';
import Badge from '../feedback/Badge';
import StepperInput from '../forms/StepperInput';
import ProductRating from './ProductRating';
import VariantPicker from './VariantPicker';
import TrustBadges from './TrustBadges';
import StockIndicator from './StockIndicator';
import { formatMoney } from './cart-store';
import type { Product } from './types';
import { bindAddToCart } from './cart-store';
import './ProductBuyBox.css';
/** class:list → className */
function cx(...args: unknown[]): string {
const out: string[] = [];
const walk = (v: unknown) => {
if (!v) return;
if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
else if (Array.isArray(v)) v.forEach(walk);
else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
};
args.forEach(walk);
return out.join(' ');
}
/**
* ProductBuyBox — Product detail purchase panel: title, rating, price, colour/size pickers,
* quantity (StepperInput), add to cart, buy now, guarantees and expandable details.
*
* @param {Product} product - Product data. A demo product is used by default.
* @param {string} categoryLabel - Eyebrow text (e.g. the category name).
* @param {string} checkoutHref - Where "Buy now" goes after adding the item. Default '#'.
* @param {Array<{title: string, body: string}>} details - Expandable sections (materials, shipping…).
* @param {string} currency - ISO currency. Default 'USD'.
* @param {string} locale - Number format locale. Default 'en-US'.
* @param {string} cartKey - Cart storage key. Default 'ack-cart'.
* @param {string} id - Base id for inner controls. Default 'buy-box'.
* @param {string} variant - Style (plain|glass). Default 'plain'.
* @param {string} size - Type scale (md|lg). Default 'md'.
* @param {string} className - Extra class.
*
* Accessibility: one h1 with the product name; pickers are radio groups; details use native
* <details>/<summary>; sold-out state disables the buttons and says so in text.
*/
export interface ProductBuyBoxProps {
product?: Product;
categoryLabel?: string;
checkoutHref?: string;
details?: Array<{ title: string; body: string }>;
currency?: string;
locale?: string;
cartKey?: string;
id?: string;
variant?: 'plain' | 'glass';
size?: 'md' | 'lg';
className?: string;
}
export function ProductBuyBox({
product = {
slug: 'aurora-headphones',
name: 'Aurora Wireless Headphones',
price: 189,
compareAt: 229,
category: 'audio',
rating: 4.8,
reviews: 312,
badge: 'Bestseller',
badgeVariant: 'warning',
colors: [{ name: 'Graphite', hex: '#2b2b2f' }, { name: 'Sand', hex: '#d8c3a5' }, { name: 'Ocean', hex: '#3d6f9e' }],
icon: 'headphones',
hue: 38,
description: 'Adaptive noise cancelling, 40-hour battery and memory-foam cushions that disappear after five minutes. Tuned in-house for a warm, detailed sound.',
inStock: true,
},
categoryLabel = 'Audio',
checkoutHref = '#',
details = [
{ title: 'Features', body: 'Hybrid ANC with transparency mode · Bluetooth 5.4 multipoint · USB-C fast charge (10 min = 5 h) · Foldable aluminium frame.' },
{ title: 'Shipping & returns', body: 'Free standard shipping over $75, delivered in 2–4 business days. Return anything within 30 days for a full refund.' },
{ title: 'Warranty', body: 'Two-year limited warranty covering manufacturing defects, with free repair or replacement.' },
],
currency = 'USD',
locale = 'en-US',
cartKey = 'ack-cart',
id = 'buy-box',
variant = 'plain',
size = 'md',
className = '',
}: ProductBuyBoxProps) {
const money = { currency, locale };
const inStock = product.inStock !== false;
const saving = product.compareAt ? Math.round((1 - product.price / product.compareAt) * 100) : 0;
const payload = JSON.stringify({ id: product.slug, name: product.name, price: product.price, icon: product.icon, hue: product.hue, image: product.image });
const addId = `${id}-add`;
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
bindAddToCart(document);
document.querySelectorAll<HTMLAnchorElement>('[data-buy-now]').forEach((link) => {
link.addEventListener('click', (e) => {
const btn = document.getElementById(link.dataset.buyNow || '') as HTMLButtonElement | null;
if (!btn || btn.disabled) return;
e.preventDefault();
btn.dataset.openCart = 'false';
btn.click();
window.location.href = link.href;
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className={cx(['buy-box', `buy-box--${variant}`, `buy-box--${size}`, className])} data-rk-ed869a="">
<div className="buy-box__top" data-rk-ed869a="">
{categoryLabel && <p className="buy-box__cat" data-rk-ed869a="">{categoryLabel}</p>}
{product.badge && <Badge variant={product.badgeVariant ?? 'default'}>{product.badge}</Badge>}
</div>
<h1 className="buy-box__title" data-rk-ed869a="">{product.name}</h1>
{product.rating !== undefined && (
<a className="buy-box__rating" href="#reviews" data-rk-ed869a="">
<ProductRating value={product.rating} count={product.reviews} size="md" />
</a>
)}
<p className="buy-box__price" data-rk-ed869a="">
<span className="buy-box__now" data-rk-ed869a="">{formatMoney(product.price, money)}</span>
{product.compareAt && <s className="buy-box__was" data-rk-ed869a=""><span className="sr-only" data-rk-ed869a="">Was </span>{formatMoney(product.compareAt, money)}</s>}
{saving > 0 && <span className="buy-box__save" data-rk-ed869a="">Save {saving}%</span>}
</p>
{product.description && <p className="buy-box__desc" data-rk-ed869a="">{product.description}</p>}
<div className="buy-box__pickers" data-rk-ed869a="">
{product.colors && product.colors.length > 0 && (
<VariantPicker label="Colour" name={`${id}-color`} type="swatch" target={addId} options={product.colors.map((c) => ({ label: c.name, hex: c.hex }))} />
)}
{product.sizes && product.sizes.length > 0 && (
<VariantPicker label="Size" name={`${id}-size`} type="pill" target={addId} options={product.sizes.map((s) => ({ label: s }))} />
)}
</div>
<div className="buy-box__actions" data-rk-ed869a="">
<StepperInput id={`${id}-qty`} name="qty" value={1} min={1} max={10} />
<button className="buy-box__add" id={addId} type="button" data-add-to-cart={payload} data-cart-key={cartKey} data-qty-from={`${id}-qty`} data-added-label="Added to cart" disabled={!inStock} data-rk-ed869a="">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-ed869a=""><path d="M6 7h12l-1 13H7z" data-rk-ed869a="" /><path d="M9 7V6a3 3 0 0 1 6 0v1" data-rk-ed869a="" /></svg>
<span data-label data-rk-ed869a="">{inStock ? 'Add to cart' : 'Sold out'}</span>
</button>
</div>
{inStock && (
<a className="buy-box__buy" href={checkoutHref} data-buy-now={addId} data-rk-ed869a="">Buy now</a>
)}
{inStock && product.stock !== undefined && product.stock <= 5 ? (
<StockIndicator stock={product.stock} viewers={8 + (product.reviews ?? 0) % 17} />
) : (
<p className="buy-box__stock" data-in={inStock ? '' : undefined} data-rk-ed869a="">
<span aria-hidden="true" data-rk-ed869a=""></span>{inStock ? 'In stock — ships in 24 hours' : 'Out of stock — back soon'}
</p>
)}
<TrustBadges variant="inline" size="sm" items={[
{ icon: 'truck', title: 'Free shipping', text: 'Over $75' },
{ icon: 'return', title: '30-day returns', text: 'Free & easy' },
]} />
{details.length > 0 && (
<div className="buy-box__details" data-rk-ed869a="">
{details.map((d, i) => (
<details key={i} open={i === 0} data-rk-ed869a="">
<summary data-rk-ed869a="">{d.title}</summary>
<p data-rk-ed869a="">{d.body}</p>
</details>
))}
</div>
)}
</div>
</>
);
}
export default ProductBuyBox;/* ProductBuyBox — ported from astro-component-kit (ecommerce/ProductBuyBox.astro) */
.buy-box:where([data-rk-ed869a]) { display: flex; flex-direction: column; gap: var(--sp-4, 1rem); min-width: 0; }
.buy-box--glass:where([data-rk-ed869a]) {
padding: var(--sp-6, 1.5rem);
border-radius: var(--r-xl, 28px);
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
}
.buy-box__top:where([data-rk-ed869a]) { display: flex; align-items: center; gap: var(--sp-3, 0.75rem); }
.buy-box__cat:where([data-rk-ed869a]) { margin: 0; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); }
.buy-box__title:where([data-rk-ed869a]) { margin: 0; font-size: clamp(1.8rem, 3.6vw, 2.6rem); font-weight: 900; line-height: 1.08; letter-spacing: -0.035em; color: var(--c-text-1, #fff); }
.buy-box--lg:where([data-rk-ed869a]) .buy-box__title:where([data-rk-ed869a]) { font-size: clamp(2.2rem, 4.4vw, 3.2rem); }
.buy-box__rating:where([data-rk-ed869a]) { width: fit-content; text-decoration: none; border-radius: var(--r-sm, 8px); }
.buy-box__rating:hover:where([data-rk-ed869a]) .p-rating__count { text-decoration: underline; }
.buy-box__price:where([data-rk-ed869a]) { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-3, 0.75rem); margin: 0; }
.buy-box__now:where([data-rk-ed869a]) { font-size: 1.9rem; font-weight: 900; color: var(--c-text-1, #fff); letter-spacing: -0.02em; }
.buy-box__was:where([data-rk-ed869a]) { font-size: 1.05rem; color: var(--c-text-2, #a3a3a3); }
.buy-box__save:where([data-rk-ed869a]) { padding: 0.2rem 0.6rem; border-radius: var(--r-full, 9999px); background: rgba(52, 211, 153, 0.12); color: var(--c-success, #34d399); font-size: 0.78rem; font-weight: 800; }
.buy-box__desc:where([data-rk-ed869a]) { margin: 0; line-height: 1.7; color: var(--c-text-2, #a3a3a3); }
.buy-box__pickers:where([data-rk-ed869a]) { display: grid; gap: var(--sp-5, 1.25rem); padding: var(--sp-2, 0.5rem) 0; }
.buy-box__actions:where([data-rk-ed869a]) { display: flex; gap: var(--sp-3, 0.75rem); align-items: stretch; }
.buy-box__actions:where([data-rk-ed869a]) .stepper-container { width: auto; flex-shrink: 0; }
.buy-box__actions:where([data-rk-ed869a]) .stepper { height: 52px; border-radius: var(--r-full, 9999px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.12); }
.buy-box__actions:where([data-rk-ed869a]) .stepper-btn { width: 48px; height: 100%; background: transparent; color: var(--c-text-1, #fff); }
.buy-box__actions:where([data-rk-ed869a]) .stepper-btn:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.07); }
.buy-box__actions:where([data-rk-ed869a]) .stepper-input { width: 44px; height: 100%; }
.buy-box__add:where([data-rk-ed869a]),
.buy-box__buy:where([data-rk-ed869a]) {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--sp-2, 0.5rem);
min-height: 52px;
border-radius: var(--r-full, 9999px);
font: inherit;
font-weight: 800;
font-size: 0.95rem;
text-decoration: none;
cursor: pointer;
transition: filter 0.2s ease, transform 0.2s ease, background 0.2s ease;
}
.buy-box__add:where([data-rk-ed869a]) { flex: 1; border: 0; background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #050505); }
.buy-box__add:hover:where([data-rk-ed869a]) { filter: brightness(1.08); transform: translateY(-1px); }
.buy-box__add.is-added:where([data-rk-ed869a]) { background: var(--c-success, #34d399); }
.buy-box__add:disabled:where([data-rk-ed869a]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-2, #a3a3a3); cursor: not-allowed; transform: none; }
.buy-box__buy:where([data-rk-ed869a]) { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.16); color: var(--c-text-1, #fff); }
.buy-box__buy:hover:where([data-rk-ed869a]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
.buy-box:where([data-rk-ed869a]) :is(a, button, summary):focus-visible:where([data-rk-ed869a]) { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.buy-box__stock:where([data-rk-ed869a]) { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: 0; font-size: 0.85rem; color: var(--c-danger, #f87171); }
.buy-box__stock[data-in]:where([data-rk-ed869a]) { color: var(--c-success, #34d399); }
.buy-box__stock:where([data-rk-ed869a]) span:where([data-rk-ed869a]) { width: 8px; height: 8px; border-radius: 50%; background: currentColor; box-shadow: 0 0 10px currentColor; }
.buy-box__details:where([data-rk-ed869a]) { border-top: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
.buy-box__details:where([data-rk-ed869a]) details:where([data-rk-ed869a]) { border-bottom: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
.buy-box__details:where([data-rk-ed869a]) summary:where([data-rk-ed869a]) {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 52px;
font-weight: 700;
color: var(--c-text-1, #fff);
cursor: pointer;
list-style: none;
}
.buy-box__details:where([data-rk-ed869a]) summary:where([data-rk-ed869a])::-webkit-details-marker { display: none; }
.buy-box__details:where([data-rk-ed869a]) summary:where([data-rk-ed869a])::after { content: '+'; font-size: 1.3rem; font-weight: 400; color: var(--c-text-2, #a3a3a3); transition: transform 0.2s ease; }
.buy-box__details:where([data-rk-ed869a]) details[open]:where([data-rk-ed869a]) summary:where([data-rk-ed869a])::after { transform: rotate(45deg); }
.buy-box__details:where([data-rk-ed869a]) p:where([data-rk-ed869a]) { margin: 0 0 var(--sp-4, 1rem); font-size: 0.9rem; line-height: 1.7; color: var(--c-text-2, #a3a3a3); }
.sr-only:where([data-rk-ed869a]) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }Información Rápida
- Categoría
- E-commerce
- Nombre de archivo
ProductBuyBox.tsx- Dependencias
- Ninguna — React + CSS puro
- Etiquetas
- ecommerceshopproduct-pageadd-to-cartpdp