Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
An unhandled error has occurred.
Sedna.UI
v0.17.0 · main

Selects

tier 2 — classes

.form-select is the whole class list a dropdown needs — it carries the same field shape .form-input does, from one rule the two names share. It styles the panel as well as the box, so the option list takes the theme.

Two selects behind one class appearance: base-select takes the option list out of the platform's hands and makes it a real panel in the page. A browser without it keeps the platform list and gets a caret painted from two gradients instead — the two halves are exclusive, behind @supports, and neither is a script or a shim. A list box has no caret either way, since there is nothing to drop.

The drop-down

A plain <select class="form-select">. Nothing has to be added to the markup.

A closed select is one row tall whatever the option says.

<div class="sedna-grid sedna-grid--sm sedna-grid--fit" style="max-width:620px">
    <div class="form-field">
        <label class="form-label" for="f-env">Target environment</label>
        <select class="form-input form-select" id="f-env">
            <option>Production</option>
            <option selected>Staging</option>
            <option>Development</option>
        </select>
    </div>
    <div class="form-field">
        <label class="form-label" for="f-window">Maintenance window</label>
        <select class="form-input form-select" id="f-window">
            <option>Immediately</option>
            <option selected>Tonight, 02:00–04:00 UTC</option>
            <option>Next Sunday, 02:00–06:00 UTC</option>
        </select>
        <p class="form-hint">A closed select is one row tall whatever the option says.</p>
    </div>
</div>

Options with content

An <option> may hold real markup — an icon, a second line, a trailing code. Add a <button> holding <selectedcontent></selectedcontent> as the select's first child and the closed box shows a clone of whichever option is chosen. Leave it out and the browser supplies its own, which runs every text node together. A browser without base appearance — Safari — shows the option's whole text in the list and in the closed box, so the .option-sep between label and hint is what keeps it readable there: Frankfurt · eu-central-1. Base appearance hides it.

The clone is the browser's to make and Blazor's render does not trigger it, so Sedna.UI.js repairs it on load and after every render — sednaUi.select.refresh() on the script page. The submitted value is the option's text run together and trimmed, so keep value explicit.

The hint is the region code, so the row reads the way the console does.

The closed box drops the second line, so the field stays one row tall.

<div class="sedna-grid sedna-grid--sm sedna-grid--fit" style="max-width:680px">
    <div class="form-field">
        <label class="form-label" for="f-region">Deployment region</label>
        <select class="form-select" id="f-region">
            <button>
                <selectedcontent></selectedcontent>
            </button>
            <optgroup label="Europe">
                <legend>Europe</legend>
                <option value="eu-west-1">
                    <i class="ri-global-line" aria-hidden="true"></i>Ireland<span class="option-sep"> · </span><span class="option-hint">eu-west-1</span>
                </option>
                <option value="eu-central-1" selected>
                    <i class="ri-global-line" aria-hidden="true"></i>Frankfurt<span class="option-sep"> · </span><span class="option-hint">eu-central-1</span>
                </option>
            </optgroup>
            <hr>
            <optgroup label="North America">
                <legend>North America</legend>
                <option value="us-east-1">
                    <i class="ri-global-line" aria-hidden="true"></i>N. Virginia<span class="option-sep"> · </span><span class="option-hint">us-east-1</span>
                </option>
            </optgroup>
        </select>
        <p class="form-hint">The hint is the region code, so the row reads the way the console does.</p>
    </div>
    <div class="form-field">
        <label class="form-label" for="f-rollout">Rollout strategy</label>
        <select class="form-select" id="f-rollout">
            <button>
                <selectedcontent></selectedcontent>
            </button>
            <option value="canary" selected>
                <i class="ri-flight-takeoff-line" aria-hidden="true"></i>
                <span class="option-text">Canary<span class="option-sep"> · </span><span class="option-hint">5% of traffic for 30 minutes, then the rest</span></span>
            </option>
            <option value="blue-green">
                <i class="ri-swap-box-line" aria-hidden="true"></i>
                <span class="option-text">Blue / green<span class="option-sep"> · </span><span class="option-hint">Switch the whole fleet in one step</span></span>
            </option>
            <option value="rolling" disabled>
                <i class="ri-loop-right-line" aria-hidden="true"></i>
                <span class="option-text">Rolling<span class="option-sep"> · </span><span class="option-hint">Unavailable while the queue is draining</span></span>
            </option>
        </select>
        <p class="form-hint">The closed box drops the second line, so the field stays one row tall.</p>
    </div>
