Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

unselect2

A dropdown with no select element in it at all.

Live demo

What it is

The name is the spec. unselect2 renders a styleable dropdown and the rendered DOM contains zero select elements — not a visible one, not a hidden one, not an aria-hidden one, not one per field. Nothing is built from a native control and then hidden, because there is none to build from: the markup you author declares its options directly, and the library renders the control plus one <input type="hidden"> per field, which is what posts.

This is a return to what the project was before 2017. The original was a jQuery page with no native control anywhere: a layer showed the chosen text, a panel of rows sat under it, and clicking a row wrote that row's value into a hidden input. That mechanism is back. What is not back are the original's limits.

What changed, and why

For a while this project did the opposite thing — it enhanced a native select, leaving the real element in the DOM, clipped to 1×1 and marked aria-hidden, to keep native form semantics. That kept submission and constraint validation for free, and it was defensible, but it meant the library called unselect2 rendered one select per field. It now renders none.

What it costs

Removing the native element is not free. Three things go, and they are stated here rather than discovered later:

  1. Native constraint validation is gone. required is this library's check and your server's. The browser enforces nothing. The client check is a convenience, not a boundary — validate on the server.
  2. With JavaScript off there is no field at all. The authored markup is inert <div>s until the script runs, and nothing is submitted for them. There is no degraded mode; there is no mode.
  3. On touch the platform picker never appears. No iOS wheel, no Android sheet — this panel is what a phone gets.

What it keeps

Everything the enhancement-era version gained, none of which needed a native element:

  • ARIA 1.2 select-only combobox semantics — role="combobox" with aria-expanded, aria-activedescendant, and announced state.
  • Full keyboard support — ↓ ↑ Home End PageUp PageDown Enter Esc, plus type-ahead that cycles on a repeated letter.
  • Grouped options, announced through role="group" and aria-labelledby.
  • Disabled options that are rendered and announced but never selectable, and are skipped by every movement key.
  • Themeable through CSS custom properties, inheriting a host design system when one is present.

Install

Copy unselect2.css and unselect2.js into your project.

<link rel="stylesheet" href="unselect2.css">
<script src="unselect2.js" defer></script>

unselect2.js is a classic script, not an ES module. It defines one global, window.Unselect2, and re-running it is a no-op.

Usage

Author a container with data-unselect2. Everything with that attribute is mounted automatically on DOMContentLoaded.

<label for="city-combobox">City</label>
<div data-unselect2 data-name="city" data-placeholder="Select a city" data-required>
  <div data-value="1">Greater Metropolitan Los Angeles</div>
  <div data-value="2">New York</div>
  <div data-group="Europe">
    <div data-value="uk">United Kingdom</div>
    <div data-value="pl" data-disabled>Poland — at capacity</div>
  </div>
</div>

That renders a combobox, a listbox, and <input type="hidden" name="city">. The hidden input is the only thing that posts.

Container attributes

attribute meaning
data-unselect2 mount this container
data-name the submitted field name — required
data-placeholder shown before a choice is made
data-required the field must be answered
data-selected a value to preselect, for a server re-render

Row attributes

attribute meaning
data-value the value posted when this row is chosen
data-disabled shown and announced, never selectable
data-group on a wrapper: its rows form a labelled group

Labels

The generated button's id is <name>-combobox, so a plain <label for="city-combobox"> works and the browser handles click-to-focus itself.

API

Unselect2.mount(elOrSelector)   // mount one container, returns the instance
Unselect2.mountAll(root)        // mount every [data-unselect2] under root
Unselect2.get(elOrSelector)     // the instance for a container, or null
Unselect2.validate(form)        // the invalid fields in a form, and paint them

An instance exposes value (get/set), isValid(), and destroy().

Events

change and input are dispatched on the hidden input and bubble, so a form-level listener sees them exactly as it would on an ordinary field. unselect2:change, unselect2:open and unselect2:close are dispatched on the container.

Validation

The library binds one submit listener per form. If a data-required field is empty it calls preventDefault(), marks the field with aria-invalid and the us2--invalid class, and focuses the first offender. The message text is yours — the library marks, your page says why.

Tests

node test/no-select.mjs

Fails if a select element is authored in any page, if the library gains a code path that could create or find one, or if the stylesheet still carries a rule that only made sense while one existed. It carries its own self-test, so it cannot pass by silently stopping to match.

It is a source check — this repo has no dependencies and so no browser to render in. The rendered-DOM assertion lives with a consumer that has one: pigfox2's Playwright suite counts document.querySelectorAll('select') on the live page across every field and requires zero.

License

MIT.

About

Accessible, dependency-free vanilla-JS enhancement for the native <select> element. No jQuery, no build step, two files.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages