|
HTML5 · native controls
Input typesOne attribute — Every HTML5 input type is still an Types like Type → what the browser gives you
Watch out: Live demo — try each control Markup <input type="email" placeholder="you@site.com">
<input type="tel" inputmode="tel">
<input type="number" min="0" max="100" step="5">
<input type="date"> <!-- native date picker -->
<input type="color"> <!-- native colour picker -->
<input type="file"> <!-- OS file dialog -->
<!-- Unknown types degrade to text automatically -->
HTML5 · declarative rules
Validation attributesDescribe the rules in the markup; the browser enforces them and messages the user. ↗ MDN reference Constraints live on the element as attributes: CSS can react to constraint state through pseudo-classes: Put Remember: client-side validation is a UX convenience, never a security boundary. Anything enforced here must be re-checked on the server. Live demo — blur a field, then Validate Markup <form novalidate>
<input required minlength="3">
<input type="email" required>
<input type="number"
min="1" max="10" step="1">
<input pattern="[0-9]{5}"
title="Five digits">
</form>
/* only flag fields the user has touched */
input:user-invalid { border-color: red; }
JavaScript · programmatic validity
Constraint Validation APIThe JS layer for rules HTML can't express — cross-field checks and server answers. ↗ MDN reference Every form control exposes a small API: The key method is The classic bug: forgetting to clear a custom error. Once you call Live demo
Taken:
admin, ada, root. Server check simulated on blur.
Markup // cross-field rule
confirm.addEventListener('input', () => {
confirm.setCustomValidity(
confirm.value === pw.value
? '' // valid — always clear!
: 'Passwords must match'
);
});
// server-derived rule (htmx or fetch)
const taken = await isTaken(name.value);
name.setCustomValidity(taken ? 'Taken' : '');
// inspect why it failed
name.validity.customError; // true
HTML5 · native autocomplete
Datalist autocompleteSuggestions attached to a normal input — the user can pick one or type their own. ↗ MDN reference A The crucial difference from Watch out: you can't style the dropdown, and filtering/positioning varies slightly between browsers. For a large or server-driven list, repopulate the Live demo — start typing a language Free text is allowed — you are not limited to the list.
Markup <input list="lang-list" name="lang">
<datalist id="lang-list">
<option value="JavaScript">
<option value="Python">
<option value="PHP">
</datalist>
<!-- suggestions, not a whitelist: -->
<!-- any typed value is still accepted -->
HTML5 · sliders & live results
Range & outputA slider for fuzzy bounded values, and a semantic element to show the result. ↗ MDN reference
Two events drive the update, and the difference matters: Tip: the thumb and track are styled through vendor pseudo-elements ( Live demo Drag and release to “save”.
Markup <label>Budget
<output for="b">$50</output>
</label>
<input id="b" type="range"
min="0" max="500" step="10"
oninput="out.value='$'+this.value">
// input = every pixel of the drag
// change = once, on release (cheap!)
HTML5 · semantic gauges
Meter & progressTwo purpose-built bars that mean different things — pick the right one. ↗ MDN reference
Rule of thumb: if the number could go down again (a level, a score), it's a Live demo value = 30 / 100 · low 35, high 70, optimum 90
Markup <!-- measurement in a range -->
<meter min="0" max="100"
low="35" high="70"
optimum="90" value="30"></meter>
<!-- task completion -->
<progress max="100" value="60"></progress>
<!-- no value = indeterminate spinner -->
<progress></progress>
HTML5 · files in the browser
File APIAccept, filter, and preview uploads on the client before anything is sent. ↗ MDN reference On To show a preview without uploading, read the file locally: Security: Live demo — files stay in your browser ?No image selected
Markup <input type="file"
accept="image/*">
<input type="file"
accept=".pdf,.png,.jpg" multiple>
// preview without uploading
const f = input.files[0];
const r = new FileReader();
r.onload = e => img.src = e.target.result;
r.readAsDataURL(f);
// f.name, f.size, f.type are available now
HTML5 · grouping & disclosure
Structure & groupingNative elements that group controls and hide detail — no framework, no JS. ↗ MDN reference
Nice touch: you can restyle the triangle marker with Live demo Advanced optionsNative disclosure — this content is hidden until toggled, with no JavaScript. Add the
open attribute to start expanded.Markup <fieldset disabled> <!-- disables all -->
<legend>Notifications</legend>
<input type="checkbox"> Email
<select>…</select>
</fieldset>
<details>
<summary>Advanced options</summary>
<p>Hidden until toggled — no JS.</p>
</details>
htmx · search-as-you-type
Active searchQuery the server on each keystroke — debounced — and swap in the results. ↗ htmx.org/examples/active-search This is the htmx take on autocomplete when the data lives on the server. The input fires a request as the user types and htmx drops the server's HTML into a results container — no JSON, no client-side template. The whole behaviour is four attributes: The trigger is what makes it feel right: Why server-side: the list can be huge, permission-filtered, or ranked by logic you don't want in the browser. htmx keeps the matching where the data is and ships back ready-to-render HTML. Live demo — type “java”, “design”, “data”… htmx markup <input type="search" name="q"
hx-post="/search/skills"
hx-target="#results"
hx-swap="innerHTML"
hx-trigger="input changed delay:200ms"
hx-sync="this:replace">
<div id="results">
<!-- server renders matched rows -->
</div>
htmx · dependent dropdowns
Cascading selectsChanging the first control asks the server to repopulate the second. ↗ htmx.org/examples/value-select When one choice determines the next — country → city, category → product, project → task — you don't ship every combination to the browser. The parent control fires Compared with doing this in JavaScript, the win is that the child data can be large, live, or access-controlled — the server decides what a given user is even allowed to see, and the browser only ever holds the relevant slice. Because it's a Pattern note: return an empty/placeholder child state when the parent is cleared, and disable the child until a parent value exists — otherwise users can type into a control that has nothing behind it yet. Live demo — pick a country first htmx markup <select name="country"
hx-get="/cities"
hx-target="#city-list"
hx-trigger="change">
<option>United Kingdom</option>
</select>
<input list="city-list">
<datalist id="city-list">
<!-- server returns options -->
</datalist>
htmx · validate → indicate → swap
Submit → progress → swapThe full round-trip: check locally, post, show activity, replace the form with the result. ↗ htmx.org docs A good submit does three things in order. First, validate on the client with While the request is in flight, htmx toggles the Belt and braces: the client check is for speed and friendliness; the server still validates everything and, on failure, returns the form with error messages for htmx to swap back in. Live demo Live payload (FormData → JSON) { }
htmx markup <form
hx-post="/signup"
hx-target="this"
hx-swap="outerHTML"
hx-indicator="#bar"
hx-disabled-elt="find button">
<input name="email" type="email" required>
<button>Create account</button>
<progress id="bar"
class="htmx-indicator"></progress>
</form>
<!-- server returns the success card; -->
<!-- htmx swaps it in over the form. -->
|
|||||||||||||||||||||