</div>

Groups and separators

Give a group a <legend> as well as its label. The attribute's heading is drawn by the browser outside this document and cannot be themed — it is what a fallback browser shows, and the <legend> is what this one does.

The legend is what the panel draws; the label is what a fallback browser draws. Write both.

<div style="max-width:420px">
    <div class="form-field">
        <label class="form-label" for="gr-region">Deployment region</label>
        <select class="form-select" id="gr-region">
            <button>
                <selectedcontent></selectedcontent>
            </button>
            <optgroup label="Europe">
                <legend>Europe</legend>
                <option value="eu-west-1">Ireland</option>
                <option value="eu-central-1" selected>Frankfurt</option>
            </optgroup>
            <hr>
            <optgroup label="North America">
                <legend>North America</legend>
                <option value="us-east-1">N. Virginia</option>
                <option value="us-west-2">Oregon</option>
            </optgroup>
        </select>
        <p class="form-hint">The <code>legend</code> is what the panel draws; the <code>label</code> is what a fallback browser draws. Write both.</p>
    </div>
</div>

List box and multiple

Two different controls the CSS treats alike: multiple takes several answers, size on its own still takes one and merely shows the options without opening.

A chosen row is a brand tint as well as brand text, and the tick is on the trailing edge. All three matter more here than in a dropdown: several rows are chosen at once, so the set of them has to be visible as a set — and colour alone carries nothing for a reader going by lightness. Pointing at a chosen row deepens the tint instead of replacing it with the neutral hover, which used to erase the state under the pointer.

multiple — several answers, and the box says so by showing them all ticked.

size without multiple — one answer, shown as a list because scanning beats opening.

<div class="sedna-grid sedna-grid--sm sedna-grid--fit" style="max-width:620px">
    <div class="form-field">
        <label class="form-label" for="lb-groups">Notify groups</label>
        <select class="form-input form-select" id="lb-groups" multiple size="4">
            <option selected>Fulfilment</option>
            <option>Network</option>
            <option selected>On call</option>
            <option>Warehouse</option>
            <option>Finance</option>
        </select>
        <p class="form-hint"><code>multiple</code> — several answers, and the box says so by showing them all ticked.</p>
    </div>
    <div class="form-field">
        <label class="form-label" for="lb-region">Region</label>
        <select class="form-input form-select" id="lb-region" size="4">
            <option>eu-central-1</option>
            <option selected>eu-west-1</option>
            <option>us-east-1</option>
            <option>ap-south-1</option>
        </select>
        <p class="form-hint"><code>size</code> without <code>multiple</code> — one answer, shown as a list because scanning beats opening.</p>
    </div>
</div>

Sections in a list box

The same <optgroup> plus <legend> as a dropdown, which is worth more in a list box: the whole list is on screen, so the headings are what stop forty rows reading as one column. Mind the arithmetic — size counts rows and a heading is a row.

size counts rows, and a <legend> is a row. Seven here shows three headings and four options; ask for the headings as well as the options you want on screen.

