Skip to content

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            |
+----------------------------------------------------+
ElementStyle
Input containerbg-card, border, rounded-xl, height 48px
Input textfont-mono, 15px, text-1
Placeholdertext-3, normal weight
Icon (left)Lucide Barcode or Wifi (RFID), 20px, text-3
Scan buttonbg-brand, white text, rounded-lg, font-semibold, 14px
Mode toggleRadio group, 12px, text-2

Modes

ModeIconBehavior
BarcodeScanBarcodeAuto-focus input, submit on hardware scan event or Enter key
RFIDWifiListen for RFID reader events, show "Menunggu RFID..." placeholder
ManualKeyboardStandard text input, submit on Enter or Scan button click

Interaction Flow

  1. Input auto-focuses on mount (POS) or on tab switch (stock-app)
  2. Hardware barcode scanner sends keystrokes ending with Enter
  3. Input detects rapid keystroke pattern (< 50ms between chars) as scanner input
  4. On submit: validate SKU format, lookup product, add to cart/list
  5. Clear input after successful submission
  6. 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

AppContextDefault Mode
POS ElectronProduct scan on main transaction screenBarcode
stock-app-mobileOpname scanning, receivingBarcode (RFID toggle available)
stock-app-webManual SKU lookupManual
buyer-appReceipt scanningBarcode

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

RetailOS - Sistem ERP Retail Modern untuk Indonesia