Recipes
Patterns recipes
Each component page documents itself. This page is for combos: real flows that wire 3-4 components together. Copy a recipe, swap the data, ship it.
On this page
Recipes
- Confirm-and-delete flow — Dialog + Form + Toast
- Searchable select — Combobox inside Field
- Tabbed settings with URL hash — Tabs + lazy load + history
- CRUD admin row — Table + Menu + Dialog + Toast
- Auth flow — Form + Field + Validation + Button
Recipe 1
Confirm-and-delete flow
The single most common destructive flow: user clicks Delete on a row, an alert dialog confirms, and after success a Toast confirms (with an Undo affordance for graceful recovery).
Uses ren-alert-dialog ren-button ren-toast
<!-- 1. Trigger -->
<button class="ren-btn ren-btn-danger" type="button"
data-dialog-trigger="dlg-confirm-delete">Delete project</button>
<!-- 2. Confirm dialog (alert variant: no backdrop dismissal) -->
<ren-alert-dialog id="dlg-confirm-delete"
aria-labelledby="dlg-confirm-delete-title">
<dialog class="ren-dialog ren-alert-dialog">
<div class="ren-dialog-header">
<h2 class="ren-dialog-title" id="dlg-confirm-delete-title">
Delete this project?
</h2>
</div>
<div class="ren-dialog-body">
All files, history, and team access will be removed permanently.
</div>
<div class="ren-dialog-footer">
<button class="ren-btn ren-btn-secondary" type="button"
data-dialog-close>Keep project</button>
<button class="ren-btn ren-btn-danger" type="button"
data-action="confirm-delete" autofocus>Delete</button>
</div>
</dialog>
</ren-alert-dialog>
<script>
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-action="confirm-delete"]');
if (!btn) return;
btn.dataset.loading = '';
try {
const backup = await api.deleteProject(projectId);
document.getElementById('dlg-confirm-delete').close();
// 3. Success toast with undo
toast.success('Project deleted', {
duration: 7000,
action: {
label: 'Undo',
onClick: () => api.restoreProject(backup),
},
});
} catch (err) {
toast.danger('Couldn\'t delete', { description: err.message });
} finally {
delete btn.dataset.loading;
}
});
</script>
Edge cases & gotchas
- Always autofocus the safe button. Here it's Delete because the dialog requires explicit choice — but for ambiguous destructive actions, autofocus Cancel.
- Show the toast after the dialog closes. If both are visible at once the focus ring jumps confusingly.
- Undo on a 7s timeout is a hard contract. If your backend can't actually undelete after a minute, don't promise undo — use a non-actionable toast instead.
- Optimistic delete? Remove the row from the UI immediately and call
api.restoreOptimistically(backup)on Undo. Adds complexity but feels instant.
Recipe 2
Searchable select inside a Field
Combobox provides the popover with filterable options, but it doesn't wrap the label / description / error scaffolding. Use Field for that — the same Field you'd use around a plain <input>.
Uses ren-field ren-combobox
<ren-field name="country" required>
<label class="ren-field-label" for="country-cb">Country</label>
<p class="ren-field-description">
Where the team will be billed.
</p>
<ren-combobox id="country-cb" name="country" placeholder="Search countries…">
<div class="ren-combobox-item" data-value="ar">Argentina</div>
<div class="ren-combobox-item" data-value="br">Brazil</div>
<div class="ren-combobox-item" data-value="cl">Chile</div>
<div class="ren-combobox-item" data-value="uy">Uruguay</div>
</ren-combobox>
<p class="ren-field-error" data-for="country"></p>
</ren-field>
Edge cases & gotchas
- Field's
nameand Combobox'snamemust match. Field wires ARIAaria-describedby+aria-invalidby name — mismatch means the error isn't announced. - For async-loaded options, set
asyncon the combobox and callcombo.setItems(arr)from yourren-searchlistener. The Field error scaffolding works the same way. - Don't use Combobox
placeholderas a label. The label inside Field is the accessible name.
Recipe 3
Tabbed settings with URL hash
A settings page where each tab is deep-linkable (#account, #billing, etc.), and Back / Forward navigates between tabs through a small application adapter.
Uses ren-tabs
<ren-tabs aria-label="Account settings">
<div class="ren-tab-list" role="tablist">
<button id="settings-account" class="ren-tab" role="tab" type="button">Account</button>
<button id="settings-billing" class="ren-tab" role="tab" type="button">Billing</button>
</div>
<section class="ren-tab-panel" role="tabpanel">Account settings</section>
<section class="ren-tab-panel" role="tabpanel" hidden>Billing settings</section>
</ren-tabs>
<script type="module">
import '../components/composites/ren-tabs/ren-tabs.js';
await customElements.whenDefined('ren-tabs');
const tabs = document.querySelector('ren-tabs');
const ids = ['settings-account', 'settings-billing'];
function restore() {
const id = location.hash.slice(1);
tabs.selectTabById(ids.includes(id) ? id : ids[0]);
}
restore();
window.addEventListener('hashchange', restore);
window.addEventListener('popstate', restore);
tabs.addEventListener('ren-tab-change', (event) => {
const id = ids[event.detail.index];
if (id && location.hash !== `#${id}`) history.pushState(null, '', `#${id}`);
});
</script>
Edge cases & gotchas
- Hash synchronization is consumer code. Ren10 exposes
selectTabById()andren-tab-change.detail.index; it does not implement a hash attribute or lazy loading. - Triggers and panels pair by document order. Use unique trigger IDs, real buttons, and the documented singular
ren-tabselectors. - For asynchronous content, own loading, error, retry and cancellation states in the application. Preserve form drafts when switching panels.