← Files Frontend Design PremiumARCHIVED FILE
skills/frontend-design-premium/references/data-entry-patterns.md
19.8 KB · Oct 4, 2026 · 12:29 UTC
# Data Entry and Composite Interaction Patterns
Read the relevant sections for advanced inputs, bulk workflows, direct manipulation, or composite widgets. Prefer a proven accessible library primitive over hand-assembling ARIA behavior.
## Multi-select and bulk actions
- Use a checkbox per item and a labeled header checkbox with checked/unchecked/indeterminate states.
- Distinguish “select this page” from “select all matching results.” Never imply the latter when only loaded rows are selected.
- Keep selection stable across sort/filter/page changes only when users can understand the scope; otherwise clear it and announce why.
- Show exact selected count and make the bulk-action toolbar persistent without shifting the table unexpectedly.
- Shift-click range selection is a progressive enhancement when ordering is stable. Platform modifier selection may supplement but not replace checkboxes.
- After a bulk mutation, report succeeded/failed counts, preserve recoverable failures, and put focus in a logical surviving location.
- Dangerous bulk actions name scope, count, consequence, and recovery in an app-owned confirmation.
## File upload and drop zones
A drop zone always includes a visible file-picker button; drag and drop is never the only method.
States: idle, drag-over, validating, queued, uploading, paused/cancelled when supported, success, partial success, error, retry, and removed.
- State accepted types, count, and size limits before selection; validate on both client and server.
- Use the native file input for selection and preserve its accessible label.
- Do not trust extension/MIME alone or expose local file paths.
- Show each file name, size, progress when measurable, status, cancel/remove, and actionable error.
- For long/large uploads, define cancellation, resumability/chunk retry, navigation behavior, duplicate handling, and session expiry.
- Preview untrusted content safely; revoke object URLs and avoid decoding huge media on the main thread.
- Announce status changes without flooding live regions. Keep layout stable as rows enter progress/error states.
## Single-select dropdowns
Decide whether the popup may remain platform-owned before choosing the component:
- Use a native `<select>` only when the browser/operating-system popup appearance and geometry are acceptable. CSS on `<option>` cannot reliably control that popup across platforms.
- When popup width, border, radius, spacing, option states, scrolling, or placement is part of the authored design, use the project's maintained accessible Select/Listbox primitive (for example, an established Reka UI, Radix UI, or Headless UI integration). Do not hand-build ARIA behavior.
Treat trigger and popup as one geometry contract:
- Match the popup's outer width to the trigger within **1 CSS px** and align their inline-start edges unless a documented responsive rule requires otherwise.
- Reuse the same border-width and border-color tokens, the same radius family, and compatible control density. Do not leave the popup on a browser-default 1 px border when the trigger uses a stronger authored border.
- Render the popup through the primitive's portal, use collision-aware placement, bound its maximum height, and keep its owned scrollbar visible and tokenized.
- Preserve selected, highlighted, disabled, loading, empty, and error states without shifting the trigger or popup geometry.
Preserve the complete interaction contract: accessible name, combobox/listbox roles supplied by the primitive, Arrow-key navigation, Enter/Space selection, Escape close, typeahead, visible focus, selected/disabled announcements, form value/touched/error integration, and localized option/accessibility text. Verify the open popup in a real browser; source-level classes on the closed trigger are not evidence that the platform popup matches.
## Combobox, autocomplete, and typeahead
Use a maintained combobox primitive that follows the WAI-ARIA pattern.
- Keep input value, committed value, highlighted option, and popup state distinct.
- Define minimum characters and debounce only for remote work. Suppress dispatch during IME composition and cancel/ignore stale responses.
- Provide loading, grouped results when needed, no-results, create-new when legitimate, retry, and clear behavior.
- Arrow keys navigate options; Enter commits the highlighted option only when not completing IME composition; Escape closes without erasing committed data.
- Expose active descendant/selection correctly and keep the highlighted option visible.
- For multi-select, render removable chips with keyboard removal and an accessible summary when values overflow.
- Do not silently commit arbitrary text when the field requires an entity ID. Preserve user input after recoverable request errors.
## Date and date-range pickers
Make the native-versus-authored date-picker decision explicit before choosing the component. Decide whether the calendar popup may remain platform-owned:
- Use native `input[type="date"]` only when the browser/operating-system calendar's language, labels, format presentation, geometry, and behavior are acceptable across the supported browser/OS matrix. Setting `lang`, formatting the closed value, or styling the input does not guarantee the open popup UI.
- When the active locale, calendar labels, footer actions, geometry, or interaction must be authored and consistent, use the project's maintained accessible date-picker primitive. Load its complete locale pack and do not hand-build calendar ARIA behavior.
- Treat date-only storage, display formatting, popup localization, and timezone as separate contracts. A localized display value is not a storage value, and locale does not choose a business timezone.
- Distinguish date-only values, local date-time, and absolute instants in the data model.
- Provide a typed input path in addition to the calendar when practical, with localized format guidance and deterministic parsing.
- Date ranges define inclusive/exclusive semantics, valid order, min/max, unavailable dates, and timezone before implementation.
- Presets such as Today, This week, Last 7 days, or Custom must follow domain definitions, not labels alone.
- Separate draft selection from committed filters when an Apply button exists; Cancel restores the prior committed range and Clear has an explicit outcome.
- Calendar keyboard behavior, focus, today, selected, range start/end, disabled dates, month navigation, announcements, Escape close, and trigger focus restoration must remain distinct.
- Localize heading, month/year, weekday headers, navigation controls, today/clear/apply/cancel actions, placeholders, validation, and accessible names. Verify there is no fallback English from the component library or browser.
- Open the popup in a real browser and verify required keyboard behavior, focus restoration, locale copy, narrow/short viewports, collision handling, bounded height, and scrolling. Add pointer/touch and a full 200% zoom matrix when the project requires extended verification. Source attributes and closed-input screenshots do not prove the open calendar.
- Japanese locale does not imply era notation. Follow `references/japanese-localization.md`.
## Inline editing
Use inline editing for short, localized changes where surrounding context is valuable. Use a full form for complex validation, dependencies, permissions, or destructive effects.
- Enter edit mode through a semantic control, not undiscoverable text click alone.
- Move focus to the editor and preserve the original value for cancellation.
- Define commit behavior explicitly: Save button, Ctrl/Cmd+Enter, Enter for single-line, or blur only when accidental commits are safe.
- Escape cancels and restores the original value; validation/error keeps the editor open with recovery.
- Prevent duplicate/stale saves. Optimistic commit requires honest rollback and conflict handling.
- On success, return focus to the edited value or next logical item and announce the result without moving the row.
## Expandable rows, disclosure, and accordion
Use a disclosure for one independent show/hide region and an accordion for a related set. An expanded table row must preserve valid table structure.
- The trigger is a button with accessible name and expanded state, linked to the controlled region.
- Do not make the entire row the only toggle when it contains links, checkboxes, or actions.
- Define whether multiple accordion panels may remain open. Do not force one-open behavior without a product reason.
- Keep focus on the trigger when toggling. If opening reveals a task requested by the user, move focus only when that is the expected next action.
- Animate height/opacity only when motion remains smooth, interruptible, and reduced-motion safe. Avoid measuring transitions that cause layout thrash.
- Lazy detail content needs loading, error, retry, stale-request, and collapse-during-load behavior.
## Stepper and multi-step forms
- Show current step, total or meaningful stage names, completed status, and optional/skippable status without using color alone.
- Validate the current step before advancing; retain values and errors when navigating back.
- Define whether completed steps are revisitable and whether URL/deep-link state is valid.
- Persist drafts when the workflow is long or interruption is likely. Communicate save status and expiry.
- Branching steps must not report misleading totals. Recompute the visible path deliberately.
- Review/confirm steps display the actual committed values and offer targeted edit links.
- Final submission is idempotent and distinguishable from per-step draft saving.
## Slider and range input
Prefer a native range input when it meets visual and interaction needs. A custom slider must implement the complete keyboard and touch model.
- Expose label, current value, min, max, and step. Show units and formatted values outside pointer-only tooltips.
- Support arrows and expected larger increments; keep focus visible on the active thumb.
- Multi-thumb sliders prevent ambiguous crossing or clearly swap semantics, maintain logical tab order, and expose each thumb's label/value.
- Provide numeric inputs when precise values matter or touch drag is difficult.
- Do not use a slider for a small set of named options better represented by radio buttons.
## Tags, chips, and badges
- Use tags/badges for status or metadata; chips may represent removable values, filters, or compact actions. Do not make static status look clickable.
- Semantic color has a text/icon/shape equivalent and meets contrast requirements.
- Removable chips use a real remove button with a specific accessible name and predictable focus after removal.
- For overflow, show a count/summary with an accessible expansion path rather than clipping silently.
- Keep label vocabulary, casing, height, radius, and icon policy aligned with `DESIGN.md`.
## Filter chips and URL state
### Filter chips
- Render active filters as removable chips between the search bar and the data list.
- Each chip shows the filter name and value (e.g., `Status: Active` followed by a remove button).
- The remove button on the chip clears that single filter, cancels pending requests, and refreshes results.
- A "Clear all" button appears when 2+ filters are active. It clears all filters at once.
- Filter chips stack horizontally with wrapping. Do not clip or overflow — scroll on small screens if needed.
- Adding or removing a filter must not reset unrelated filters.
### URL state for filters and pagination
- Persist active filters, search query, current page, page size, and sort direction in URL search params.
- This enables browser Back/Forward navigation, shareable/bookmarkable filtered views, and preserved state on page refresh.
- Use `useSearchParams` (Next.js) or `useSearchParams` (React Router) to read/write filter state.
- Do not duplicate state: URL is the source of truth. Component state derives from URL.
- On page load, read initial filter state from URL. If no URL params exist, use sensible defaults (no filters, page 1).
- Allow `UX-CONTRACT.md` to override URL persistence only for transient, sensitive, non-shareable, or architecture-constrained state; record the reason rather than silently keeping committed dataset state in component memory.
```tsx
// Example: sync filters with URL
const searchParams = useSearchParams();
const router = useRouter();
const status = searchParams.get("status") ?? "";
const page = parseInt(searchParams.get("page") ?? "1", 10);
function setFilter(key: string, value: string) {
const params = new URLSearchParams(searchParams.toString());
if (value) params.set(key, value);
else params.delete(key);
params.set("page", "1"); // reset to first page on filter change
router.push("?" + params.toString());
}
```
## Density: compact and comfortable
Offer a density toggle when the primary view is a dense data table or list. This is common in ops/admin SaaS.
| Mode | Row height | Padding | Font size | When to use |
|------|-----------|---------|-----------|-------------|
| **Comfortable** (default) | 48-56px | `py-3` `px-4` | 14px | Default for most users, easier scanning |
| **Compact** | 32-40px | `py-1.5` `px-3` | 13px | Users who need to see many rows at once |
### Implementation
- Store the preference in localStorage (key: `density`).
- Apply a `.density-compact` class on the table or a CSS custom property `--row-height`.
- Do not change column count or responsiveness — compact mode only reduces vertical space.
- Toggle: a small icon button (rows-dense / rows-normal) near the search/filter bar.
- Label: "Compact view" / "Comfortable view" with accessible state.
```css
/* CSS variable approach */
table[data-density="compact"] {
--row-py: 4px;
--cell-font-size: 0.8125rem;
}
table[data-density="comfortable"] {
--row-py: 12px;
--cell-font-size: 0.875rem;
}
```
### When NOT to offer density
- Consumer-facing marketing pages or landing sections — density control confuses non-admin users.
- Single-purpose screens with a fixed small dataset.
- Kanban/card views — use card size instead of table density.
## Drag, reorder, and direct manipulation
Any drag operation has a non-drag alternative such as move up/down, destination menu, or keyboard reorder.
- Announce the grabbed item, position, valid destinations, and completed position when custom interaction is justified.
- Provide a sufficiently large handle and do not make scrolling impossible on touch.
- Persist reorder only after a clear commit or use honest optimistic rollback.
- Preserve focus and offer Undo for accidental reorder where reliable.
## Form validation patterns (Formik + Yup / React Hook Form + Zod)
These two libraries dominate React production forms. They share the same architectural concerns: client-side schema validation, server error recovery, common error banners, and inline field errors. The patterns below apply to both; pick the library that matches your codebase.
### Common pattern: two error layers
Real applications always display two error layers regardless of library:
1. **Common error banner** — a top-of-form alert for server-level failures (network error, authentication denied, session expired). State managed independently from the form library:
```tsx
const [commonError, setCommonError] = useState("");
// In JSX
{commonError && (
<div role="alert" className="bg-error-50 text-error-600 p-3 rounded-md">
{commonError}
</div>
)}
```
2. **Inline field errors** — per-field messages set via `setFieldError` (Formik) / `setError` (RHF) or schema validation.
### Formik + Yup
```tsx
const { handleChange, handleSubmit, values, errors, setFieldError } =
useFormik({
initialValues: { email: "", password: "" },
validationSchema: yup.object({
email: yup.string().email().required(),
}),
onSubmit(values, { setFieldError }) {
// client pre-check before server round-trip
},
});
```
**Server error to field mapping:**
```tsx
// onSubmit
onSubmit: async (values, { setFieldError }) => {
try {
await api.login(values);
} catch (err) {
if (err.field === "email") {
setFieldError("email", "このメールアドレスは登録されていません");
} else {
setCommonError("ログインに失敗しました。もう一度お試しください");
}
}
}
```
**Post-submit field recovery:**
```tsx
const handleChange = (e) => {
setFieldError(e.target.name, ""); // clear stale server error
setCommonError(""); // clear banner error on any edit
formik.handleChange(e);
};
```
### React Hook Form + Zod
```tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("正しいメールアドレスを入力してください"),
password: z.string().min(8, "パスワードは8文字以上必要です"),
});
type LoginForm = z.infer<typeof loginSchema>;
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
});
```
**Server error to field mapping:**
```tsx
const onSubmit = async (data: LoginForm) => {
try {
await api.login(data);
} catch (err) {
if (err.field) {
setError(err.field, {
message: err.message || "入力内容を確認してください",
});
} else {
setCommonError("サーバーエラーが発生しました");
}
}
};
```
**Post-submit field recovery:**
```tsx
// Use the `onChange` event from register to clear server errors
// or create a wrapped handler:
const handleFieldChange = (field: keyof LoginForm) => (e: React.ChangeEvent<HTMLInputElement>) => {
if (errors[field]) setError(field, {}); // clear without message
setCommonError("");
field.onChange(e); // original register onChange
};
// Usage: <Input {...register("email", { onChange: handleFieldChange("email") })} />
```
### Client pre-check pattern (both)
Validate *before* submit to keep the form responsive and avoid unnecessary API calls. This is for business logic that the schema cannot express (e.g., corporate-email-only rules):
```tsx
// Formik
onSubmit(values) {
let hasError = false;
if (!yup.string().email().isValidSync(values.email)) {
setFieldError("email", "正しいメールアドレスを入力してください");
hasError = true;
}
if (hasError) return;
}
// RHF + Zod — use trigger() or manual check
const onSubmit = async (data: LoginForm) => {
const result = loginSchema.safeParse(data);
if (!result.success) {
// Zod will have already populated errors via resolver;
// add extra business checks after
if (data.email.endsWith("@gmail.com")) {
setError("email", { message: "企業メールアドレスをご使用ください" });
return;
}
}
};
```
### Guidelines (library-agnostic)
- Use schema validation (`validationSchema` / `resolver`) for structural rules (required, format, min/max).
- Use `setFieldError` / `setError` + `commonError` for server-level or business-logic validation.
- Never use `alert()`, `confirm()`, or `prompt()` for form feedback — use the app-owned error UI.
- Apply `novalidate`/`noValidate` on `<form>` to disable native browser bubbles.
- Keep loading state (`isPending` / `isSubmitting`) to prevent double submit. Button must show busy state without changing width.
- On invalid submit, focus/scroll to the first field in error.
- For Japanese forms, error messages should explain what is wrong and how to fix, not just state failure (e.g., "パスワードは8文字以上で、英小文字と数字が必要です" rather than "パスワードが無効です").
## Accessible authentication inputs
- Allow password managers and paste; do not make users transcribe credentials or solve memory/puzzle tests unnecessarily.
- Use correct autocomplete values, reveal controls, and accessible error recovery.
- Support passkeys, magic links, or one-time codes with appropriate semantics when the product offers them.
- Re-authentication for sensitive actions preserves the user's pending task and explains why it is required.
SHA-256: ec14f7d19f7bb057d87d73a975f47bcf960879c0773e81612d595514de758238