Form controls
Input, textarea, select and the free-standing hint — the four parts a GRIDD form is assembled from, inside or outside a GdField.
Input
Code
<GdInput v-model="address" />
<GdInput v-model="search" sunken placeholder="Search shed, deck, pool…">
<template #leading><GdIcon :size="15">…</GdIcon></template>
</GdInput>
<GdInput model-value="Not editable" disabled />
<GdInput model-value="Not a lot/plan" invalid />Units & glyphs
Code
<GdInput v-model="area">
<template #trailing>m²</template>
</GdInput>Number
Try typing 0812, then leave the field
model: {"area":812,"storeys":2}
Code
<GdInputNumber v-model="area" unit="m²" :min="1" :max="100000" />
<GdInputNumber v-model="storeys" unit="storeys" :min="1" :max="12" :step="1" />Date & time
Code
<GdInput v-model="date" type="date" :min="noticeStart" :max="noticeEnd" />Textarea
Code
<GdTextarea v-model="notes" :rows="3" />Select
Code
<GdSelect v-model="lga" :options="[
{ value: 'riverton', label: 'Riverton City Council' },
{ value: 'northbank', label: 'Northbank Regional Council' },
{ value: 'eastvale', label: 'Eastvale City Council' }
]" />Options that arrive late
Code
<GdSelect v-model="zone" :options="zones" placeholder="Pick a zone" @open="loadZones" />Search select
Code
<GdSearchSelect
v-model="district"
:options="districts"
eyebrow="District"
search-placeholder="Search 13 districts…"
aria-label="District"
/>
// an option is { value, label, sub?, tag?, keywords?, disabled? }
{ value: 'riverton-central', label: 'Riverton Central',
sub: 'Riverton City Plan 2016', tag: 'RC', keywords: 'cbd downtown' }Hint
We'll email a six-digit code. It expires in ten minutes.
Signing in accepts the terms of use.
This council has not published its overlays yet.
Code
<GdHint>Invite-only during the pilot.</GdHint>
<GdHint tone="quiet">Signing in accepts the terms of use.</GdHint>
<GdHint tone="danger">This council has not published its overlays yet.</GdHint>API
GdInput
Props
modelValuestring | number—`v-model`. No default — an unbound input starts empty.
type"text" | "email" | "url" | "search" | "tel" | "number" | "password" | "date" | "time""text"`date` and `time` use the platform's own picker — a hand-rolled calendar is a project, and the native one is keyboard-complete on day one.
placeholderstring—A hint of format, never information. It is not a label.
disabledbooleanfalseInert and unfocusable.
readonlybooleanfalseFocusable and selectable, not editable. Prefer it to `disabled` for a value the user may want to copy.
invalidbooleanfalseThe error ring. Usually set for you by `GdField`, through the slot props.
size"sm" | "md""md"Two heights only — there is no `lg` input in the system.
sunkenbooleanfalseA recessed field — the search variant.
idstring—The control's id. `GdField` generates one and passes it through the slot.
describedBystring—Ids of the hint and error that describe this field. Also supplied by `GdField`.
Slots
leadingA glyph inside the boundary, before the value — the search magnifier.
trailingA unit or glyph after the value, separated by a rule — "600 | m²".
GdInputNumber
Props
modelValuenumber | nullnull`v-model`. null MEANS empty — a form can tell "not answered" from 0, which for a setback are different answers.
unitstring—The unit, as the trailing affix — "m²", "m", "storeys".
minnumber—Clamped on BLUR, never mid-keystroke — silently rewriting 9 because min is 10 makes 95 untypeable.
maxnumber—Clamped on blur, same reason.
stepnumber1The ArrowUp/ArrowDown increment, and the rounding grain on blur — a step of 1 makes "2.4 storeys" normalise to 2.
placeholderstring—Placeholder text.
disabledboolean—Inert.
invalidboolean—The invalid look — usually driven by GdField's error.
size"sm" | "md""md"GdInput's own sizes.
idstring—From GdField's slot, linking the label.
describedBystring—From GdField's slot, naming the hint and error.
GdTextarea
Props
modelValuestring—`v-model`.
placeholderstring—Same rule as the input's: format, not information.
disabledbooleanfalseInert and unfocusable.
readonlybooleanfalseFocusable, not editable.
invalidbooleanfalseThe error ring.
rowsnumber3The resting height. The user can drag it taller — vertically only.
idstring—The control's id, normally from `GdField`.
describedBystring—Ids of the hint and error, normally from `GdField`.
GdSelect
Props
modelValuestring | number—`v-model`. Matched against each option's `value`.
optionsrequiredGdSelectOption[]—The choices, as DATA: `{ value: string | number, label: string, disabled?: boolean }`. The listbox owns the keyboard; the caller owns the meaning.
placeholderstring"Select…"Shown when nothing is selected.
disabledbooleanfalseInert and unfocusable.
invalidbooleanfalseThe error ring.
size"sm" | "md""md"Matches the input's two heights.
eyebrowstring—A tiny bold line above the value, INSIDE the trigger — "City".
barebooleanfalseStrips the field chrome, for a select embedded in composed chrome that already owns the border and the focus ring. The keyboard cue becomes the hover surface.
idstring—The trigger's id, normally from `GdField`.
describedBystring—Ids of the hint and error, normally from `GdField`.
ariaLabelstring—The accessible name for a select with no visible `GdField` label. It has to be a prop: the root is a `<span>` with no role, so an `aria-label` written at the call site lands there and names nothing.
Events
open[]Fired when the listbox opens — hang expensive option-loading off it. Emitted on EVERY opening, not only the first, and from the open watcher rather than the click handler, so ArrowDown and a click count the same.
GdSearchSelect
Props
modelValuestring | number—`v-model`. Matched against each option's `value`, exactly as GdSelect.
optionsrequiredGdSearchSelectOption[]—The choices, as DATA: `{ value, label, sub?, tag?, keywords?, disabled? }`. `sub` is the qualifier under the label, `tag` a mono identifier at the end of the row, `keywords` extra text the filter matches and nobody sees.
placeholderstring"Select…"Shown on the TRIGGER when nothing is selected.
searchPlaceholderstring"Search…"The filter field's placeholder. Say what is being searched — "Search 47 councils…" tells the reader the size of the list before they open it.
emptyTextstring"No matches"What the panel says when the filter matches nothing. Rendered outside the listbox with `role="status"`, so it is announced.
disabledbooleanfalseInert and unfocusable.
invalidbooleanfalseThe error ring.
size"sm" | "md""md"The trigger's two heights, matching GdSelect's — two pickers side by side must not be two heights.
eyebrowstring—A tiny bold line above the value, INSIDE the trigger — "Council".
barebooleanfalseStrips the trigger's field chrome, for a picker inside chrome that already owns the border and the ring.
idstring—The trigger's id, normally from `GdField`.
describedBystring—Ids of the hint and error, normally from `GdField`.
ariaLabelstring—The accessible name — and it names TWO elements, the trigger and the filter input. A prop for the same reason as GdSelect's: the root is a `<span>` with no role.
Events
open[]Fired when the panel opens, on every opening. Same contract as GdSelect's — hang expensive option-loading off it and hold the once-guard in the caller.
GdHint
Props
tone"muted" | "quiet" | "danger""muted"`quiet` for meta a reader can skip; `danger` for a standing warning. There is no id prop — this joins no describedby chain.
Slots
defaultThe sentence. Renders a `<p>`.
Usage Guidelines
- Put every control inside a GdField and let it supply
id,describedByandinvalid. Setting those by hand is the path to a form nobody can hear. - Use
sunkenfor a search field sitting in chrome — a toolbar, a panel head. Leave it off for a field in a form. - Use
readonly, notdisabled, for a value the user may want to read or copy. A disabled control cannot be focused to ask about. - Use
GdTextareaonly for prose someone will write more than a line of. A long single value is still an input. - Use
@openon the select when assembling the options is expensive, and hold the once-guard in the caller — the component fires on every opening. - Reach for
GdSearchSelectpast about twelve options — the point where type-ahead stops helping, because you no longer know the exact word the list is sorted by. Below that a plain select is less machinery for the same job. - Put a count in the search select's
searchPlaceholder— "Search 47 councils…" tells a reader how big the list is before they open it, which is the one fact a trigger showing one value cannot. - Use
keywordsfor text that should match but not show — a council id, an old name people still type. The alternative is printing the code in the label, which makes every row longer to serve the few who search by it. - Pass
ariaLabelto any select with no visible label. Anaria-labelwritten at the call site lands on the wrapper and names nothing. - Use
GdHintfor prose about a form; use GdField'shintfor prose about a control. The first is read in document order, the second is announced with the field. - Do not put information in a placeholder. If a placeholder is carrying something the user needs, give the field a real label and a hint instead.
Behavior & Anatomy
The input sets no width
GdField is a grid, so an input inside one stretches on its own; forcing width: 100% here would stack a filter bar one control per row, and the only fix would be a width override at every call site — which is the same as having no rule. The focus ring moves to the wrapper for the matching reason: the wrapper is what a user sees as the control, and the bare input is only part of it.
Why the unit gets a rule
A unit is separated by a rule rather than by spacing, because "600 m²" reads as one value and "600 | m²" reads as a value and its unit, which is what it is.
Why the select is not the browser's
The menu is the SYSTEM's — selected row in the accent wash with a check — so it obeys the tokens and the theme, which the operating system's popup does not. The ARIA is the combobox-with-listbox pattern implemented in full, because a custom select that loses the native keyboard is a downgrade wearing a skin: arrows move, Home/End jump, type-ahead jumps to the first match, Enter and Space choose, Escape closes and focus returns to the trigger. Past about twelve options the design adds a filter input at the top of the menu. That is GdSearchSelect — a separate component rather than a searchable prop, because three parts of this contract change at once when the filter appears: focus lands in a text field instead of on the listbox, the trigger stops being the element with role="combobox", and typing filters instead of jumping to the first match. A boolean that re-points the keyboard, the ARIA and the meaning of every keystroke is a different component wearing the same name.
Why the search select owns its filtering
GdSearchSuggest deliberately does not filter — a map search hits a geocoder, so the caller owns the results and the component owns presentation and the keyboard. In a search select the options ARE the data, so matching them is the component's job: a caller that has to filter its own array to feed a "searchable" select has been handed a search box that does nothing. That is the line between the two. If the matching is not yours to do, it is GdSearchSuggest.
The active row is scrolled into view, which GdSelect does not do. aria-activedescendant moves the highlight without moving focus, and a browser only auto-scrolls what it focuses — so on a list long enough to need a filter, ArrowDown walks the highlight straight out of the visible box unless the component scrolls it back. At twelve options that rarely shows; past twelve it is the first thing you hit.
@open fires when the listbox opens, so a caller whose options are expensive to assemble can hang that work off the first opening instead of preloading everything — or reaching inside the component for its state. It is emitted from the open watcher rather than from the click handler, so ArrowDown and a click count the same, and a call that finds the menu already open counts for nothing.
Why the hint has no id
quiet steps the SIZE down rather than the colour: the next ink below ink-muted is 3.29:1 and fails AA at caption size, so the loudness comes off in scale and the contrast stays where it is.
danger matches GdField's error exactly, so a standing warning and a field's own validation read as one voice. It is still prose, not a live region — a message that appears in answer to a submit belongs in GdField or GdBanner, which announce it. Without this component the sentence becomes a local .field-hint in every app, each one a slightly different grey.