Well plate

A labelled laboratory plate with supplied well states, 44px selection controls and keyboard navigation across up to 1536 wells.

Plate 042

Arrow keys move between wells. Enter or Space selects a well.

Well1234
A
B
C

Inspect a plate

Plate 042 / Preparation record

Sample positions

Arrow keys move between wells. Enter or Space selects a well.

Well123456
A
B
C
D

Selected position: A1

Use it for

  • Sample placement and plate inspection with application-supplied rows, columns and well states.
  • Unique row and column labels and at most one record per coordinate. Missing records are labelled Unrecorded.
  • Use value and onValueChange for application-owned selection; readOnly provides a static plate.

Not for

  • Inferring empty wells, assay outcomes or sample identities from missing records.
  • Layouts larger than 1536 wells; the component reports the limit instead of silently truncating a plate.

Three ways in. They share one source, so the pixels match whichever you pick.

1. Import the package

npm install @noorddev/vlak-react

// once, next to your app's root
import "@noorddev/vlak-react/css";

import { WellPlate } from "@noorddev/vlak-react";

Precompiled React and one stylesheet. No compiler to configure. Per-component imports work too: @noorddev/vlak-react/components/well-plate.

2. Vendor the source

npx @noorddev/vlak-cli add well-plate

The StyleX leaf lands in components/vlak/, for your own compiler to own. See StyleX.

3. Through shadcn

npx shadcn add https://vlak.dev/r/well-plate.json

The same registry item, installed by shadcn's CLI.

CSS only

<link rel="stylesheet" href="node_modules/@noorddev/vlak/css/vlak.css" />

No React. Link vlak.css and use the markup and classes below.

import { WellPlate } from "@noorddev/vlak-react";

<WellPlate label="Plate 042" rows={["A", "B", "C"]} columns={["1", "2", "3", "4"]} wells={[
  { row: "A", column: "1", status: "Loaded", label: "Control" },
  { row: "A", column: "2", status: "Loaded", label: "Sample 042" },
  { row: "B", column: "1", status: "Reserved", disabled: true },
]} defaultValue={{ row: "A", column: "1" }} />

WellPlate

A plate of up to 1536 wells, with roving keyboard focus and explicit unrecorded cells.

PropTypeDefaultDescription
labelrequiredstring
rowsrequiredreadonly string[]
columnsrequiredreadonly string[]
wellsrequiredreadonly WellRecord[]
valueWellPosition | null
defaultValueWellPosition | nullnull
onValueChange(value: WellPosition) => void
descriptionReactNode
disabledbooleanfalse
readOnlybooleanfalseStatic well cells add no tab stops; their scroll container remains focusable.

Also accepts Omit<HTMLAttributes<HTMLDivElement>, "defaultValue">.

KeysDoes
TabEnters the plate at one enabled well and then leaves the grid.
Arrow keysMoves focus along a row or column, skipping disabled wells without changing selection.
Home, EndMoves to the first or last enabled well in the current row.
Ctrl+Home, Ctrl+EndMoves to the first or last enabled well in the plate.
Enter, SpaceSelects the focused well. Controlled selection waits for the caller's updated value.
<div class="rs-well-plate"><p class="rs-well-plate-label">Plate 042</p><div class="rs-well-plate-scroll"><table class="rs-well-plate-table"><caption>Supplied well records</caption><thead><tr><th class="rs-well-plate-heading" scope="col">Well</th><th class="rs-well-plate-heading" scope="col">1</th></tr></thead><tbody><tr><th class="rs-well-plate-heading" scope="row">A</th><td class="rs-well-plate-cell"><div class="rs-well-plate-well rs-well-plate-static"><span class="rs-well-plate-coordinate">A1</span><span>Loaded</span></div></td></tr></tbody></table></div></div>
.rs-well-plate.rs-well-plate-label.rs-well-plate-description.rs-well-plate-scroll.rs-well-plate-table.rs-well-plate-heading.rs-well-plate-cell.rs-well-plate-well.rs-well-plate-selected.rs-well-plate-unavailable.rs-well-plate-static.rs-well-plate-coordinate