Script Zone Dragger
A generic sort-cards-into-zones interaction, tuned for teaching script loading strategies. Each card is a script with a size in KB; each zone is a loading bucket (blocking, defer, async, lazy — or whatever your lesson needs). Drag a card or focus one and press a zone shortcut to move it. Per-zone totals roll up the KB sum so consumers can build a budget walkthrough on top.
Preview
Script loading strategy
Sort each script into the zone that matches how it should load.
Drag a card, or focus one and press 1 Blocking2 Defer3 Async4 Lazy←/→ cycle zones
Blocking1 script · 110KB
Runs before parse continues.
framework.js110KB
coreDefer2 scripts · 50KB
Runs after HTML parse.
router.js32KB
carousel.js18KB
Async2 scripts · 128KB
Runs as soon as ready.
analytics.js48KB
csv-parser.js80KB
worker-safeLazy1 script · 64KB
Loads on interaction or viewport.
chat-widget.js64KB · third-party
Customize
Layout
4
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/script-zone-dragger.jsonUsage
import { useState } from "react";
import {
ScriptZoneDragger,
type ScriptZoneAssignments,
} from "@craft-bits/core";
const [assignments, setAssignments] = useState<ScriptZoneAssignments>({
framework: "blocking",
analytics: "async",
chat: "lazy",
});
<ScriptZoneDragger
scripts={[
{ id: "framework", label: "framework.js", sizeKb: 110 },
{ id: "analytics", label: "analytics.js", sizeKb: 48 },
{ id: "chat", label: "chat-widget.js", sizeKb: 64 },
]}
zones={[
{ id: "blocking", label: "Blocking", keyShortcut: "1" },
{ id: "defer", label: "Defer", keyShortcut: "2" },
{ id: "async", label: "Async", keyShortcut: "3" },
{ id: "lazy", label: "Lazy", keyShortcut: "4" },
]}
assignments={assignments}
onAssignmentsChange={setAssignments}
/>Anatomy
- Header. Optional
titlerendered with thecb-labelstyle, optionaldescriptionsub-line, and aResetbutton that restores the assignments todefaultAssignments. Each is independently hideable. - Hint. A keyboard-shortcut hint sentence: one chip per zone shortcut plus arrow keys for cycling. Hide with
hideHint. - Zone grid. Auto-fit grid of zones — single column on mobile, two-up on tablet, dense
minmax(14rem, 1fr)on desktop. Each zone collects the scripts whose assignment matches its id. - Zone header. Zone label, count chip, and optional KB total. The zone tints its label and accent dot to the zone's
color. - Card. A draggable, focusable button with the script label, size, optional
metaline, and optionalbadge.
Understanding the component
- Assignments are a map. Scripts live in a flat array; their zone membership lives in a
Record<scriptId, zoneId>map. The component groups scripts at render time so consumers never own the per-zone array shape. Moving a script is a one-field map update. - Native drag, native drop. Every card is
draggable. Every zone runs the canonicaldragoverthenpreventDefaultopt-in — without it, the browser silently rejects the drop and the drop handler never fires. The drag payload travels throughdataTransferwith a custom MIME type plus atext/plainfallback. - Depth-counter hover state. A
useRefcounter ticks up ondragenterand down ondragleave. The zone reads as hovered when the counter is positive, which keeps the dashed border stable when the cursor crosses into a nested card child. - Keyboard fallback. Focus a card and press a zone's
keyShortcutto snap into that zone;ArrowRight/ArrowDowncycles forward,ArrowLeft/ArrowUpcycles back. The shortcut chips in the hint sentence match the actual zone shortcuts. - Controlled vs uncontrolled. Pass
assignmentsandonAssignmentsChangefor controlled; passdefaultAssignments(and skip both) for uncontrolled.onMovefires after every successful move with the script id and the source / target zones.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
scripts | ScriptZoneDraggerScript[] | required | Scripts to sort. |
zones | ScriptZoneDraggerZone[] | required | Drop zones the scripts can land in. |
assignments | ScriptZoneAssignments | — | Controlled script-id-to-zone-id map. |
defaultAssignments | ScriptZoneAssignments | {} | Uncontrolled initial assignments. |
defaultZoneId | string | first zone | Zone used for any unassigned script. |
onAssignmentsChange | (next) => void | — | Fired with the next full map after every move. |
onMove | (event) => void | — | Fired with { scriptId, fromZone, toZone } after every move. |
title | ReactNode | — | Optional heading above the zones. |
description | ReactNode | — | Optional sub-headline under the title. |
hideZoneTotals | boolean | false | Hide the per-zone KB readout. |
hideReset | boolean | false | Hide the Reset button. |
hideHint | boolean | false | Hide the keyboard hint sentence. |
headingAs | 'h2' | 'h3' | 'h4' | 'h3' | Tag for the title element. |
aria-label | string | — | Accessible name for the outer container. |
className | string | — | Merged onto the root via cn(). |
ScriptZoneDraggerScript
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier. |
label | ReactNode | Visible card label. |
sizeKb | number | Size in KB — feeds the per-zone total. |
meta | ReactNode | Optional sub-label rendered under the label. |
badge | ReactNode | Optional badge shown on the right. |
ScriptZoneDraggerZone
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier — matched against the assignments map. |
label | ReactNode | Visible heading. |
description | ReactNode | Optional helper text under the label. |
color | string | Optional CSS colour — drives the label tint, accent dot, and hover border. |
keyShortcut | string | Optional printable character that snaps the focused card into this zone. |
emptyLabel | ReactNode | Optional empty-state placeholder. |
Accessibility
- The wrapper is a
<section>withdata-cb-edu="script-zone-dragger". Each zone renders asrole="region"witharia-labelledbypointing at its heading. - Cards are focusable elements with
role="button", descriptivearia-label, and the nativedraggableattribute. Drags are realdragstartthendragover(withpreventDefault) thendropcycles. - Keyboard users press the zone's
keyShortcutto jump there, orArrowLeft/ArrowRight(alsoUp/Down) to cycle. The shortcut chips in the hint sentence stay in sync with the actual zone shortcuts. - Hovered zones expose
data-state="hover"so styling stays unified between pointer and drag-over paths. The drag-source card exposesdata-cb-dragging="true"for a translucent placeholder effect. - Hover state survives nested card children because
dragenter/dragleaveare counted with auseRefdepth counter, not a boolean — no flicker when the cursor crosses into the size or badge. - Reduced-motion preferences short-circuit every enter / exit / layout transition.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/perf-javascript/ui/ScriptZoneDragger.tsx). The original was wired to a project-leveljs-perf-simulatorengine, a hard-coded critical/deferred/worker triplet, and a Live TTI headline that summed parse and execute costs of the critical zone. This rewrite drops the engine coupling, the TTI headline, the bounce and insight banners, and the worker-safe rejection rule — consumers pass their ownscriptsandzones, the zone-shortcut keys live on each zone descriptor instead of a fixed1/2/3map, and the per-zone size sum is the only built-in summary.