primer/react

GitHub's design system implemented as React components for building consistent user interfaces.

Product name thumbnail

Product Name

Product Description

$9.99 $12.99
import React, { useState } from 'react'; import styled, { css } from 'styled-components'; // ============================================================================ // Types // ============================================================================ interface ProductColor { id: string; imageUrl: string; alt?: string; } interface Product { id: string; name: string; description: string; price: number; salePrice?: number; thumbnailUrl: string; thumbnailAlt?: string; detailUrl: string; colors: ProductColor[]; } interface ProductCardProps { product: Product; className?: string; onColorSelect?: (colorId: string) => void; } // ============================================================================ // Styled Components // ============================================================================ const Card = styled.article` background-color: #ffffff; border: 1px solid #e0e0e0; padding: 1rem; margin-bottom: 1.5rem; border-radius: 4px; transition: box-shadow 0.2s ease; &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } `; const ThumbnailLink = styled.a` display: block; position: relative; overflow: hidden; border-radius: 2px; img { display: block; width: 100%; height: auto; aspect-ratio: 1 / 1; object-fit: cover; transition: transform 0.3s ease; &:hover { transform: scale(1.02); } } `; const Title = styled.h2` font-family: 'Open Sans', sans-serif; font-size: 0.9rem; font-weight: 600; line-height: 1.45rem; margin-top: 1rem; margin-bottom: 0; a { color: #1a1a1a; text-decoration: none; transition: color 0.2s ease; &:hover { color: #0066cc; } } `; const Description = styled.span` color: #555555; display: block; font-family: 'Open Sans', sans-serif; font-size: 0.85rem; margin-top: 0.15rem; `; const PriceRow = styled.div` display: flex; align-items: baseline; gap: 0.3rem; margin-top: 0.8rem; flex-wrap: wrap; `; const Price = styled.span<{ $isOnSale: boolean }>` color: ${({ $isOnSale }) => ($isOnSale ? '#1a1a1a' : '#1a1a1a')}; display: inline-block; font-size: 0.85rem; font-weight: 600; ${({ $isOnSale }) => $isOnSale && css` color: #cc0000; `} `; const SalePrice = styled.span` color: #888888; display: inline-block; font-size: 0.85rem; text-decoration: line-through; `; const ColorsContainer = styled.div` display: flex; gap: 0.4rem; margin-top: 0.8rem; flex-wrap: wrap; `; const ColorButton = styled.button<{ $isSelected: boolean }>` display: inline-flex; align-items: center; justify-content: center; height: 30px; width: 30px; padding: 0; border: 2px solid ${({ $isSelected }) => ($isSelected ? '#0066cc' : '#e0e0e0')}; border-radius: 50%; background: transparent; cursor: pointer; transition: all 0.2s ease; overflow: hidden; &:hover { border-color: ${({ $isSelected }) => ($isSelected ? '#0066cc' : '#999999')}; transform: scale(1.05); } &:focus-visible { outline: 2px solid #0066cc; outline-offset: 2px; } img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; } `; // ============================================================================ // Component // ============================================================================ export const ProductCard: React.FC = ({ product, className, onColorSelect, }) => { const [selectedColorId, setSelectedColorId] = useState( product.colors.length > 0 ? product.colors[0].id : null ); const isOnSale = product.salePrice !== undefined && product.salePrice < product.price; const handleColorClick = (colorId: string) => { setSelectedColorId(colorId); onColorSelect?.(colorId); }; return ( {/* Thumbnail */} {product.thumbnailAlt {/* Title */} <a href={product.detailUrl}>{product.name}</a> {/* Description */} {product.description} {/* Price */} ${product.price.toFixed(2)} {isOnSale && ( ${product.salePrice!.toFixed(2)} )} {/* Color Options */} {product.colors.length > 0 && ( {product.colors.map((color) => { const isSelected = selectedColorId === color.id; return ( handleColorClick(color.id)} role="radio" aria-checked={isSelected} aria-label={color.alt || `Color option`} type="button" > {color.alt ); })} )} ); }; // ============================================================================ // Default Export // ============================================================================ export default ProductCard; // ============================================================================ // Example Usage // ============================================================================ /* import React from 'react'; import { ProductCard } from './ProductCard'; const EXAMPLE_PRODUCT: Product = { id: 'prod-001', name: 'Classic T-Shirt', description: 'Comfortable 100% cotton t-shirt', price: 12.99, salePrice: 9.99, thumbnailUrl: 'https://placehold.it/180x180', thumbnailAlt: 'Classic T-Shirt', detailUrl: '/product/classic-t-shirt', colors: [ { id: 'red', imageUrl: 'https://placehold.it/30x30/ff0000/ffffff' }, { id: 'blue', imageUrl: 'https://placehold.it/30x30/0066cc/ffffff' }, { id: 'green', imageUrl: 'https://placehold.it/30x30/00cc66/ffffff' }, { id: 'black', imageUrl: 'https://placehold.it/30x30/222222/ffffff' }, ], }; function App() { return (
console.log('Selected color:', colorId)} />
); } */

Social Bar