Skip to content

UI State Basic

Basic examples of a UI state component.

Products

Wireless Headphones

$49 / unit

Subtotal:

Phone Stand

$29 / unit

Subtotal:

item(s) ready to checkout

Clear cart

Code

The following example assumes that you have imported the component and set up the theme.

Basic UI State
<!-- Global state -->
<wcp-ui-state global>
<wcp-store type="number" name="$cartCount" value="0"></wcp-store>
<wcp-store type="boolean" name="$showToast" value="false"></wcp-store>
<wcp-store type="string" name="$toastMsg" value=""></wcp-store>
</wcp-ui-state>
<div class="app">
<!-- bind-show: visible only while toast is active -->
<!-- bind-text: message set from JavaScript -->
<div class="toast" bind-show="$showToast" bind-text="$toastMsg"></div>
<main class="main">
<p class="section-title">Products</p>
<div class="products">
<!-- Scoped state: Wireless Headphones -->
<wcp-ui-state>
43 collapsed lines
<wcp-store type="number" name="$qty" value="1"></wcp-store>
<wcp-store type="number" name="$price" value="49"></wcp-store>
<wcp-store type="boolean" name="$inStock" value="true"></wcp-store>
<!-- derive: recomputes whenever $qty or $price changes -->
<wcp-store derive="'$' + ($qty * $price)" name="$subtotal"></wcp-store>
<!-- bind-class-toggle: dims the card when out of stock -->
<article class="card" bind-class-toggle="out-of-stock:!$inStock">
<h2 class="card-title">Wireless Headphones</h2>
<p class="card-price">$49 / unit</p>
<!-- bind-prop: syncs input value with $qty store -->
<!-- bind-event: updates $qty whenever the number input changes -->
<label class="qty-label">
Qty
<input type="number" bind-event="input:$qty(EVENT.target.valueAsNumber)" bind-prop="value:$qty" max="10" min="1" />
</label>
<!-- bind-text: shows the derived subtotal expression -->
<p class="subtotal">
Subtotal:
<strong bind-text="$subtotal"></strong>
</p>
<div class="card-actions">
<!-- bind-attr-toggle: disables button when qty < 1 or out of stock -->
<!-- bind-event (multiple): bumps cart count AND resets qty in one click -->
<button
class="btn-primary"
bind-attr-toggle="disabled:$qty < 1 || !$inStock"
bind-event="click:$cartCount($cartCount + $qty) $qty(1)"
data-product="Wireless Headphones"
>
Add to cart
</button>
<!-- bind-event (.prevent modifier): prevents default anchor navigation -->
<!-- bind-text: label changes based on stock state -->
<a class="btn-secondary" bind-event="click.prevent:$inStock(!$inStock)" href="#toggle-stock">
<span bind-text="$inStock ? 'In stock' : 'Out of stock'"></span>
</a>
</div>
</article>
</wcp-ui-state>
<!-- Scoped state: Phone Stand. Same store names, fully isolated scope. -->
<wcp-ui-state>
34 collapsed lines
<wcp-store type="number" name="$qty" value="1"></wcp-store>
<wcp-store type="number" name="$price" value="29"></wcp-store>
<wcp-store type="boolean" name="$inStock" value="true"></wcp-store>
<wcp-store derive="'$' + ($qty * $price)" name="$subtotal"></wcp-store>
<article class="card" bind-class-toggle="out-of-stock:!$inStock">
<h2 class="card-title">Phone Stand</h2>
<p class="card-price">$29 / unit</p>
<label class="qty-label">
Qty
<input type="number" bind-event="input:$qty(EVENT.target.valueAsNumber)" bind-prop="value:$qty" max="10" min="1" />
</label>
<p class="subtotal">
Subtotal:
<strong bind-text="$subtotal"></strong>
</p>
<div class="card-actions">
<button
class="btn-primary"
bind-attr-toggle="disabled:$qty < 1 || !$inStock"
bind-event="click:$cartCount($cartCount + $qty) $qty(1)"
data-product="Phone Stand"
>
Add to cart
</button>
<a class="btn-secondary" bind-event="click.prevent:$inStock(!$inStock)" href="#toggle-stock">
<span bind-text="$inStock ? 'In stock' : 'Out of stock'"></span>
</a>
</div>
</article>
</wcp-ui-state>
</div>
<!-- bind-show: only visible when cart has items -->
<div class="cart-summary" bind-show="$cartCount > 0">
<p>
<strong bind-text="$cartCount"></strong>
item(s) ready to checkout
</p>
<!-- JavaScript API: clear cart and show toast, triggered on click -->
<!-- bind-event (.prevent): prevents the anchor from navigating -->
<a id="clear-cart" class="btn-clear" bind-event="click.prevent:$cartCount(0)" href="#clear">Clear cart</a>
</div>
</main>
</div>
<script>
18 collapsed lines
// JavaScript API: show a temporary toast whenever an item is added to the cart.
document.addEventListener("DOMContentLoaded", () => {
const globalUiState = document.querySelector("wcp-ui-state[global]");
const $cartCount = globalUiState.getStore("$cartCount");
const $toastMsg = globalUiState.getStore("$toastMsg");
const $showToast = globalUiState.getStore("$showToast");
let timeout;
$cartCount.listen(value => {
$toastMsg.set(value === 0 ? "Cart cleared" : "product added to cart");
$showToast.set(true);
clearTimeout(timeout);
timeout = setTimeout(() => $showToast.set(false), 2500);
});
});
</script>
<style>
175 collapsed lines
.app {
position: relative;
}
.toast {
position: absolute;
inset-block-start: 0;
inset-inline-start: 50%;
z-index: 100;
padding: 10px 22px;
font-size: 0.88rem;
color: var(--sb-color-text);
pointer-events: none;
background: var(--sb-color-gray-6);
border-radius: 24px;
transform: translateX(-50%);
animation: fade-in 0.2s ease;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.main {
max-inline-size: 900px;
padding: 0 24px;
padding-block-start: 1em;
}
.section-title {
margin-block-end: 24px;
font-size: 0.72rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.12em;
}
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.card {
padding: 24px;
background: var(--sb-color-gray-7);
border: 1px solid var(--sb-color-gray-5);
border-block-start: 3px solid var(--sb-color-accent);
border-radius: 4px;
}
.card.out-of-stock {
opacity: 0.55;
}
.card-title {
margin-block: 0 4px;
font-size: 1.1rem;
}
.card-price {
font-size: 0.88rem;
color: var(--sb-color-gray-3);
}
.qty-label {
display: flex;
gap: 12px;
align-items: center;
font-size: 0.85rem;
color: var(--sb-color-gray-3);
}
.qty-label input {
inline-size: 70px;
padding: 6px 10px;
font-size: 0.95rem;
color: var(--sb-color-gray-0);
text-align: center;
background: var(--sb-color-gray-5);
border: 1px solid var(--sb-color-gray-4);
border-radius: 6px;
}
.subtotal {
margin-block-end: 0.5em;
font-family: sans-serif;
font-size: 0.9rem;
color: var(--sb-color-gray-3);
}
.subtotal strong {
color: var(--text);
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.btn-primary {
flex: 1;
padding: 9px 16px;
font-size: 0.88rem;
color: #fff;
cursor: pointer;
background: var(--sb-color-accent);
border: none;
border-radius: 6px;
}
.btn-primary:disabled {
cursor: not-allowed;
opacity: 0.4;
}
.btn-secondary {
padding: 9px 14px;
font-size: 0.82rem;
color: var(--sb-color-gray-3);
cursor: pointer;
background: transparent;
border: 1px solid var(--sb-color-gray-4);
border-radius: 6px;
transition: border-color 0.2s;
}
.btn-secondary:hover {
border-color: var(--muted);
}
.cart-summary {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
padding: 0.5em 1em;
margin-block-start: 1em;
background: var(--sb-color-gray-6);
border: 1px solid var(--sb-color-gray-5);
border-radius: 4px;
}
.cart-summary p {
font-size: 0.95rem;
}
.btn-clear {
display: inline-block;
padding: 8px 18px;
font-size: 0.85rem;
color: var(--sb-color-accent-high);
text-decoration: none;
cursor: pointer;
background: transparent;
border: 1px solid var(--sb-color-gray-4);
border-radius: 6px;
transition:
border-color 0.15s,
color 0.15s;
}
.btn-clear:hover {
color: var(--sb-color-gray-2);
border-color: var(--sb-color-gray-2);
}
</style>