<div class="form-field" style="max-width:340px">
    <label class="form-label" for="lb-sections">Escalate to</label>
    <select class="form-input form-select" id="lb-sections" multiple size="7">
        <optgroup label="On call">
            <legend>On call</legend>
            <option selected>Duty engineer</option>
            <option>Duty lead</option>
        </optgroup>
        <optgroup label="Teams">
            <legend>Teams</legend>
            <option>Fulfilment</option>
            <option selected>Network</option>
            <option>Warehouse</option>
        </optgroup>
        <optgroup label="Off rota">
            <legend>Off rota</legend>
            <option disabled>Finance &mdash; no rota this week</option>
            <option>Facilities</option>
        </optgroup>
    </select>
    <p class="form-hint">
        <code>size</code> counts rows, and a <code>&lt;legend&gt;</code> is a row. Seven here shows
        three headings and four options; ask for the headings as well as the options you want on
        screen.
    </p>
</div>

Rows with content

.option-text and .option-hint, exactly as in the dropdown above. A list box keeps the second line where the closed box drops it, because there is no closed box — which is the case for choosing a list box over a dropdown when each option needs a sentence to tell it from its neighbour.

The same .option-text and .option-hint a dropdown uses. A two-line row is taller than the row height size divides by, so set size to roughly half the rows you want on screen — here six shows three.

<div class="form-field" style="max-width:420px">
    <label class="form-label" for="lb-rich">Escalation policy</label>
    <select class="form-input form-select" id="lb-rich" multiple size="6">
        <option selected>
            <i class="ri-phone-line" aria-hidden="true"></i>
            <span class="option-text">Call the duty engineer<span class="option-sep"> · </span><span class="option-hint">Rings the rota phone, then the lead after 5 minutes</span></span>
        </option>
        <option>
            <i class="ri-mail-send-line" aria-hidden="true"></i>
            <span class="option-text">E-mail the team<span class="option-sep"> · </span><span class="option-hint">One message to the group address, no acknowledgement</span></span>
        </option>
        <option selected>
            <i class="ri-notification-3-line" aria-hidden="true"></i>
            <span class="option-text">Push to the on-call app<span class="option-sep"> · </span><span class="option-hint">Repeats every 2 minutes until someone accepts</span></span>
        </option>
        <option disabled>
            <i class="ri-chat-3-line" aria-hidden="true"></i>
            <span class="option-text">Post to the channel<span class="option-sep"> · </span><span class="option-hint">Unavailable &mdash; no channel is connected</span></span>
        </option>
    </select>
    <p class="form-hint">
        The same <code>.option-text</code> and <code>.option-hint</code> a dropdown uses. A two-line
        row is taller than the row height <code>size</code> divides by, so set <code>size</code> to
        roughly half the rows you want on screen &mdash; here six shows three.
    </p>
</div>

Moving rows between two lists

Live — pick a group and press the arrow. For choosing a set out of a long list, where a column of checkboxes is unreadable and a multiple select alone never answers what have I chosen so far. The right-hand list is that answer, and it is an ordinary multiple select, so a form posts it with no help.

.transfer is the three-column grid and .transfer-actions the column of buttons; data-transfer plus data-transfer-from / -to / -add / -remove is what sednaUi.transfer reads. Both selects get a bubbling change after every move — which is all a Blazor @onchange needs — and insertion is alphabetical only where the destination was already sorted: re-ordering a list whose order means something would destroy it silently.

Selection is left to the platform, and that is the point: a plain click takes one row and drops the previous, Ctrl or Shift extends. Here selection is a staging act — these are the ones I am about to move — and the answer is which list a row ends up in. So nothing arrives selected and the destination's own selection is cleared: rows that landed selected accumulated into a highlight the reader never made, and the next Remove acted on all of it. The first row moved is scrolled into view instead.

Pick one or more, then Add. A double-click moves a single row.

This is the answer the form posts. The left list posts nothing.

