Skip to content
Components

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

GdInput deliberately sets NO width. GdField is a grid, so an input inside one stretches on its own. The focus ring sits on the WRAPPER, which is what a user sees as the control.
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

m²
Code
<GdInput v-model="area">
  <template #trailing>m²</template>
</GdInput>

Number

m²

Try typing 0812, then leave the field

storeys

model: {"area":812,"storeys":2}

A measurement, not a spinner. The model is number | null — null means empty. Type freely; normalising and clamping happen on BLUR, so '0812' becomes 812 when you leave the field, not while your cursor is in it. ArrowUp and ArrowDown step; storeys steps by 1, so 2.4 normalises to 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

GdInput type="date" — the platform's own picker, which is keyboard-complete and localised on day one. Bounds fall through as ordinary min/max attributes. A hand-rolled calendar is a project, and this is the deliberate decision not to start it.
Code
<GdInput v-model="date" type="date" :min="noticeStart" :max="noticeEnd" />

Textarea

Vertical resize only — horizontal resize breaks the measure rule and lets a user drag prose past 68 characters.
Code
<GdTextarea v-model="notes" :rows="3" />

Select

The keyboard is the full combobox contract: arrows move, Home/End jump, typing jumps to the first match, Enter and Space choose, Escape closes and focus returns to the trigger.
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

Live — open it. The emit fires on EVERY opening, not only the first: the once-guard belongs to the caller, which is the only party that knows whether the data has gone stale.
Code
<GdSelect v-model="zone" :options="zones" placeholder="Pick a zone" @open="loadZones" />

Search select

Focus lands in the FIELD, not on the list — the first thing a searchable picker is for is typing. Arrows move the highlight and scroll it into view, Enter chooses, Escape closes and returns focus to the trigger. The filter matches the sub-line and the hidden `keywords` too, so a code or an old name finds its row.
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.

quiet steps the SIZE down rather than the colour, so the loudness comes off in scale and the contrast stays where it is.
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

PropTypeDefault
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.

disabledbooleanfalse

Inert and unfocusable.

readonlybooleanfalse

Focusable and selectable, not editable. Prefer it to `disabled` for a value the user may want to copy.

invalidbooleanfalse

The 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.

sunkenbooleanfalse

A 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

leading

A glyph inside the boundary, before the value — the search magnifier.

trailing

A unit or glyph after the value, separated by a rule — "600 | m²".

GdInputNumber

Props

PropTypeDefault
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.

stepnumber1

The 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

PropTypeDefault
modelValuestring—

`v-model`.

placeholderstring—

Same rule as the input's: format, not information.

disabledbooleanfalse

Inert and unfocusable.

readonlybooleanfalse

Focusable, not editable.

invalidbooleanfalse

The error ring.

rowsnumber3

The 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

PropTypeDefault
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.

disabledbooleanfalse

Inert and unfocusable.

invalidbooleanfalse

The error ring.

size"sm" | "md""md"

Matches the input's two heights.

eyebrowstring—

A tiny bold line above the value, INSIDE the trigger — "City".

barebooleanfalse

Strips 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

PropTypeDefault
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.

disabledbooleanfalse

Inert and unfocusable.

invalidbooleanfalse

The 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".

barebooleanfalse

Strips 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

PropTypeDefault
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

default

The sentence. Renders a `<p>`.

Usage Guidelines

  • Put every control inside a GdField and let it supply id, describedBy and invalid. Setting those by hand is the path to a form nobody can hear.
  • Use sunken for a search field sitting in chrome — a toolbar, a panel head. Leave it off for a field in a form.
  • Use readonly, not disabled, for a value the user may want to read or copy. A disabled control cannot be focused to ask about.
  • Use GdTextarea only for prose someone will write more than a line of. A long single value is still an input.
  • Use @open on the select when assembling the options is expensive, and hold the once-guard in the caller — the component fires on every opening.
  • Reach for GdSearchSelect past 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 keywords for 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 ariaLabel to any select with no visible label. An aria-label written at the call site lands on the wrapper and names nothing.
  • Use GdHint for prose about a form; use GdField's hint for 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.

Navigate

Esc