Scan Input
The scan input pattern handles barcode scanning, RFID reading, and manual SKU entry. It is used in POS for product lookup and in stock-app for inventory operations.
Scan atau ketik SKU...
Barcode
RFID
Manual
Anatomy
+----------------------------------------------------+
| [Barcode icon] Scan atau ketik SKU... [Scan] |
+----------------------------------------------------+
| Mode: ( ) Barcode ( ) RFID ( ) Manual |
+----------------------------------------------------+| Element | Style |
|---|---|
| Input container | bg-card, border, rounded-xl, height 48px |
| Input text | font-mono, 15px, text-1 |
| Placeholder | text-3, normal weight |
| Icon (left) | Lucide Barcode or Wifi (RFID), 20px, text-3 |
| Scan button | bg-brand, white text, rounded-lg, font-semibold, 14px |
| Mode toggle | Radio group, 12px, text-2 |
Modes
| Mode | Icon | Behavior |
|---|---|---|
| Barcode | ScanBarcode | Auto-focus input, submit on hardware scan event or Enter key |
| RFID | Wifi | Listen for RFID reader events, show "Menunggu RFID..." placeholder |
| Manual | Keyboard | Standard text input, submit on Enter or Scan button click |
Interaction Flow
- Input auto-focuses on mount (POS) or on tab switch (stock-app)
- Hardware barcode scanner sends keystrokes ending with Enter
- Input detects rapid keystroke pattern (< 50ms between chars) as scanner input
- On submit: validate SKU format, lookup product, add to cart/list
- Clear input after successful submission
- Show inline error for unknown SKU (red border, "SKU tidak ditemukan" below)
React Component
tsx
interface ScanInputProps {
mode: 'barcode' | 'rfid' | 'manual'
onModeChange: (mode: 'barcode' | 'rfid' | 'manual') => void
onScan: (value: string) => void
placeholder?: string
error?: string
autoFocus?: boolean
}
export function ScanInput({ mode, onModeChange, onScan, placeholder, error, autoFocus = true }: ScanInputProps) {
const inputRef = useRef<HTMLInputElement>(null)
const [value, setValue] = useState('')
useEffect(() => {
if (autoFocus) inputRef.current?.focus()
}, [autoFocus, mode])
const handleSubmit = () => {
if (value.trim()) {
onScan(value.trim())
setValue('')
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSubmit()
}
}
const icons = { barcode: ScanBarcode, rfid: Wifi, manual: Keyboard }
const Icon = icons[mode]
const placeholders = {
barcode: 'Scan barcode atau ketik SKU...',
rfid: 'Menunggu RFID tag...',
manual: 'Ketik SKU produk...',
}
return (
<div>
{/* Input */}
<div className={cn(
'flex items-center gap-2 px-3 h-12 bg-card border rounded-xl transition-colors',
error ? 'border-danger' : 'border-border focus-within:border-brand'
)}>
<Icon className="w-5 h-5 text-3 shrink-0" />
<input
ref={inputRef}
type="text"
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder ?? placeholders[mode]}
className="flex-1 font-mono text-[15px] bg-transparent outline-none text-1 placeholder:text-3"
readOnly={mode === 'rfid'}
/>
<button
onClick={handleSubmit}
className="px-3 py-1.5 bg-brand text-white text-sm font-semibold rounded-lg hover:bg-brand-hover transition-colors"
>
Scan
</button>
</div>
{/* Error */}
{error && (
<p className="text-xs text-danger mt-1 ml-1">{error}</p>
)}
{/* Mode Toggle */}
<div className="flex items-center gap-4 mt-2">
{(['barcode', 'rfid', 'manual'] as const).map(m => (
<label key={m} className="flex items-center gap-1.5 cursor-pointer">
<input
type="radio"
name="scan-mode"
checked={mode === m}
onChange={() => onModeChange(m)}
className="accent-brand"
/>
<span className="text-xs text-2 capitalize">{m}</span>
</label>
))}
</div>
</div>
)
}Usage
| App | Context | Default Mode |
|---|---|---|
| POS Electron | Product scan on main transaction screen | Barcode |
| stock-app-mobile | Opname scanning, receiving | Barcode (RFID toggle available) |
| stock-app-web | Manual SKU lookup | Manual |
| buyer-app | Receipt scanning | Barcode |
Sound Feedback
- Success scan: Short confirmation beep (200ms, 880Hz)
- Error scan: Double low beep (200ms, 440Hz, repeat)
- RFID read: Subtle ping sound
POS Performance
In POS context, the scan input must process a barcode within 100ms. Debounce is NOT applied --- every Enter keypress triggers immediate lookup.
Don't
- Don't add validation delay on barcode input --- hardware scanners send data instantly
- Don't disable the manual mode --- cashiers need it as fallback when scanner fails
- Don't show a loading spinner for barcode lookup --- use optimistic matching from local cache