Variant Selector
Color swatches, size pickers, and option selectors for product variants in retail/POS.
Size Selector
Size
M
<div class="variant-selector">
<div class="variant-selector-label">
<span>Size</span>
<span class="variant-selector-selected">M</span>
</div>
<div class="variant-selector-options">
<button class="variant-selector-option">XS</button>
<button class="variant-selector-option">S</button>
<button class="variant-selector-option variant-selector-option-selected">M</button>
<button class="variant-selector-option">L</button>
<button class="variant-selector-option">XL</button>
<button class="variant-selector-option variant-selector-option-unavailable">XXL</button>
</div>
</div> Requires: ux.min.css
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between text-sm font-medium text-base-content">
<span>Size</span>
<span class="font-normal text-base-content/60">M</span>
</div>
<div class="flex flex-wrap gap-2">
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content hover:border-base-content/40">XS</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content hover:border-base-content/40">S</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-primary rounded-xl bg-primary/10 text-primary">M</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content hover:border-base-content/40">L</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content hover:border-base-content/40">XL</button>
<button class="relative flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content opacity-50 cursor-not-allowed">XXL</button>
</div>
</div> Requires: tw.min.css
// Toggle selection on click:
document.querySelectorAll('.variant-selector-option').forEach(opt => {
opt.addEventListener('click', () => {
if (opt.classList.contains('variant-selector-option-unavailable')) return;
opt.closest('.variant-selector-options')
.querySelectorAll('.variant-selector-option')
.forEach(o => o.classList.remove('variant-selector-option-selected'));
opt.classList.add('variant-selector-option-selected');
});
}); Color Swatches
Color
Ocean Blue
Only 3 left in stock
<div class="variant-selector variant-selector-color">
<div class="variant-selector-label">
<span>Color</span>
<span class="variant-selector-selected">Ocean Blue</span>
</div>
<div class="variant-selector-options">
<button class="variant-selector-option" style="background: #1e3a5f" aria-label="Navy"></button>
<button class="variant-selector-option variant-selector-option-selected" style="background: #3b82f6" aria-label="Ocean Blue"></button>
<button class="variant-selector-option" style="background: #10b981" aria-label="Emerald"></button>
<button class="variant-selector-option variant-selector-option-light" style="background: #fef9c3" aria-label="Cream"></button>
<button class="variant-selector-option variant-selector-option-dark" style="background: #111827" aria-label="Charcoal"></button>
<button class="variant-selector-option variant-selector-option-unavailable" style="background: #ef4444" aria-label="Red - Sold Out"></button>
</div>
<div class="variant-selector-stock variant-selector-stock-low-stock">
<span class="variant-selector-stock-dot"></span>
<span class="variant-selector-stock-text">Only 3 left in stock</span>
</div>
</div> Requires: ux.min.css
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between text-sm font-medium text-base-content">
<span>Color</span>
<span class="font-normal text-base-content/60">Ocean Blue</span>
</div>
<div class="flex flex-wrap gap-1">
<button class="flex items-center justify-center min-w-[36px] w-9 h-9 p-0 rounded-full border-[3px] border-transparent shadow-[inset_0_0_0_2px_var(--color-base-100)]" style="background: #1e3a5f" aria-label="Navy"></button>
<button class="flex items-center justify-center min-w-[36px] w-9 h-9 p-0 rounded-full border-[3px] border-primary shadow-[inset_0_0_0_2px_var(--color-base-100)]" style="background: #3b82f6" aria-label="Ocean Blue"></button>
<button class="flex items-center justify-center min-w-[36px] w-9 h-9 p-0 rounded-full border-[3px] border-transparent shadow-[inset_0_0_0_2px_var(--color-base-100)]" style="background: #10b981" aria-label="Emerald"></button>
<button class="flex items-center justify-center min-w-[36px] w-9 h-9 p-0 rounded-full border-[3px] border-transparent shadow-[inset_0_0_0_1px_var(--color-base-300),inset_0_0_0_2px_var(--color-base-100)]" style="background: #fef9c3" aria-label="Cream"></button>
<button class="flex items-center justify-center min-w-[36px] w-9 h-9 p-0 rounded-full border-[3px] border-transparent shadow-[inset_0_0_0_2px_var(--color-base-100)]" style="background: #111827" aria-label="Charcoal"></button>
<button class="flex items-center justify-center min-w-[36px] w-9 h-9 p-0 rounded-full border-[3px] border-transparent opacity-50 cursor-not-allowed" style="background: #ef4444" aria-label="Red - Sold Out"></button>
</div>
<div class="flex items-center gap-1 text-xs mt-1">
<span class="w-1.5 h-1.5 rounded-full bg-warning"></span>
<span class="text-base-content/60">Only 3 left in stock</span>
</div>
</div> Requires: tw.min.css
// Color swatch selection with label update:
document.querySelectorAll('.variant-selector-color .variant-selector-option').forEach(opt => {
opt.addEventListener('click', () => {
if (opt.classList.contains('variant-selector-option-unavailable')) return;
const parent = opt.closest('.variant-selector');
parent.querySelectorAll('.variant-selector-option')
.forEach(o => o.classList.remove('variant-selector-option-selected'));
opt.classList.add('variant-selector-option-selected');
const label = parent.querySelector('.variant-selector-selected');
if (label) label.textContent = opt.getAttribute('aria-label');
});
}); Chip & Button Group Variants
ERPlora Material
Quantity
<!-- Chip variant -->
<div class="variant-selector variant-selector-chip">
<div class="variant-selector-label">ERPlora Material</div>
<div class="variant-selector-options">
<button class="variant-selector-option variant-selector-option-selected">Cotton</button>
<button class="variant-selector-option">Polyester</button>
<button class="variant-selector-option">Silk</button>
<button class="variant-selector-option variant-selector-option-disabled">Linen</button>
</div>
</div>
<!-- Button group variant -->
<div class="variant-selector variant-selector-button-group" style="margin-top: 1.5rem;">
<div class="variant-selector-label">Quantity</div>
<div class="variant-selector-options">
<button class="variant-selector-option">1 Pack</button>
<button class="variant-selector-option variant-selector-option-selected">3 Pack</button>
<button class="variant-selector-option">6 Pack</button>
<button class="variant-selector-option">12 Pack</button>
</div>
</div> Requires: ux.min.css
<!-- Chip variant -->
<div class="flex flex-col gap-2">
<div class="text-sm font-medium text-base-content">ERPlora Material</div>
<div class="flex flex-wrap gap-2">
<button class="flex items-center justify-center h-auto px-4 py-1 text-sm font-medium rounded-full bg-primary border-2 border-primary text-primary-content">Cotton</button>
<button class="flex items-center justify-center h-auto px-4 py-1 text-sm font-medium rounded-full bg-base-100 border-2 border-base-300 text-base-content hover:border-base-content/40">Polyester</button>
<button class="flex items-center justify-center h-auto px-4 py-1 text-sm font-medium rounded-full bg-base-100 border-2 border-base-300 text-base-content hover:border-base-content/40">Silk</button>
<button class="flex items-center justify-center h-auto px-4 py-1 text-sm font-medium rounded-full bg-base-100 border-2 border-base-300 text-base-content opacity-40 cursor-not-allowed pointer-events-none">Linen</button>
</div>
</div>
<!-- Button group variant -->
<div class="flex flex-col gap-2 mt-6">
<div class="text-sm font-medium text-base-content">Quantity</div>
<div class="flex flex-wrap">
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-l-xl bg-base-100 text-base-content">1 Pack</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-primary bg-primary/10 text-primary -ml-0.5 z-[1]">3 Pack</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 bg-base-100 text-base-content -ml-0.5">6 Pack</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-r-xl bg-base-100 text-base-content -ml-0.5">12 Pack</button>
</div>
</div> Requires: tw.min.css
// Generic variant selection handler:
document.querySelectorAll('.variant-selector-options').forEach(group => {
group.addEventListener('click', (e) => {
const opt = e.target.closest('.variant-selector-option');
if (!opt || opt.classList.contains('variant-selector-option-disabled')) return;
group.querySelectorAll('.variant-selector-option')
.forEach(o => o.classList.remove('variant-selector-option-selected'));
opt.classList.add('variant-selector-option-selected');
});
}); Options with Price & Image Variant
Storage
256 GB
ERPlora Style
<!-- Options with price info -->
<div class="variant-selector">
<div class="variant-selector-label">
<span>Storage</span>
<span class="variant-selector-selected">256 GB</span>
</div>
<div class="variant-selector-options">
<button class="variant-selector-option">
<div class="variant-selector-option-content">
<span class="variant-selector-option-name">128 GB</span>
<span class="variant-selector-option-price">$799</span>
</div>
</button>
<button class="variant-selector-option variant-selector-option-selected">
<div class="variant-selector-option-content">
<span class="variant-selector-option-name">256 GB</span>
<span class="variant-selector-option-price">$899</span>
</div>
</button>
<button class="variant-selector-option">
<div class="variant-selector-option-content">
<span class="variant-selector-option-name">512 GB</span>
<span class="variant-selector-option-price">$1099</span>
<span class="variant-selector-option-diff variant-selector-option-diff-up">+$200</span>
</div>
</button>
</div>
</div>
<!-- Image variant -->
<div class="variant-selector variant-selector-image" style="margin-top: 1.5rem;">
<div class="variant-selector-label">ERPlora Style</div>
<div class="variant-selector-options">
<button class="variant-selector-option variant-selector-option-selected">
<img src="https://picsum.photos/seed/style1/100/100" alt="Classic">
</button>
<button class="variant-selector-option">
<img src="https://picsum.photos/seed/style2/100/100" alt="Modern">
</button>
<button class="variant-selector-option">
<img src="https://picsum.photos/seed/style3/100/100" alt="Vintage">
</button>
</div>
</div> Requires: ux.min.css
<!-- Options with price info -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between text-sm font-medium text-base-content">
<span>Storage</span>
<span class="font-normal text-base-content/60">256 GB</span>
</div>
<div class="flex flex-wrap gap-2">
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content">
<div class="flex flex-col items-center gap-0.5">
<span class="font-medium">128 GB</span>
<span class="text-xs text-base-content/60">$799</span>
</div>
</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-primary rounded-xl bg-primary/10 text-primary">
<div class="flex flex-col items-center gap-0.5">
<span class="font-medium">256 GB</span>
<span class="text-xs text-primary/80">$899</span>
</div>
</button>
<button class="flex items-center justify-center min-w-[44px] h-11 px-4 text-sm font-medium border-2 border-base-300 rounded-xl bg-base-100 text-base-content">
<div class="flex flex-col items-center gap-0.5">
<span class="font-medium">512 GB</span>
<span class="text-xs text-base-content/60">$1099</span>
<span class="text-xs text-error">+$200</span>
</div>
</button>
</div>
</div>
<!-- Image variant -->
<div class="flex flex-col gap-2 mt-6">
<div class="text-sm font-medium text-base-content">ERPlora Style</div>
<div class="flex flex-wrap gap-2">
<button class="flex items-center justify-center w-14 h-14 p-0.5 border-2 border-primary rounded-xl overflow-hidden">
<img class="w-full h-full object-cover rounded-lg" src="https://picsum.photos/seed/style1/100/100" alt="Classic">
</button>
<button class="flex items-center justify-center w-14 h-14 p-0.5 border-2 border-base-300 rounded-xl overflow-hidden hover:border-base-content/40">
<img class="w-full h-full object-cover rounded-lg" src="https://picsum.photos/seed/style2/100/100" alt="Modern">
</button>
<button class="flex items-center justify-center w-14 h-14 p-0.5 border-2 border-base-300 rounded-xl overflow-hidden hover:border-base-content/40">
<img class="w-full h-full object-cover rounded-lg" src="https://picsum.photos/seed/style3/100/100" alt="Vintage">
</button>
</div>
</div> Requires: tw.min.css
// Selection with price label update:
document.querySelectorAll('.variant-selector-option').forEach(opt => {
opt.addEventListener('click', () => {
if (opt.classList.contains('variant-selector-option-disabled') ||
opt.classList.contains('variant-selector-option-unavailable')) return;
const group = opt.closest('.variant-selector-options');
group.querySelectorAll('.variant-selector-option')
.forEach(o => o.classList.remove('variant-selector-option-selected'));
opt.classList.add('variant-selector-option-selected');
// Update selected label
const label = opt.closest('.variant-selector')?.querySelector('.variant-selector-selected');
const name = opt.querySelector('.variant-selector-option-name');
if (label && name) label.textContent = name.textContent;
});
}); Classes Reference
| Class | Description |
|---|---|
| .variant-selector | Base container (flex column) |
| .variant-selector-label | Label row with title and selected value |
| .variant-selector-selected | Current selection text (muted) |
| .variant-selector-options | Options container (flex wrap) |
| .variant-selector-option | Individual selectable option |
| .variant-selector-option-selected | Selected state (primary border/bg) |
| .variant-selector-option-disabled | Disabled state (reduced opacity) |
| .variant-selector-option-unavailable | Unavailable with strikethrough line |
| .variant-selector-sm / -lg | Size variants for options |
| .variant-selector-color | Color swatch mode (circular options) |
| .variant-selector-option-light | Light swatch with visible border |
| .variant-selector-option-dark | Dark swatch (white checkmark) |
| .variant-selector-image | Image thumbnail selector |
| .variant-selector-chip | Chip/pill style options |
| .variant-selector-button-group | Connected button group (no gap) |
| .variant-selector-dropdown | Dropdown select variant |
| .variant-selector-option-content | Content wrapper for name + price |
| .variant-selector-option-name | Option name text |
| .variant-selector-option-price | Option price text |
| .variant-selector-option-diff | Price difference indicator |
| .variant-selector-stock | Stock status indicator row |
| .variant-selector-stock-in-stock | Green dot - in stock |
| .variant-selector-stock-low-stock | Yellow dot - low stock |
| .variant-selector-stock-out-of-stock | Red dot - out of stock |