Keyboard-safe Form
motiq/keyboard-safe-formFreeComponent
A presentation-only mobile form whose sticky Submit/Cancel bar rides above the virtual keyboard via the VisualViewport API (degrading gracefully to a plain sticky footer where unavailable), with an aria-linked validation summary, cancellable async submission, and an unsaved-changes discard guard. The app owns the fields and submission.
Live preview
live · rendered by the registry
Rendered by motiq on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://www.motiq.dev/r/keyboard-safe-form.jsonSource
1"use client";23/**4 * KeyboardSafeForm — a mobile form that stays usable while the on-screen5 * keyboard is open, and behaves like an ordinary form everywhere else.6 *7 * The hard part on mobile is that opening the virtual keyboard shrinks the8 * *visual* viewport without shrinking the *layout* viewport, so a normal9 * `position: fixed`/sticky footer ends up hidden behind the keyboard. This10 * component solves that with the **VisualViewport API when it exists** and11 * **degrades to a plain sticky footer when it doesn't** — it never depends on12 * an unsupported "virtual keyboard" API. When the keyboard overlaps the layout13 * viewport, the sticky action bar is lifted by exactly that overlap so Submit /14 * Cancel stay reachable, and the focused field is scrolled into view.15 *16 * It is presentation-only: the **app owns the fields and the submit work**17 * (passed as `children` + an `onSubmit` that returns a promise). The component18 * owns the *scaffolding* production forms always need:19 *20 * - A validation summary (`errors`, or an inline `onValidate` on submit) that21 * lists every error and links each one to its field by `fieldId`; activating22 * an item moves focus to that field. Submit is blocked while errors remain.23 * - Async submission via `useAsyncStatus`: idle → pending → success | error,24 * with a cancellable in-flight state, a retry on failure, and a polite live25 * region. Status is conveyed with icon + text, never colour alone.26 * - An unsaved-changes guard: when `dirty`, cancelling (or leaving the page)27 * asks before discarding.28 *29 * Accessibility & safety: labelled action buttons, `aria-live` status, focus30 * moved to the summary on invalid submit and restored after the discard dialog,31 * ≥44px targets, safe-area insets, 200%-zoom-safe layout, and a reduced-motion32 * path that drops every transition while keeping the exact same behaviour.33 * Clean-room original.34 */3536import * as React from "react";37import { motion, AnimatePresence } from "motion/react";3839import { cn } from "@/lib/utils";40import { useReducedMotion, useAsyncStatus } from "@/lib/motiq";4142/* -------------------------------------------------------------------------- */43/* Types */44/* -------------------------------------------------------------------------- */4546export interface FieldError {47 /** `id` of the field this error refers to — must match the input's `id`. */48 fieldId: string;49// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from motiq
All 100 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Activity Streamactivity-stream | motiq | Components | Free | 1 dep | |
| Adaptive Safe-Zone Gridadaptive-safe-zone-grid | motiq | Components | Free | no deps | |
| Agent Run Timelineagent-run-timeline | motiq | Components | Free | 1 dep | |
| AI Response Streamai-response-stream | motiq | Components | Free | 1 dep | |
| Animated Accordionanimated-accordion | motiq | Components | Free | 2 deps | |
| Animated Buttonanimated-button | motiq | Components | Free | 1 dep | |
| Animated Dialoganimated-dialog | motiq | Components | Free | 2 deps | |
| Animated Gridanimated-grid | motiq | Components | Free | 2 deps |
