Back to Widget Patterns
v2.5Interactive Tooling
Product Feature Configurator
Tesla-style product builder. Option groups with select, radio, and checkbox types. A running price total computed from the selections. Composes PrimaryButtonCTA for the final action.
Live preview
Rendered with real component code. Each example demonstrates a documented variant.
Laptop configurator
Build your laptop
Choose your options and see the total update as you go.
Your configuration
RAM8 GB
Storage256 GB SSD
ColorSilver
Total$1,499
Side-by-side layout
Configure your monitor
Pick the specs that match your workspace.
Your configuration
Screen size24"
Resolution1080p (Full HD)
Panel typeIPS
Total$699
React props
Configure the component via props in React or via attributes in HTML.
| Prop | Type | Default | Description |
|---|---|---|---|
| basePrice | number | required | Starting price before any option deltas are applied |
| groups | OptionGroup[] | required | Ordered list of option groups; each group is a select, radio, or checkbox fieldset |
| title | string | undefined | Optional heading rendered above the option groups |
| description | string | undefined | Optional supporting copy shown below the title |
| currency | string | "USD" | ISO 4217 currency code passed to Intl.NumberFormat for price display |
| layout | "stacked" | "side-by-side" | "stacked" | Stacked renders groups above the summary; side-by-side places them in a two-column grid |
| showPreview | boolean | false | When true, renders preview images from options that carry imageSrc |
| finalCta | { label: string; href: string } | undefined | Renders a PrimaryButtonCTA below the price summary when provided |
| onConfigChange | (selections: Record<string, string | string[]>, totalPrice: number) => void | undefined | Called on every selection change with the full selections map and computed total |
| className | string | undefined | Extra class applied to the root section element |
HTML usage
The HTML variant uses the same shared styles and class names. Drop into any stack.
<link rel="stylesheet" href="path/to/product-feature-configurator/styles.css">
<section class="pfc pfc--stacked">
<header class="pfc__header">
<h2 class="pfc__title">Build your laptop</h2>
<p class="pfc__description">Choose your options and see the total update as you go.</p>
</header>
<div class="pfc__body">
<!-- Groups column -->
<div class="pfc__groups">
<!-- Radio group -->
<fieldset class="pfc__group">
<legend class="pfc__group-label">RAM</legend>
<div class="pfc__options" role="group">
<label class="pfc__option" for="ram-8gb">
<input id="ram-8gb" type="radio" name="ram" value="8gb" checked>
<span class="pfc__option-label">
<span class="pfc__option-name">8 GB</span>
<span class="pfc__option-desc">Great for everyday tasks</span>
</span>
</label>
<label class="pfc__option" for="ram-16gb">
<input id="ram-16gb" type="radio" name="ram" value="16gb">
<span class="pfc__option-label">
<span class="pfc__option-name">16 GB</span>
<span class="pfc__option-desc">Recommended for developers</span>
</span>
<span class="pfc__option-price">+$200</span>
</label>
</div>
</fieldset>
<!-- Select group -->
<fieldset class="pfc__group">
<legend class="pfc__group-label">Storage</legend>
<div class="pfc__options">
<select class="pfc__select" name="storage">
<option value="256gb">256 GB SSD</option>
<option value="512gb">512 GB SSD (+$100)</option>
<option value="1tb">1 TB SSD (+$250)</option>
</select>
</div>
</fieldset>
</div>
<!-- Right column: summary + actions -->
<div class="pfc__right">
<div class="pfc__summary" aria-live="polite">
<p class="pfc__summary-heading">Your configuration</p>
<div class="pfc__summary-line">
<span class="pfc__summary-key">RAM</span>
<span class="pfc__summary-val">8 GB</span>
</div>
<div class="pfc__summary-line">
<span class="pfc__summary-key">Storage</span>
<span class="pfc__summary-val">256 GB SSD</span>
</div>
<div class="pfc__total">
<span class="pfc__total-label">Total</span>
<span class="pfc__total-price">$1,499</span>
</div>
</div>
<div class="pfc__actions">
<a class="pbc pbc--solid pbc--rounded" href="/checkout">Add to cart</a>
</div>
</div>
</div>
</section>
<script>
/* Update summary and total when any input changes.
Read current values, look up price deltas, and write the new total. */
(function () {
var section = document.querySelector('.pfc');
var totalEl = section.querySelector('.pfc__total-price');
var BASE_PRICE = 1499;
var DELTAS = { ram: { '8gb': 0, '16gb': 200, '32gb': 500 },
storage: { '256gb': 0, '512gb': 100, '1tb': 250 } };
section.addEventListener('change', function () {
var total = BASE_PRICE;
Object.keys(DELTAS).forEach(function (name) {
var el = section.querySelector('[name="' + name + '"]:checked') ||
section.querySelector('[name="' + name + '"]');
if (el) total += DELTAS[name][el.value] || 0;
});
totalEl.textContent = '$' + total.toLocaleString();
});
}());
</script>Customization
Override these CSS custom properties to integrate the component into your brand.
.pfc {
--pfc-bg: var(--brand-surface, #ffffff);
--pfc-text-color: var(--brand-ink, #102542);
--pfc-muted-color: rgba(16, 37, 66, 0.55);
--pfc-accent: var(--brand-accent, #1e5fcf);
--pfc-group-bg: rgba(0, 0, 0, 0.025);
--pfc-option-border: rgba(0, 0, 0, 0.12);
--pfc-option-bg-selected: rgba(30, 95, 207, 0.07);
--pfc-preview-bg: rgba(0, 0, 0, 0.03);
--pfc-summary-bg: rgba(0, 0, 0, 0.03);
--pfc-price-color: var(--brand-accent, #1e5fcf);
--pfc-radius: 0.875rem;
--pfc-padding: 2rem;
--pfc-gap: 1.25rem;
}