busy-office-ui
Menu

Scan feedback

The visible half of a barcode scan, for a user whose eyes are on the rack: a ~600ms viewport wash — green on capture, red when your validation rejects the code — inside a neutral frame that carries the verdict without colour: one thin band when accepted, a thick doubled band when rejected. It is paired, always, with the data-scan-status live region the RF patterns already link, so the cue is two-channel by construction. The device's beep and LED stay the device's job; this component never plays audio. Not for form validation on an ordinary screen — a seated user reading a form gets the field's own message beside the control it concerns; a full-viewport flash there is noise aimed at nobody. Reach for this only when the user is physically away from the screen between inputs — which is the RF floor's whole context.

Try it — capture flashes ok, your validation flashes error

View code
<!-- Type a code and press Enter (a hardware scanner does exactly
     this, faster). data-scan-flash paints the viewport ok-wash on capture;
     REJECT* codes demo YOUR validation calling flashScanResult('error'). -->
<div class="bo-form-field">
  <label class="bo-form-field__label" for="scan-demo">Scan barcode</label>
  <input class="bo-input bo-input--code" id="scan-demo" data-scan-input data-scan-flash
      placeholder="Try 4006381333931, then REJECT-1" aria-describedby="scan-demo-status" />
  <p id="scan-demo-status" data-scan-status aria-live="polite" class="bo-visually-hidden"></p>
</div>

Capture ≠ validity: data-scan-flash makes the behavior paint ok the moment the terminator arrives (the scanner said "I read it"), and flashScanResult('error', reason) is your validation's verdict — the framework cannot know whether a perfectly-scanned code belongs on this PO. Under reduced motion the wash is static for the stamp's lifetime; under forced colors it becomes a Highlight frame, since a repainted background would vanish exactly when contrast matters most.

Markup

<input class="bo-input bo-input--code" data-scan-input data-scan-flash
    autofocus aria-describedby="scan-status" />
<p id="scan-status" data-scan-status aria-live="polite" class="bo-visually-hidden"></p>
<script type="module">
  import { initScanInput, flashScanResult } from "@busy-office/ui/js";
  initScanInput(); // capture -> ok flash (data-scan-flash) + live-region announce
  document.addEventListener("bo:scan", (e) => {
    // Capture is not validity. YOUR check decides, then paints the verdict:
    if (!isOnThisPo(e.detail.value)) flashScanResult("error", "Not on this PO");
  });
</script>

What the platform gives you, and what the behavior adds

Without initScanInput()What the behavior adds
A keyboard-wedge scanner types the code and sends Enter, so a plain input inside a form posts the scan. The screen still worksthe bo:scan event, so a screen can react without a round trip
Nothing announces the capture — the field just changesthe [data-scan-status] live region announcement
No flash: the CSS keys off data-scan-result, which only the behavior stampsthe flash, and flashScanResult() for your verdict
A trailing terminator character can land in the valuedata-scan-terminator stripping

Unlike dialog or combobox, there is no native element underneath this one — a scanner is a keyboard, and the platform has no idea a scan happened. That makes the boundary more important here, not less: the no-JS path is a real form post and a server re-render, which every RF pattern page documents as its No-JS row. Build that path first and treat the flash as the enhancement it is.

Quick reference generated

ClassRole

State attributes: data-scan-flashdata-scan-inputdata-scan-resultdata-scan-statusdata-scan-terminator

API generated from CSS

data-* hooks data-scan-flashdata-scan-inputdata-scan-result="error|ok"data-scan-statusdata-scan-terminator
JS required initScanInput() owns the moment: with data-scan-flash on the input, every capture stamps data-scan-result="ok" on <body> for ~700ms (scan.css paints the wash; a timer clears the stamp, so the reduced-motion static wash cannot linger). flashScanResult(kind, message?) is exported for YOUR validation path — 'error' overrides a live ok, and a message lands in the [data-scan-status] live region.
  • Two-channel by contract: the wash is never the only signal — every capture and rejection also reaches the data-scan-status live region. That is the deliberate improvement over device-SDK overlays, which are colour-only.
  • The overlay is pointer-inert and sits below toasts: a flash must never eat a tap or cover an error a consumer just raised.
  • RF-profile member (its whole audience): counted inside rf-essentials' 41 kB budget gate (40 until roadmap 388.1).
  • No audio, ever — beeps and LEDs belong to the device service; pairing with them is configuration, not CSS.
Contrast — 8 AA pairs this component uses (generated, CI-gated)
TextonLightDark
text-primary bg-surface 17.74 16.15
text-primary bg-canvas 16.98 18.08
bg-surface text-primary 17.74 16.15
text-primary bg-hover 16.12 13.75
text-primary bg-selected 17.01 11.86
text-primary bg-surface-raised 17.74 14.51
danger bg-muted 4.39 5.19
text-primary bg-muted 16.12 13.75

Maturity generated

Introduced
v0.5.0
CSS floor
Chrome/Edge 99 · Firefox 97 · Safari 15.4
Alignment scored
2026-08-28
Assistive-tech evidence
None recorded

Pin this component's first release with npm i @busy-office/ui@0.5.0. The floor is derived from this component's stylesheet alone — set by @layer — so it sits at or below the whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5. No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour. What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see accessibility.

Design-system alignment generated

100% (18/18) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-28.

Rule Score Evidence
Typography 3 / 3 sets no type at all — zero font-size declarations in scan.css; the component is a frame and a wash over whatever the page already renders
Colour 3 / 3 zero raw colour (success/danger tokens; Highlight only inside @media forced-colors), and two-channel WITHOUT hue: the verdict is carried by frame geometry — accepted 6px solid, rejected 18px double — plus the [data-scan-status] live region. check:claims compares the two states' computed border width/style and never their colour, in normal rendering AND under CDP forced-colors emulation
Spacing 3 / 3 every literal states its reason in place: the 6px and 18px frames (peripheral vision reads mass before detail), the 600ms explicit over the 300ms token, and the z-index calc naming the toast layer it sits below
Interaction 3 / 3 the page draws the platform-vs-behavior line as a four-row table plus the no-JS path — and says why the line matters MORE here: there is no native element underneath, a scanner is just a keyboard
Content 3 / 3 the opener carries the required wrong-choice clause — "Not for form validation on an ordinary screen" — and links the alternative (the field's own message), naming the boundary as physical distance from the screen
Fit 3 / 3 prescribed in the RF/warehouse screens and nowhere else — NOT by the field matrix, which covers field types and has no scan row (corrected 2026-09-06, roadmap 286.4; the clause replaced here read "prescribed where the field matrix puts it" while `scan` appears 0 times on /concepts/design-language): 4 of 39 pattern screens run a live [data-scan-input] — goods-receipt, rf-count, rf-pick, rf-putaway — and every one of the four now links it from its own Components-used badges; adopted by po-app's own receive screen (data-scan-input + data-scan-flash), and counted inside rf-essentials' 40 kB RF budget gate (RF_BUDGET_KB = 40). Corrected 2026-09-05 (roadmap 279.1): this read "linked from /patterns/goods-receipt and /patterns/rf-landing", which is the component page's own OUTBOUND Related list published as an inbound fact — neither of those two linked it, and all three that did went unnamed. Count ELEMENTS in the DOM across frames; grepping the built HTML counts an inline <code>data-scan-input</code> in prose as a use.
↑↓ move Enter open Esc close