Forms
Native elements are fully styled (helpers/forms.css), no JS reimplementations. Live example: demo/forms.html.
Base Fields
| Class | Purpose |
|---|---|
.ncss-field | Label+input+hint as a vertical group |
.ncss-label / --required | Field label, --required adds a red * |
.ncss-input / .ncss-select / .ncss-textarea | Consistent look, including :user-invalid error state |
.ncss-field-hint / -error | Help text / error text below a field |
Other Field Types (styled cross-browser)
| Type | Notes |
|---|---|
type="file" | The biggest cross-browser difference of any field type - ::file-selector-button (+ ::-webkit-file-upload-button for older Safari) brings the browser button up to .ncss-btn--secondary looks |
type="color" | Swatch surface via ::-webkit-color-swatch(-wrapper)/::-moz-color-swatch matched to ncss radius, fixed height matching the other fields |
type="date/time/datetime-local/month/week" | Calendar/clock icon follows the theme automatically (color-scheme), no custom adjustment needed |
type="search" | appearance: none needed, otherwise Safari rounds the field fully off on its own |
type="number" | Native spinner arrows deliberately NOT removed - real functionality, no reason to cut it |
<input list> + <datalist> | Native combobox - an editable text field WITH a suggestion list (unlike <select>, which only allows choosing, no free text). .ncss-input applies with no special rule (generic on any <input>) |
<input class="ncss-input" list="cities" placeholder="Choose or type…">
<datalist id="cities">
<option value="Berlin"></option>
<option value="Hamburg"></option>
</datalist>
Styled Dropdowns (opt-in)
The native suggestion list of a <datalist> AND the options popup of a <select> are not stylable via CSS in any browser (checked as of 2026) - for <select> there's now a CSS path via appearance: base-select, which ncss currently only enables in Chrome/Edge 135+ (see Component Reference for why Safari is deliberately excluded), and none at all for <datalist>.
js/combobox.js (opt-in) replaces the native popup look with a real, styled dropdown list in both cases - the original markup stays unchanged (<input list>/<datalist> or <select>/<option>), full native functionality remains without JS (progressive enhancement, no functional loss). For <select> it kicks in exactly where ncss does NOT enable appearance: base-select (Firefox and currently also Safari) - where the native technique already delivers a styled popup (Chrome/Edge), the script does nothing.
<script src="js/combobox.js" defer></script>
Always include <button><selectedcontent> as the first child, even for a plain text select - optional per spec (the browser generates a matching default button otherwise), but it keeps the behavior consistent and predictable across browsers.
<select class="ncss-select">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option>Option A</option>
<option>Option B</option>
</select>
Rich <option> content (natively only with appearance: base-select, replicated by js/combobox.js elsewhere): since the "Customizable Select" extension, <option> may contain arbitrary child elements (icon+label instead of just text), and the same <button><selectedcontent> then also controls how the current selection is shown in its closed state. "The Golden Rule of Customizable Select": text stays mandatory (visible or via .ncss-visually-hidden), icons are an addition, never a replacement.
<select class="ncss-select">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option value="apple">
<span aria-hidden="true">🍎</span>
<span>Apple</span>
</option>
</select>
js/combobox.js replicates the same rich content for the JS fallback path (Firefox and currently also Safari) - <option> child elements are cloned 1:1 into the custom dropdown list instead of just the squashed-together text. Visually identical result, confirmed with real tests in Chromium (native) as well as WebKit and Firefox (both JS fallback).
Live search (opt-in)
components/livesearch.css + js/livesearch.js show results while typing - grouped (e.g. “Pages”, “People”), with the search term highlighted. Unlike js/combobox.js, which filters an existing <datalist>, results come from the server via fetch(). Pattern: WAI-ARIA combobox with listbox - focus stays in the input, aria-activedescendant points to the chosen option, a status message announces the number of results. Without JavaScript it stays a normal search form (submitting goes to the results page).
<form class="ncss-livesearch" action="/search/" role="search" data-ncss-livesearch="/api/search">
<label class="ncss-visually-hidden" for="q">Search</label>
<input id="q" class="ncss-input" type="search" name="q">
<div class="ncss-livesearch-results" hidden></div>
</form>
<script src="js/livesearch.js" defer></script>
Response format of the endpoint (GET /api/search?q=…, already filtered):
{
"groups": [
{ "label": "Pages", "items": [ { "title": "Contact", "url": "/contact/", "text": "Directions and opening hours" } ] }
],
"all": "/search/?q=..."
}
data-ncss-livesearch | Endpoint (required) |
data-param | Name of the search parameter, default q |
data-min / data-delay | Minimum length (default 2) and delay after the last keystroke (default 180 ms); older requests are cancelled via AbortController, responses are cached per query |
data-mode="client" | Endpoint is a STATIC JSON file in the same format (unfiltered): loaded once, filtered in the browser - for small data sets without server logic |
data-text-empty/-all/-status | Override texts (placeholders {q}, {n}); default German/English by <html lang> |
Combines with .ncss-search (expanding field) and .ncss-input-group. Inside a <dialog>, Esc first only closes the result list, not the dialog. After that, browser behaviour applies again: a type="search" field clears its value on the next Esc, only the following Esc closes the dialog. Live: demo.
Checkbox, Radio & Switch
.ncss-choice | Checkbox/radio row - native elements, only accent-color + alignment |
.ncss-switch | Toggle switch built from <input type="checkbox">, redrawn via appearance:none, no JS. Add role="switch" in markup |
.ncss-range | Unified <input type="range">, track/thumb matched |
Grouping & Feedback
fieldset / legend | Fully styled with no dedicated class (like <blockquote>) - replaces the native 3D border |
progress / meter | accent-color for the base color. meter deliberately keeps its own green/yellow/red rating logic instead of the brand color |
output | Highlighted presentation for a computed result |
Input Group
components/input-group.css - multiple form controls visually fused into ONE unit (shared border line instead of a gap, only the outer corners rounded). Covers two cases: field + button (search bar) and multiple fields side by side (e.g. first name/last name). Deliberately NOT called "form group" - too easy to confuse with the already-existing <fieldset>/<legend> grouping (semantics rather than pure styling).
<label class="ncss-visually-hidden" for="search">Search term</label>
<div class="ncss-input-group">
<input class="ncss-input" type="search" id="search" placeholder="Search…">
<button type="submit" class="ncss-btn ncss-btn--primary">Search</button>
</div>
Accessibility: a placeholder is NOT a substitute for a label - it disappears once text is entered, and isn't reliably announced as the field's name by every screen reader. Every field needs a real <label>, visually hidden via .ncss-visually-hidden if needed - as a sibling BEFORE the .ncss-input-group, not nested inside it (otherwise it would incorrectly count as :first-child and the corner rounding would land on the invisible label instead of the first real field). For multiple tightly-fused fields with no room for a per-field sibling label (e.g. first/last name), aria-label directly on the <input> is the right technique, without changing the group's child structure:
<div class="ncss-input-group">
<input class="ncss-input" aria-label="First name" placeholder="First name">
<input class="ncss-input" aria-label="Last name" placeholder="Last name">
</div>
Currently covers .ncss-input/-textarea/.ncss-btn as direct children - not (yet) .ncss-select-wrapper (its visible border sits on the inner <select>, not the wrapper itself).
Buttons
.ncss-btn + --primary/--secondary/--danger. Every variant has REAL :active feedback (not just :hover) - see Touch & Mobile Viewports for the reasoning.
<button class="ncss-btn ncss-btn--primary">Submit</button>