<div class="transfer" data-transfer style="max-width:640px">
    <div class="form-field">
        <label class="form-label" for="tr-available">Available groups</label>
        <select class="form-input form-select" id="tr-available" multiple size="7" data-transfer-from>
            <option>Facilities</option>
            <option>Finance</option>
            <option>Network</option>
            <option>Procurement</option>
            <option>Security</option>
            <option>Warehouse</option>
        </select>
        <p class="form-hint">Pick one or more, then Add. A double-click moves a single row.</p>
    </div>

    <div class="transfer-actions">
        <button class="btn btn-icon" type="button" data-transfer-add aria-label="Add to notified"
                data-tip="Moves everything selected on the left over to the right.">
            <i class="ri-arrow-right-line"></i>
        </button>
        <button class="btn btn-icon" type="button" data-transfer-remove aria-label="Remove from notified"
                data-tip="Moves everything selected on the right back to the left.">
            <i class="ri-arrow-left-line"></i>
        </button>
    </div>

    <div class="form-field">
        <label class="form-label" for="tr-notified">Notified on escalation</label>
        <select class="form-input form-select" id="tr-notified" multiple size="7" data-transfer-to>
            <option>Fulfilment</option>
            <option>On call</option>
        </select>
        <p class="form-hint">This is the answer the form posts. The left list posts nothing.</p>
    </div>
</div>

Disabled and required

A disabled <option> should say why in its own text, or it reads as a bug. A required select needs an empty first option, or the first real one counts as an answer the reader never gave.

The whole control is disabled — nothing to change here.

One option disabled. Say why in the option, or it reads as a bug.

An empty first option is what makes required bite on a select.

<div class="sedna-col" style="max-width:420px; gap:16px">
    <div class="form-field">
        <label class="form-label" for="ss-plan">Plan</label>
        <select class="form-input form-select" id="ss-plan" disabled>
            <option selected>Enterprise</option>
            <option>Team</option>
        </select>
        <p class="form-hint">The whole control is disabled — nothing to change here.</p>
    </div>
    <div class="form-field">
        <label class="form-label" for="ss-strategy">Rollout</label>
        <select class="form-input form-select" id="ss-strategy">
            <option selected>Canary</option>
            <option>Blue / green</option>
            <option disabled>Rolling — unavailable while the queue drains</option>
        </select>
        <p class="form-hint">One <code>option</code> disabled. Say why in the option, or it reads as a bug.</p>
    </div>
    <div class="form-field">
        <label class="form-label form-label--required" for="ss-team">Escalation team</label>
        <select class="form-input form-select" id="ss-team" required>
            <option value="" selected>Choose a team…</option>
            <option value="fulfilment">Fulfilment</option>
            <option value="network">Network</option>
        </select>
        <p class="form-hint">An empty first option is what makes <code>required</code> bite on a select.</p>
    </div>
</div>

Three sizes

.form-select-sm and -lg — the select's own tier, not the input's, because a painted caret needs its room back at every size.

<div class="sedna-col sedna-gap-2" style="max-width:320px">
    <select class="form-input form-select form-select-sm" aria-label="Small status filter">
        <option>Any status — small</option>
        <option>Failed</option>
    </select>
    <select class="form-input form-select" aria-label="Status filter">
        <option>Any status — default</option>
        <option>Failed</option>
    </select>
    <select class="form-input form-select form-select-lg" aria-label="Large status filter">
        <option>Any status — large</option>
        <option>Failed</option>
    </select>
</div>

Beside a filter field

.sedna-w-fit is what keeps a filter dropdown the width of its value instead of the width of the bar. The heights agree because the tiers do.

<div class="toolbar" style="max-width:620px">
    <div class="toolbar-filters">
        <input class="form-input toolbar-input" type="search" placeholder="Find an order" aria-label="Find an order" />
        <select class="form-input form-select form-select-sm sedna-w-fit" aria-label="Status">
            <option selected>Any status</option>
            <option>Dispatched</option>
            <option>Awaiting stock</option>
        </select>
        <select class="form-input form-select form-select-sm sedna-w-fit" aria-label="Region">
            <option selected>All regions</option>
            <option>EU-West</option>
        </select>
    </div>
    <button class="btn btn-sm btn-primary" type="button"><i class="ri-add-line"></i> New order</button>
</div>