MobTranslate styleguide

Input and gestures

Make every essential action discoverable and operable with keyboard, touch and a single pointer.

Choose the simplest interaction

Use native buttons, links and form controls for their established behavior. A gesture can accelerate a task, but it must not hide the only way to complete it. Keyboard access and a single-pointer alternative to dragging are separate requirements.

InteractionWeb behaviorRequired alternative or constraint
Tap or clickActivate a native button or linkKeyboard activation and an accessible name
HoverOptional supporting previewEquivalent focus access; never essential hover-only actions
Drag reorderMove an item to a new positionVisible move controls or another non-drag pointer operation, plus keyboard access
SwipeOptional navigation or dismissalVisible equivalent control and recovery for consequential actions
Long pressOptional context actionDiscoverable menu button or documented keyboard route
Pinch or wheel zoomBrowser or viewer magnificationDo not disable browser zoom; provide viewer controls if custom zoom is essential

Gesture vocabulary and interruption

Give immediate moving/pressed feedback without committing at gesture start. A cancelled gesture preserves the prior value or position. Do not assign the same long-press/drag combination both to moving items and to selecting a batch in the same context.

The reorder lab shows the active moving item with a boundary and text status. A cancelled native drag removes feedback and keeps order unchanged. Drop and visible move controls use the same order transition. Native event-handler checks are separate from physical touch-device evidence.

GesturePurposeWeb ownership and recovery
Double tapContextual zoom or openingKeep an explicit control and browser zoom; do not make timing essential.
Long press / pick up and moveContext actions, selection or movingChoose one unambiguous contract; expose menu/move controls.
Scroll and panReveal content within a surfaceUse native scroll regions; preserve nested page scrolling and keyboard access.
SwipePeer navigation or an optional actionProvide visible navigation/dismissal and undo for consequential changes.
Pinch / compound pan and pinchMagnify and navigate a viewerPreserve browser gestures; custom viewers need bounded controls and cancellation.
Predictive backAndroid previews the previous destination before commitmentNative SDK/platform behavior; ordinary web history is not a predictive-back implementation.

Input methods can coexist

A phone or tablet can have a mouse, trackpad, keyboard or stylus connected while touch remains available. Screen width does not establish the active input. Treat cursor/hover capabilities as optional enhancements; essential actions keep visible native controls.

Primary click/tap activates; secondary click can open the object’s context menu; wheel and trackpad gestures usually scroll the region under the pointer. Browser cursor conventions include link pointer, text I-beam and appropriate resize handles. A stylus tool cursor can communicate tool size, while basic pen input should retain a usable activation path.

  • Reuse the installed Context Menu with its visible trigger/keyboard route instead of intercepting every secondary click.
  • Use real overflow regions for vertical and horizontal content; do not hijack wheel events across the page.
  • Allow selectable text and ordinary clipboard operations in source content. Browser/platform selection handles and menus remain native.
  • Use native text fields for composition and editing; do not replace IME, handwriting or accessibility input with a key-code-only parser.

Text selection, scrolling and virtual keyboard boundaries

Mouse dragging across selectable text and touch dragging to scroll are different intentions. Keep ordinary browser selection, touch handles, editable caret behavior and context menus available; avoid blanket user-select:none or preventDefault handlers on source panels.

Physical and on-screen keyboards are managed by the browser/OS. This web library does not detect keyboard attachment reliably or forcibly hide a virtual keyboard. Choose appropriate native input types/inputmode and keep the current field, error and recovery actions reachable when the viewport changes.

Enter submits a form when that is its task; a multiline editor must still permit new lines. Space activates a focused native button, including the audio player’s play/pause action. Escape dismisses supported overlays with focus return; do not copy native-reference advice into a global web blur/clear handler that removes keyboard position or silently destroys entered text.

Reordering lab

Desktop native drag and visible move buttons update the same local state. The buttons work with a tap, click, Enter or Space. Stable item keys preserve control identity when items move, and a live status reports the new position. Native drag availability differs across touch browsers; the buttons remain the dependable alternative.

Reorder this example list using drag, pointer, touch or keyboard controls. This does not change dictionary records.

  1. 1. Entry text
  2. 2. Source record
  3. 3. Review state

Drag an item or use its move buttons.

Targets and pointer cancellation

  • Aim for at least 44×44 CSS px targets for primary touch controls. WCAG 2.2 AA target-size minimum is 24×24 CSS px subject to its stated spacing and other exceptions; the larger product target is a usability choice, not the AA threshold.
  • Use click activation on native controls rather than committing a consequential action at pointer-down.
  • Keep adjacent targets separate and visible. A large invisible hit area must not overlap another action.
  • Do not suppress scrolling or browser gestures across the entire page. Restrict custom touch-action rules to the relevant interaction region.
  • Cancel or restore an interrupted gesture. Never publish a change merely because the pointer left the screen.

Keyboard and focus contract

  • Tab moves between controls; Enter/Space activates buttons. Arrow-key behavior belongs to documented composite widgets such as menus, tabs and radio groups.
  • Use the library primitive’s keyboard model instead of assigning a different key scheme to each page.
  • Keep focus visible and in a logical order after insertion, removal or reorder. Announce meaningful results without moving focus unnecessarily.
  • Use Escape for dismissible overlays where appropriate. Restore focus to a meaningful surviving control.
  • Avoid positive tabindex values and global single-letter shortcuts that interfere with typing or assistive technology.

Implementation

For production reordering, persist only after validation, handle save failure, and provide undo when appropriate. This documentation lab intentionally changes local example state only.

