Next: (hygguile components fly-out), Previous: (hygguile components feedback), Up: API
Render a free-text field with a list of suggestions.
The suggestion list is a native <datalist>: the browser draws it
in its own way, filters it as the reader types, and leaves the field
free to accept a value that is not on the list. That is the correct
behaviour for a combobox, and none of it needs a script. A page that
needs a fixed set of choices wants select instead; a page that
needs several values wants multi-select.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
A list of strings or (value . label) pairs.
The initial value.
A hint shown while the field is empty.
Whether the field is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether several values may be entered (the datalist is unchanged; the
field is a normal text input and this only sets multiple).
Whether the control is disabled.
Classes for the field wrapper.
(combobox #:label "Colour" #:name "colour"
#:options '("amber" "asparagus" "jeans-blue"))
Render a date field backed by the browser’s own date control.
The value travels in ISO form (2026-10-11), which is the only
form a <input type="date"> accepts and the only form a server
should have to parse. The browser draws a calendar in the reader’s own
locale, so the page never has to ship a date-picker script or guess at a
format.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value, in ISO form.
The earliest accepted date, in ISO form.
The latest accepted date, in ISO form.
Whether the field is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Classes for the field wrapper.
(date-field #:label "Starts" #:name "start" #:value "2026-10-11")
Render a from/to date pair as one field.
The pair is a <fieldset> with a <legend>, because that is
what a group of related controls is, and each half keeps its own label
and its own id. The names are derived from name, so a server
reads start=...&end=... without the page inventing a second
convention.
Both halves are native date inputs, which means the range is usable with scripting off – a page that wants the second date to follow the first can add that behaviour on top, but it is not required for the field to work.
The group’s legend, e.g. "Stay".
The shared name; the halves become <name>-start and
<name>-end.
The initial start date, in ISO form.
The initial end date, in ISO form.
The first half’s label. Defaults to "From".
The second half’s label. Defaults to "To".
The earliest accepted date for both halves.
The latest accepted date for both halves.
Whether both halves are required.
Optional helper copy for the group.
Optional error message for the group.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether both halves are disabled.
Classes for the fieldset.
(date-range-field #:label "Stay" #:name "stay"
#:start "2026-10-11" #:end "2026-10-14")
Render a file field as a drop zone.
This is file-field with #:dropzone? #t and multi-file
selection on by default. It is still a native file input under a label,
so the picker, the accept filter and the upload work with scripting off;
the drop-zone treatment and the list of chosen names are the
enhancement.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
A native accept list.
Whether several files may be chosen. Defaults to true.
Whether a file is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
The text inside the drop zone.
Classes for the field wrapper.
(dropzone #:label "Attachments" #:name "files" #:accept "application/pdf")
Return the attribute pairs a control should carry for wiring.
Splice them into the control’s attribute list:
`((input (@ ,@(field-control-attributes wiring)
(type "email"))))
The result carries the shared id, the name, the
description and error links, required and aria-invalid
when they apply, and nothing else – a control’s own type, value and
classes are its own.
An invalid control carries both aria-invalid and
aria-errormessage, pointing at the message itself. The two say
different things – that the control is wrong, and which words say why
– and a reader that understands only the first still gets the message
through the description link, because the error is part of it.
Return the aria-describedby value for the control, or #f.
Return the identifier of the field’s error message.
Return the identifier of the field’s helper copy.
Return the identifier the control and its label share.
Return the identifiers and attributes one control shares with its field shell.
The wiring is an association list rather than an opaque object, so it
can be inspected, logged and passed between procedures. Read it through
the accessors below, or hand it to field-control-attributes to
get the attribute pairs a control should carry.
When name and id are both absent, a unique id is generated, so a field can be rendered without a form name (a filters bar, say) and still have a label bound to it.
The form field name.
An explicit identifier; defaults to name.
Optional helper copy, which gets an id and joins the description.
Optional error message, which gets an id and joins the description.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is required.
(field-wiring #:name "email" #:help-text "We never share it.")
Render a file picker, optionally as a drop zone.
The control is a native <input type="file">, so the browser’s own
picker, its filter for accept, its multi-file handling and its
multipart/form-data upload all work without a script. The
visible box is a <label> bound to the input, which means clicking
it opens the picker and a keyboard reader reaches the input itself.
With dropzone? the box is bigger, says it accepts dropped files, and carries the attributes a small script uses to list the chosen names. Dropping is an enhancement: with scripting off the label still opens the picker, and the filename is still shown by the browser.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
A native accept list, e.g. "image/*,.pdf".
Whether several files may be chosen.
An optional capture hint for a camera on a phone.
Whether a file is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Render the larger drop-zone treatment. Defaults to false.
The text inside the drop zone.
Classes for the field wrapper.
(file-field #:label "Avatar" #:name "avatar" #:accept "image/*") (dropzone #:label "Attachments" #:name "files" #:multiple #t)
Render one labelled, described, validated field shell around a control.
The shell owns the things every field needs and every hand-written field
eventually gets wrong: the label is bound to the control’s id, the
helper copy and the error are linked through aria-describedby,
the error is announced with role="alert" and named by
aria-errormessage, and the required state is both visible (a
marker) and spoken (a visually hidden word plus aria-required on
the control).
The shell also states which of the three states the field is in, as a
class on the wrapper – hg-field-invalid, hg-field-valid,
or neither. The classes are what the stylesheet colours the control’s
border with, so a page never has to pick a border colour per state, and
a field that is invalid looks invalid wherever it sits on the page.
control is a procedure of one argument, the wiring, and returns the control’s SXML – a node or a list of nodes. A control that renders more than one element (a select and its tags, an input and a dropzone) returns them all and the shell places them in order.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
Optional helper copy, rendered below the control.
Optional error message, rendered above the help text and announced. It wins over valid when both are given.
#t to mark the field as filled in correctly, or a string to say
so in words as well. Rendered below the control with
role="status".
Whether the field is required. Renders a visible marker and a spoken one.
Render the label visually hidden but still available as the control’s name.
A procedure of one argument (the wiring) returning the control’s nodes.
Classes for the field’s wrapper.
Classes for the label.
Classes for the helper copy.
Classes for the error message.
Classes for the success message.
(form-field #:label "Email"
#:name "email"
#:required #t
#:help-text "We never share it."
#:control
(lambda (w)
`((input (@ ,@(field-control-attributes w)
(type "email")
(class ,(mk-class '(p-3 rounded-xl))))))))
(form-field #:label "Email"
#:name "email"
#:error "Enter an email address."
#:control ...)
Render a text field with something written inside its edges.
An input group is one bordered box that holds the field and the marks that belong to it: the currency before an amount, the domain after a handle, the unit after a quantity. The marks are not part of the value, so a reader never has to type or delete them, and a screen reader reaches the field without walking over them – both sides are decorative text beside the control, not inside it.
prefix and suffix each take a string, which is rendered as the group’s own addon box, or SXML (a node or a list of nodes), which is rendered as written. That is what keeps the door open for a ‘select’ of units on the right of a number, or a ‘button’ on the right of a search.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value.
A hint shown while the field is empty.
The native input type. Defaults to 'text.
What is written before the field: a string, or SXML.
What is written after the field: a string, or SXML.
Whether the field is required.
An optional autocomplete hint.
An optional virtual-keyboard hint.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Whether the control is read-only.
Classes for the field wrapper.
Classes for the input itself, which carries no border of its own.
Classes for the bordered box around the field and its addons.
(input-group #:label "Price" #:name "price" #:type 'number
#:prefix "€" #:suffix "/ month")
Render a multi-value choice control with the chosen values shown as tags.
The control is a native <select multiple>, which is the only
multi-select that works without a script and is announced correctly by
assistive technology. The tags above it are a *second view* of the same
selection, not a replacement: they show what is chosen without opening
the menu, and, when remove-href is given, each tag is a link that
removes that value – a plain navigation, so removal works with
scripting off too.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
A list of strings or (value . label) pairs.
The values that start selected.
How many rows the menu shows. Defaults to the number of options, capped at 8.
Whether at least one value is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
A procedure of one argument (a selected value) returning the URL that
removes it, or #f for tags that are not removable.
The accessible label template for a removal link. Defaults to
"Remove".
Classes for the field wrapper.
(multi-select #:label "Topics" #:name "topics"
#:options '((a . "Accessibility") (s . "SSR"))
#:selected '("ssr"))
Render a numeric field with an optional unit and optional stepper buttons.
The control is a native <input type="number">: the browser’s own
spinner, number keyboard on touch, and native validation all come for
free. With stepper? the field also gets a pair of buttons that
step the value by one step; those are a progressive enhancement
(they carry js-only), so with scripting off the native input is
still the whole control rather than a broken one.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value.
The lowest accepted value.
The highest accepted value.
The increment between accepted values.
An optional unit shown beside the field, e.g. "kg".
A hint shown while the field is empty.
Whether the field is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Add the decrement/increment buttons. Defaults to false.
Classes for the field wrapper.
Classes for the input itself.
(number-field #:label "Seats" #:name "seats" #:min 1 #:max 12 #:stepper? #t)
Render a one-time code field.
The code is one input, not one input per digit. A single field is the
shape the platform expects: autocomplete="one-time-code" lets a
phone offer the code it just received, a paste fills the whole code at
once, and a screen reader reads one value rather than six boxes it has
to count through. The digits are spaced apart by the stylesheet, which
is the part of a six-box field that actually helps.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value.
How many digits the code has. Defaults to 6.
Whether the code is required.
The autocomplete hint. Defaults to "one-time-code".
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Classes for the field wrapper.
Classes for the input itself.
(otp-field #:label "Verification code" #:name "code" #:length 6)
Render a password field with an optional reveal control.
The field is a native <input type="password"> and stays one: the
value is masked by the browser, the right keyboard and autofill
behaviour come with the type, and the form works with scripting off. The
reveal button is a progressive enhancement that flips the field to plain
text while the reader asks it to – it carries js-only, so a page
rendered without scripts shows the field and its label and nothing that
cannot work.
Give the field an autocomplete that matches what it is
("current-password" to sign in, "new-password" to set a
password), which is what lets a password manager fill it, and what tells
a browser offering to generate a password which field it is generating
one for.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value. A page should normally leave this empty.
A hint shown while the field is empty.
Whether the field is required.
The autocomplete hint. Defaults to "current-password".
Render the show/hide control. Defaults to true.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Whether the control is read-only.
Classes for the field wrapper.
Classes for the input itself.
(password-field #:label "Password" #:name "password" #:required #t
#:autocomplete "current-password")
Render a search control as a real search landmark.
The form carries role="search", so assistive technology can jump
straight to it, and the field is a native <input type="search">:
pressing Enter submits, the browser offers its own clear control, and
nothing depends on a script. The submit button is a real submit button,
so the form still works when it is the only way to send the query.
The field’s name. Defaults to "Search".
The query parameter name.
An explicit identifier; defaults to name.
The current query, so a results page shows what was searched for.
A hint shown while the field is empty.
The URL the search posts to.
'get (the default) or 'post.
The submit button’s label, or #f for a field that submits on
Enter only.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether a query is required.
Hide the label visually while keeping it as the field’s name. Defaults to true, which is what a compact search bar wants.
Classes for the search form.
(search-field #:name "q" #:action "/search" #:value "cozy")
Render a range slider that prints the value it is at.
The control is a native <input type="range">: it drags, it takes
arrow keys, and it submits a number with no script. The number beside
it is an <output> bound to the field, which the browser reads out
as the field’s result; a small script keeps it in step while the reader
drags, and without one the number is simply the value the page rendered.
Always give a slider a start value and, where it matters, a scale worth reading: a reader cannot see the positions between the ends, so the number is the only place the current value is stated in words.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The value it starts at. Defaults to 50.
The lowest value. Defaults to 0.
The highest value. Defaults to 100.
The increment. Defaults to 1.
An optional unit printed beside the number, e.g. "°C".
Whether a value is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Classes for the field wrapper.
Classes for the slider itself.
(slider #:label "Warmth" #:name "warmth" #:value 21 #:min 16 #:max 26
#:unit "°C")
Render a numeric field with the decrement/increment buttons already on.
This is number-field with #:stepper? #t, named for the
thing a page asks for. The buttons are an enhancement over the native
input, so the control keeps working with scripting off.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value.
The lowest accepted value.
The highest accepted value.
The increment. Defaults to 1.
An optional unit shown beside the field.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the field is required.
Whether the control is disabled.
Classes for the field wrapper.
(stepper #:label "Quantity" #:name "qty" #:value 1 #:min 1 #:max 9)
Render a single-line text field through the field shell.
‘input’ is still the right component when a page does not need the shell’s wiring (an inline search box, a bare numeric filter); this one is for a form, where the label, the hint, the error and the required state have to agree with every other field beside it.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value.
A hint shown while the field is empty.
The native input type. Defaults to 'text.
Whether the field is required.
An optional autocomplete hint.
An optional virtual-keyboard hint.
An optional validation pattern.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Whether the control is read-only.
Classes for the field wrapper.
Classes for the input itself.
(text-field #:label "Email" #:name "email" #:type 'email #:required #t)
Render a time field backed by the browser’s own time control.
The value is the ISO time form (09:30, or 09:30:00 with
seconds), and the browser draws it in the reader’s locale. step
is in seconds, which is how a time input says whether it wants minutes
or seconds.
The field’s visible name.
The form field name.
An explicit identifier; defaults to name.
The initial value, in ISO time form.
The earliest accepted time.
The latest accepted time.
The increment between accepted times, in seconds.
Whether the field is required.
Optional helper copy.
Optional error message.
#t to mark the field as filled in correctly, or a string to say
so in words as well.
Whether the control is disabled.
Classes for the field wrapper.
(time-field #:label "Starts at" #:name "starts-at" #:step 900)
Return the errors among checks, in the order they were given.
checks is a list of (field-id . message) pairs in which
the message is #f for a field that passed – the shape a
predicate that answers with the words to show produces, so a server can
write its rules once and read the failures off them:
(validation-errors
(list (cons "email"
(and (not (valid-email? email))
"Enter an email address."))
(cons "password"
(and (< (string-length password) 12)
"Use at least 12 characters."))))
;; => (("email" . "Enter an email address."))
The result is exactly what validation-summary takes, and an empty
result means the form is fine: a page never has to remember which of its
checks failed, or in what order to show them.
A pair whose first element is #f is a problem with the form
rather than with one field, and is listed without a link.
Render the form-level summary of what went wrong, at the top of a form.
A field’s own error message is announced when the reader reaches the field; a summary is announced when the page loads, and it lists every problem in one place with a link to each. That is the pattern WCAG’s error identification asks for, and it is the difference between a form a reader has to hunt through and a form that says what to fix.
errors is a list of (field-id . message) pairs, which is
also the shape a server already has when it re-renders a form it refused
– validation-errors builds one from the rules themselves. The
summary renders nothing at all when the list is empty, so a page can
render it unconditionally.
A list of (field-id . message) pairs; an empty list renders
nothing.
The summary’s heading. Defaults to "There is a problem".
An explicit identifier; defaults to "hg-validation-summary".
Ask the page’s script to put focus on the summary when it loads, which is where a reader should start after a refused submit. Off by default: a page that renders a summary for reasons of its own does not get to move the reader’s focus.
Classes for the summary box.
(validation-summary #:errors '(("email" . "Enter an email address")
("password" . "Enter your password")))
Next: (hygguile components fly-out), Previous: (hygguile components feedback), Up: API