More @nexa/ui components: Table, Tabs, Dropdown and a reactive search box
Nine purely visual components, and six with real client-side state — without any of the latter needing an island (Step 9). Every example in this chapter is functional exactly as written: each one was tested in a real browser before being written down here.
Table, Badge and Avatar
<div class="nx-table-wrap">
<table class="nx-table nx-table-zebra">
<thead><tr><th>Client</th><th>Status</th></tr></thead>
<tbody>
<tr>
<td><span class="nx-avatar nx-avatar-sm">HM</span> Hector M.</td>
<td><span class="nx-badge nx-badge-success">Paid</span></td>
</tr>
<tr>
<td><span class="nx-avatar nx-avatar-sm">AR</span> Ana R.</td>
<td><span class="nx-badge nx-badge-warning">Pending</span></td>
</tr>
</tbody>
</table>
</div>nx-table-wrap adds horizontal scroll on small screens; nx-table-zebra alternates row backgrounds. nx-badge and nx-avatar are CSS-only — none of the three needs JavaScript yet (that comes in the sort/filter section below).
Breadcrumbs, Alert and Divider
<nav class="nx-breadcrumbs" aria-label="breadcrumb">
<a class="nx-breadcrumbs-link" href="/">Home</a>
<a class="nx-breadcrumbs-link" href="/orders">Orders</a>
<span class="nx-breadcrumbs-link" aria-current="page">#1024</span>
</nav>
<div class="nx-alert nx-alert-warning">
<p class="nx-alert-title">Low stock</p>
<p>Only 3 units left.</p>
</div>
<hr class="nx-divider" />nx-breadcrumbs puts the "/" between steps with plain CSS (::after) — no need to write it by hand. aria-current="page" marks the current step. nx-alert has three color variants (success/warning/danger) via the left border.
Tooltip, Progress and Skeleton
<span class="nx-tooltip">
Hover here
<span class="nx-tooltip-content">Extra info, no JavaScript</span>
</span>
<div class="nx-progress">
<div class="nx-progress-bar" style={`width: ${data.percent}%`}></div>
</div>
<div class="nx-skeleton" style="width: 200px; height: 1rem;"></div>nx-tooltip-content shows on :hover/:focus-within — keyboard accessible, zero lines of JS. nx-progress-bar needs its width set by hand (fixed, or dynamic with data.*). nx-skeleton is an animated placeholder while something loads.
Tabs, no island needed
function selectTab(event) {
ui.selectTab(event);
}
<div class="nx-tab-group">
<div class="nx-tab-list" role="tablist">
<button class="nx-tab" data-tab="uno" aria-selected="true" onClick={selectTab}>Uno</button>
<button class="nx-tab" data-tab="dos" onClick={selectTab}>Dos</button>
</div>
<div class="nx-tab-panel" data-tab="uno">Contenido uno</div>
<div class="nx-tab-panel" data-tab="dos" hidden>Contenido dos</div>
</div>ui.selectTab(event) reads which button was clicked (event.currentTarget) and its data-tab, finds the nearest .nx-tab-group, and does the rest — no need to pass it any element by hand. The same one-line selectTab function works for every button in every tab group on the page.
Accordion, with an optional exclusive mode
function toggle(event) {
ui.toggleAccordionItem(event);
}
<div class="nx-accordion" data-exclusive="true">
<div class="nx-accordion-item">
<button class="nx-accordion-trigger" aria-expanded="false" onClick={toggle}>¿Envían a todo el país?</button>
<div class="nx-accordion-panel" hidden>Sí, a todo el país.</div>
</div>
</div>By default several items can be open at once. With data-exclusive="true" on the containing .nx-accordion, opening one closes the rest — useful for an FAQ where only one answer makes sense to read at a time.
Dropdown, closed by an outside click or Escape
function toggleMenu(event) {
ui.toggleDropdown(event);
}
function pickOption(event) {
ui.selectDropdownOption(event);
}
<div class="nx-dropdown">
<button class="nx-dropdown-trigger" aria-haspopup="listbox" aria-expanded="false" onClick={toggleMenu}>
Elegir talle…
</button>
<ul class="nx-dropdown-menu" role="listbox" hidden>
<li class="nx-dropdown-option" role="option" data-value="s" onClick={pickOption}>S</li>
<li class="nx-dropdown-option" role="option" data-value="m" onClick={pickOption}>M</li>
<li class="nx-dropdown-option" role="option" data-value="l" onClick={pickOption}>L</li>
</ul>
</div>Same as Dialog (Step 7), but without trapping focus — a dropdown doesn't block the rest of the page. Picking an option updates the trigger's text, marks aria-selected on the chosen option, and closes the menu on its own.
A search box inside a table (and sorting it by column)
function filterOrders(event) {
ui.filterTable(document.getElementById("orders"), event.target.value);
}
function sortByColumn(event) {
ui.sortTable(event);
}
<input type="search" onInput={filterOrders} placeholder="Buscar pedido…" />
<table id="orders" class="nx-table">
<thead>
<tr>
<th data-sortable onClick={sortByColumn}>Cliente</th>
<th data-sortable onClick={sortByColumn}>Total</th>
</tr>
</thead>
<tbody>
<tr><td>Beta</td><td>30</td></tr>
<tr><td>Alfa</td><td>10</td></tr>
<tr><td>Gama</td><td>20</td></tr>
</tbody>
</table>ui.filterTable(table, query) hides tbody rows whose text doesn't match — this is the "search inside the table" piece. ui.sortTable(event) sorts by the clicked <th>'s column, toggling ascending/descending, comparing as numbers when both cells are numeric (10 before 20, not "10" before "2" by text order). Both run client-side, over rows the server already rendered — no re-fetch, and no island.
A full page-wide search box, as a reactive component
function onSearchInput(event) {
const input = event.currentTarget;
input.pageSearch ??= ui.createPageSearch(document.getElementById("catalogo"));
input.pageSearch.setQuery(event.target.value);
}
<input type="search" onInput={onSearchInput} placeholder="Buscar en la página…" />
<div id="catalogo">
<article data-searchable>Zapatillas Nezha</article>
<article data-searchable>Camiseta Jade</article>
<article data-searchable>Gorra roja</article>
</div>ui.createPageSearch(container) filters any data-searchable element inside container, with a real Signal from @nexa/reactivity underneath (the same Step 4 of this tutorial) — not a simulation. One real gotcha: the chunk Nexa extracts from onSearchInput is an isolated module, so it can't depend on a variable declared elsewhere in the page (a file-level let doesn't travel with the chunk). That's why the controller is hung off the <input> itself (input.pageSearch), which does persist between one keystroke and the next because it's the same DOM element.
Island, or a ui function? How to decide
If the state is local to one concrete widget and can be modeled as "directly manipulate the DOM that's already on the page" (open/close, mark an active tab, sort/filter already-rendered rows), it's a ui function + onClick/onInput — no Virtual DOM, no re-render, the usual progressive activation. Reserve an island (Step 9) for when you genuinely need a full framework with its own lifecycle (Vue, React) or real composition with deep props/children — never as the default choice for "this button does something". The six components in this chapter (Tabs, Accordion, Dropdown, Table sort/filter, and the page search box) are proof you can go quite far without opening that door.