tsx
<button type="button"
  disabled={index === 0}
  aria-label={`Move ${item.label} up`}
  onClick={() => moveItem(item.id, -1)}>
  Up
</button>
<p role="status">{reorderAnnouncement}</p>
// Drag and this button must call the same state transition.

Failure cases

  • Avoid clickable div elements with ad hoc keyboard emulation when a button fits.
  • Do not make hover, double-click, long press, swipe or drag the only route to an essential task.
  • Do not rely on pointer coordinates to infer which language entry a user meant when an explicit selection is available.
  • Avoid preventing default key events globally; it can break scrolling, text editing and screen-reader commands.

Verification

  • Reorder every item with the move controls using keyboard only, then repeat with a single pointer without dragging.
  • Test desktop drag separately from the alternative controls. Confirm the same final ordering and position announcement.
  • Verify focus remains on the moved item’s surviving control, or is deliberately restored if that control becomes disabled.
  • Test touch on a real device, zoom, interrupted drags and page scrolling. Desktop emulation alone does not prove native touch gesture behavior.

Compiled reorder example

tsx
'use client';
import {useEffect, useReducer, useRef, useState} from 'react';
import {Button} from '@mobtranslate/ui';
import './input-gestures-explorer.css';

const initialItems = [{ id: 'entry', label: 'Entry text' }, { id: 'source', label: 'Source record' }, { id: 'review', label: 'Review state' }];
type OrderAction = { type: 'step'; id: string; delta: number } | { type: 'drop'; id: string; targetId: string } | { type: 'reset' };
function orderReducer(state: { items: typeof initialItems; announcement: string }, action: OrderAction) {
  if (action.type === 'reset') return { items: initialItems, announcement: 'Example order reset.' };
  const from = state.items.findIndex((item) => item.id === action.id);
  const to = action.type === 'step' ? from + action.delta : state.items.findIndex((item) => item.id === action.targetId);
  if (from < 0 || to < 0 || to >= state.items.length || from === to) return state;
  const items = [...state.items];
  const [item] = items.splice(from, 1);
  items.splice(to, 0, item);
  return { items, announcement: `${item.label} moved to position ${to + 1} of ${items.length}.` };
}
export function InputGesturesExplorer() {
  const [state, dispatch] = useReducer(orderReducer, { items: initialItems, announcement: 'Drag an item or use its move buttons.' });
  const dragged = useRef<string | null>(null);
  const [dragging,setDragging]=useState<string|null>(null);
  const [dragResult,setDragResult]=useState<string|null>(null);
  const list = useRef<HTMLOListElement>(null);
  const pendingFocus = useRef<string | null>(null);
  useEffect(() => {
    if (!pendingFocus.current) return;
    const item = Array.from(list.current?.children ?? []).find(node => node.getAttribute('data-item-id') === pendingFocus.current);
    item?.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();
    pendingFocus.current = null;
  }, [state.items]);
  const step = (id: string, delta: number) => { setDragResult(null); pendingFocus.current = id; dispatch({ type: 'step', id, delta }); };
  return <div className="reorder-lab space-y-4" data-reorder-lab>
    <p className="text-sm">Reorder this example list using drag, pointer, touch or keyboard controls. This does not change dictionary records.</p>
    <ol ref={list} data-order-preview className="space-y-3">{state.items.map((item, index) => <li key={item.id} data-item-id={item.id} data-dragging={dragging===item.id?true:undefined} draggable onDragStart={(event) => { dragged.current = item.id; setDragging(item.id); setDragResult(null); event.dataTransfer.setData('text/plain', item.id); event.dataTransfer.effectAllowed = 'move'; }} onDragEnd={() => { if(dragged.current)setDragResult("Move cancelled. Order unchanged."); dragged.current = null; setDragging(null); }} onDragOver={(event) => event.preventDefault()} onDrop={(event) => { event.preventDefault(); if (dragged.current) { if(dragged.current===item.id)setDragResult('Order unchanged.'); else dispatch({ type: 'drop', id: dragged.current, targetId: item.id }); } dragged.current = null; setDragging(null); }} className="flex flex-wrap items-center justify-between gap-3 p-3 border border-[var(--color-border)] rounded-lg">
      <span>{index + 1}. {item.label}</span><div className="flex gap-2"><Button variant="outline" disabled={index === 0} aria-label={`Move ${item.label} up`} onClick={() => step(item.id, -1)}>Up</Button><Button variant="outline" disabled={index === state.items.length - 1} aria-label={`Move ${item.label} down`} onClick={() => step(item.id, 1)}>Down</Button></div>
    </li>)}</ol>
    <p role="status" data-reorder-status className="text-sm">{dragging?`Moving ${initialItems.find(item=>item.id===dragging)?.label}. Drop on an item to change its position; cancel the drag to keep the order.`:dragResult??state.announcement}</p>
    <Button variant="ghost" onClick={() => { dragged.current=null; setDragging(null); setDragResult(null); dispatch({ type: 'reset' }); }}>Reset order</Button>
  </div>;
}

Compiled drag feedback styles

css
.reorder-lab { min-inline-size:0; }
.reorder-lab [data-dragging] { border:2px solid var(--color-muted-foreground); background:var(--color-muted); color:var(--color-muted-foreground); cursor:grabbing; }
.reorder-lab li > div { flex-wrap:wrap; min-inline-size:0; max-inline-size:100%; }
.reorder-lab .mt-btn { block-size:auto; white-space:normal; min-block-size:44px; padding-block:.5rem; }
.reorder-lab p { overflow-wrap:anywhere; }
@media(forced-colors:active) { .reorder-lab [data-dragging] { border-color:Highlight; background:Canvas; color:CanvasText; } }