# Forms And Controls

> Input design for data apps - period and date-range pickers, numeric and currency inputs, scenario assumption controls, segmented filters, search, validation timing, and control layout. Covers the finance-specific cases that generic form advice gets wrong. Trigger on "form", "input", "date picker", "date range", "filter control", "validation", "assumption input", "scenario slider", "dropdown", "search box".

- Skill: `lukehle/forms-and-controls` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lukehle/forms-and-controls`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lukehle/forms-and-controls/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Lukehle (https://skillmd.com/u/lukehle)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lukehle/forms-and-controls

---


# Forms and controls

Controls in a data app are not a signup form. Nobody is being onboarded — they are steering a view
they will steer again tomorrow. That changes what good looks like: **speed, precision, and
reversibility** over friendliness and hand-holding.

---

## Period selection — the control finance actually uses most

Generic date pickers are wrong here. People do not want to pick July 1 and July 31; they want "July"
or "Q3" or "last month".

```
[ Jul 2026 ▾ ]   [ Month | Quarter | Year | Custom ]     ← named periods first
Comparison: [ Prior period | Prior year | Plan | None ]  ← comparison is a first-class control
```

Rules that matter:

- **Named periods before custom ranges.** Custom is the escape hatch, not the default.
- **Show period status.** `Jul 2026 (closed)` versus `Aug 2026 (open)` changes how the numbers should
  be read — an open period can still move.
- **Comparison basis is its own control.** "Up 12%" is meaningless without it, and burying it in a
  settings panel guarantees misreads.
- **Never default to a custom range**, and never default to a range whose boundaries are ambiguous.
- Use **half-open ranges** internally (`>= start AND < end`) so the final day is not silently
  truncated at midnight — the same trap as in SQL.
- Disable or mark future periods rather than allowing a silent empty result.

---

## Numeric and currency inputs

Finance users type numbers all day, in formats generic inputs reject.

**Accept what people actually type**, then normalize on blur:

| They type | Interpret as |
|---|---|
| `1,234.56` | 1234.56 |
| `$1,234` | 1234 |
| `(500)` | −500 (accounting negative) |
| `1.2M`, `1200k` | 1200000 |
| `1 234` | 1234 |
| `-` or empty | null, not 0 |

Rejecting `1,234.56` because of the comma is the single most annoying thing a finance form can do.

Other essentials:

- **`inputmode="decimal"`** for a sensible mobile keypad; `type="text"` with your own parsing beats
  `type="number"` (which suppresses commas and adds spinners nobody wants).
- **Right-align numeric inputs** with tabular figures, so a column of them stacks
  (`data-typography`).
- **Show the unit in the field or its label**, not as separate text the eye must associate.
- **Never silently coerce.** If input cannot be parsed, say so — do not quietly substitute 0. A
  zero that should have been an error propagates into a total.
- **Empty is not zero.** Distinguish "no value entered" from "the value is zero"; they mean
  different things in a forecast.

---

## Scenario and assumption inputs

For models where a viewer changes an assumption and watches the output move:

- **Pair every slider with a number field.** Sliders are for exploring, typed values are for
  precision, and finance users will want the exact 3.5%.
- **Bound them to defensible ranges**, and say what the bound is. An unbounded growth-rate slider
  invites a 900% scenario that produces a meaningless chart.
- **Show the baseline.** A marker for "plan" or "current" makes deviation readable at a glance.
- **Always offer Reset to baseline.** Exploration without a way back is a trap.
- **Debounce the recompute (~250ms), but update the input display instantly.** The field must feel
  responsive even when the model behind it is not.
- Show the delta from baseline next to the output, not just the new absolute value.

---

## Filters

- **Segmented control** for 2-5 mutually exclusive options — visible, one click, no hidden state.
- **Multi-select dropdown** for many options, with search above ~10 and a count in the trigger
  (`Region (3)`).
- **Search** for high-cardinality fields (customer, account) — never a 4,000-item dropdown.
- **Checkboxes** for a handful of independent toggles.

Every filter needs: a visible active state, individual removal, and a global reset. See
`app-interaction-patterns` for the surrounding behaviour.

**Show the effect**: "1,204 of 14,882 rows". A filter whose impact is invisible will be forgotten and
the number quoted out of context.

---

## Validation timing

The rule that avoids the most irritation:

> **Validate on blur, not on keystroke. Re-validate on change once a field has already errored.**

Validating while someone types tells them `1,2` is invalid on their way to `1,234`. Once a field has
shown an error, switching to on-change lets them see it clear as they fix it.

- **Errors sit next to the field**, not in a summary at the top.
- **Say what is wrong and what is acceptable**: "Enter a percentage between 0 and 100" beats
  "Invalid input".
- **Never block submission without saying which field.** If you disable the submit button, the reason
  must be visible.
- **Warnings are not errors.** An unusual-but-legal value gets a warning that can be overridden; a
  wrong one gets an error. Conflating them either blocks legitimate work or lets bad data through.

---

## Layout

- **One column.** Multi-column forms produce ambiguous tab order and get misread.
- **Labels above fields.** Fastest to scan, and it survives narrow widths without reflowing.
- **Group related fields with spacing**, not boxes (`visual-hierarchy`).
- **Controls in one consistent place** — a top bar or a left rail, not both.
- **Controls must not outweigh results.** If the filter panel dominates the view, the page is about
  its own configuration.
- Primary action on the right in a button row, and only one primary action.

---

## Defaults

The most under-considered part of a control surface. A good default means most viewers never touch
the controls at all.

- Default to **the most common case**: current closed period, all segments, standard comparison.
- **Never default to something that produces an empty view.**
- **Persist a viewer's changes** within the session so a re-render does not reset their work.
- Make the default recoverable — a Reset that returns to it, labelled as such.

---

## Related skills

- `app-interaction-patterns` — filter behaviour and state
- `microcopy-and-states` — the words in labels, errors, and empty results
- `data-typography` — numeric alignment and formatting
- `artifact-accessibility` — labels, focus, keyboard
- `visual-hierarchy` — where controls sit relative to results

