Skip to content
NativeCSS
Docs contents

Forms

Native elements are fully styled (helpers/forms.css), no JS reimplementations. Live example: demo/forms.html.

Base Fields

ClassPurpose
.ncss-fieldLabel+input+hint as a vertical group
.ncss-label / --requiredField label, --required adds a red *
.ncss-input / .ncss-select / .ncss-textareaConsistent look, including :user-invalid error state
.ncss-field-hint / -errorHelp text / error text below a field

Other Field Types (styled cross-browser)

TypeNotes
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-livesearchEndpoint (required)
data-paramName of the search parameter, default q
data-min / data-delayMinimum 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/-statusOverride 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-choiceCheckbox/radio row - native elements, only accent-color + alignment
.ncss-switchToggle switch built from <input type="checkbox">, redrawn via appearance:none, no JS. Add role="switch" in markup
.ncss-rangeUnified <input type="range">, track/thumb matched

Grouping & Feedback

fieldset / legendFully styled with no dedicated class (like <blockquote>) - replaces the native 3D border
progress / meteraccent-color for the base color. meter deliberately keeps its own green/yellow/red rating logic instead of the brand color
outputHighlighted 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>