Native controls
Checked against 2994ef5 on 2026-11-10 by Capy (review pass: Subagent 124 and fable). The files it covers are unchanged since.
HTML elements keep their HTML meaning. Where an element’s meaning matches a
native control exactly, niso draws the platform’s control instead of a CSS
box: a switch is a SwiftUI Toggle on Apple platforms and a Material 3
Switch on Android, with its stock look, size and haptics. The page still sees an ordinary
element, with its value, events and layout.
A screen reader can find a native control and operate it, and an activation with no touch reaches the page as a press. It gets no label, though: a button’s text is drawn by niso, and the hosts set no accessibility label, so VoiceOver and TalkBack announce an unnamed button. That’s from the hosts’ code; it hasn’t been tried with a screen reader. Nothing else on the page is exposed (Accessibility).
Which elements
Section titled “Which elements”On main today:
| Element | Apple platforms (SwiftUI) | Android (Compose, Material 3) | Desktop (iced) |
|---|---|---|---|
<input type="checkbox" switch> |
Toggle |
Switch |
toggler |
<input type="range"> |
Slider |
Slider |
slider |
<progress> |
ProgressView, linear |
linear progress indicator | progress_bar |
<button> |
button | Button |
button |
<input> (text, search, email, url, tel, password, number), <textarea> |
TextField; TextEditor for a multi-line field on a Mac |
outlined text field, or a bare BasicTextField |
text_input, text_editor |
Radio group (fieldset or role="radiogroup") |
segmented or menu Picker |
connected button group, or radio button rows | radio buttons |
The core decides what is a control (crates/niso-core/src/controls.rs)
and sends it to the host as a CONTROL or TEXT_FIELD op of the
render stream; the hosts draw them in
Controls.swift,
Controls.kt
and controls.rs.
A control is native only while its appearance is auto, the default. With
appearance: none it’s a plain CSS box you style yourself: that’s the cue niso
takes for “I want my own look”, rather than drawing a lookalike.
Decisions lists the controls that will
be native where the platform has them, and built as composites where it
doesn’t: checkbox, radio, select, menus, date and time pickers, steppers,
dialogs, sheets, popovers, tab bars and the rest of a minimum set. They aren’t
native yet: a <select>, a checkbox or a date input is drawn as a CSS box.
Size and layout
Section titled “Size and layout”A control is laid out like a replaced element: the page places it, and its
size is the native control’s. Hosts measure their own controls at startup and
report the sizes (niso_dom_set_control_size), so a switch takes exactly the
room the platform’s switch needs. Until a host reports them, as when
niso-run runs a page headless, the core uses default sizes from its own
table (crates/niso-core/src/controls.rs: DEFAULT_SIZES).
A button keeps its children: they render as the native button’s label, so a
button can hold an icon and text. An unstyled button gets the host’s stock
button style (padding, height, font, colors and corner radius, in light and
dark, reported with niso_dom_set_button_style), and styling one changes only
what the page set: a fill makes it a filled button, no fill a borderless one.
Text fields work the same way: an unstyled field is the stock one
(niso_dom_set_field_style), and a field whose box the page restyles becomes a
bare editor inside the box the page drew.
Styling
Section titled “Styling”Each control takes a fixed set of CSS properties that map to native modifiers:
colors, border-radius, font, padding and size. Within that set the
control stays native; other properties don’t reach it. accent-color isn’t in
it: stylo drops it (Differences),
and a stock control keeps the platform’s own tint. A control
is drawn dark when its element’s used color scheme is dark, as browsers choose
it: its color-scheme allows dark and the user prefers it.
The control handles its own touches. The host reports a change with
niso_dom_control_input (the new value, and whether the user let go, so a
slider fires input while dragging and change at the end), and then shows
the state the next frame sends, which script may have changed or refused. A
text field reports edits and selection changes (niso_dom_text_edit,
niso_dom_text_select), and an edit a beforeinput listener cancels leaves
the field as it was. Taps on a button are ordinary taps, so they reach
whatever its label holds.
Radio groups as segmented controls
Section titled “Radio groups as segmented controls”A group of radio buttons is one native control: a fieldset or an element
with role="radiogroup" whose radio buttons (two or more) share a name and
that has no other form controls. niso picks the control from the options:
- Segmented: up to 5 options of up to 15 characters each, in a group laid
out as a row (a flex container in a row). A segmented
Pickeron Apple platforms, a connected button group on Android. iced has no segmented control, so on desktop it’s radio buttons. - List: any other group. A menu
Pickeron Apple platforms, a column of radio button rows on Android.
<div class="quality" role="radiogroup" aria-label="Stream quality"> <label><input type="radio" name="quality" value="low"> Low</label> <label><input type="radio" name="quality" value="high" checked> High</label></div>The page places the group, and the control fills its box at the platform’s
height. Styles on the radio buttons and labels never show, since the platform
draws the options, labeled with each radio button’s label text. A group that
holds more than its radio buttons and their labels, such as a <legend> or a
hint, keeps it: the control takes only the radio buttons’ area. A group where
the page hides a radio button to draw its own (opacity: 0,
visibility: hidden) or gives one appearance: none stays plain radio
buttons. In a browser it stays plain radio buttons, so the same page works on
the web. examples/radio uses one.