Visual design in Handhold courses balances clarity, hierarchy, and engagement. Every scene should have one primary visualization that commands attention.
Visual Hierarchy
One Primary Visualization Per Scene
Every scene has one thing the learner should look at. Everything else supports it. If two visualizations compete for attention, the learner looks at neither.
In split mode: one panel is primary (the thing you’re explaining), the other is secondary (the output, the comparison, the context).
What Drives Attention
- Focus — the highlighted region draws the eye
- Zoom — enlarged content commands attention
- Annotations — floating text labels pull focus
- Animation — the moving thing wins
If everything is focused, nothing is focused. If everything is annotated, the annotations become noise.
Code Block Design
Sizing by Purpose
| Purpose | Lines | Notes |
|---|
| Problem example (the mess) | 30-80 | Show the scale of the problem. Don’t read it all—focus on the worst parts. |
| Solution code | 5-25 | Focused on the teaching point. Every line earns its place. |
| Type definitions | 5-15 | Compact. Type blocks are read, not walked through. |
| Walkthrough traces | 6-10 | Step-by-step execution. Short columns of state. |
| Full implementation | 20-40 | The “real thing.” Requires zoom choreography. |
| Config/boilerplate | 3-8 | Show quickly, don’t dwell. Use none animation. |
The 15-Line Rule
Any code block over 15 lines MUST have zoom choreography. This is not optional.
At normal scale, lines beyond ~18 are below the viewport. The learner hears you describe “the return statement on line 25” while looking at line 8.
Fix: zoom to 1.2x-1.3x on focused regions so the focused code is legible.
Code Quality
The code in your blocks is teaching material. It must be:
- Correct — no bugs, no pseudo-code (unless explicitly labeled)
- Idiomatic — use the language’s conventions and modern syntax
- Minimal — strip everything not relevant to the teaching point
- Readable — short variable names are fine for algorithms; descriptive names for application code
Inline annotations (// ! text) should be used sparingly for permanent callouts that shouldn’t depend on focus state.
Region Design
Plan regions before writing narration. Regions map to the concepts you’ll explain:
function dijkstra(graph, start) {
const dist = new Map() // Region: init (lines 2-5)
const visited = new Set()
for (const node of graph.nodes) {
dist.set(node, node === start ? 0 : Infinity)
}
while (visited.size < graph.nodes.length) {
const u = closest(dist, visited) // Region: pick (lines 7-9)
if (!u) break
visited.add(u) // Region: mark (line 10)
for (const [v, w] of graph.neighbors(u)) { // Region: relax (lines 11-17)
if (visited.has(v)) continue
const alt = dist.get(u) + w
if (alt < dist.get(v)) { // Region: check (line 14)
dist.set(v, alt) // Region: update (line 15)
}
}
}
return dist
}
Each region is a concept: init, pick, mark, relax, check, update. Not “lines 2-5” or “the top part.”
Split Layout Design
The Two-Panel Convention
Left panel: the thing you’re building or explaining (code, algorithm, definition).
Right panel: what it produces or how it compares (preview, diagram, data structure, alternative).
This convention means the learner always knows where to look: code is left, result is right.
When to Split
- Code + output: Show code on the left, rendered preview on the right
- Before + after: Show the problem on the left, the solution on the right
- Algorithm + trace: Show code on the left, execution state on the right
- Two approaches: Compare naive vs. optimized side by side
When NOT to Split
- Single visualization that needs full width (long code, wide diagram)
- Three or more things to show (use sequential show/hide instead)
- Unrelated visualizations (if they’re not being compared, don’t split)
- Permanent layout (split is temporary—enter, compare, exit)
Split Timing
The right panel should enter slightly after the left:
{{split}} {{show: source slide 0.3s}} {{show: preview slide 0.5s spring}}
This creates a 1-2 beat where the learner’s eye goes to the left panel first (it appeared first), then the right panel arrives. The staggered entry prevents visual overload.
Preview Design
Interactive Over Static
When possible, use template=react to make previews interactive. Clickable buttons, typing inputs, state changes—these make the learner engage physically, not just visually.
{{split}} {{show: counter-code}} {{show: counter-preview slide 0.5s spring}} Click the button on the right. Watch the count.
Encourage interaction in the narration: “Click it.” “Type something.” “Add a notification.”
Static for Design Teaching
For CSS, layout, and design topics, use raw HTML previews. The styling is the point, not the interactivity.
Container Queries
Preview iframes resize with the layout. In split mode, each panel is roughly half the stage width. Preview content MUST handle this gracefully.
Prefer @container queries over @media queries:
<style>
.wrapper { container-type: inline-size; }
.card { display: flex; flex-direction: column; }
@container (min-width: 400px) {
.card { flex-direction: row; }
}
</style>
Preview Simplicity
Preview code runs in an iframe with no build tools. For React previews:
- Use
React.createElement calls (SWC compiles JSX to this)
- No imports—React and ReactDOM are globals
- Keep state simple—useState and basic event handlers
- No external dependencies, no fetch calls, no timers
Diagram Design
Node Type Semantics
Diagram nodes render as icons with a label below (not boxes with text inside). By default every node gets an AWS icon matching its type.
Choose node types that communicate the component’s role:
| Type | Default icon | Represents |
|---|
[service] | AWS EC2 | Application components, microservices, API endpoints |
[database] | AWS RDS | Databases, persistent storage |
[client] | AWS Client | End users, browsers, mobile clients |
[user] | AWS User | User silhouette, actors in a system |
[server] | AWS EC2 | Server instances, compute nodes |
[cache] | AWS ElastiCache | Caches, CDNs, in-memory stores |
[queue] | AWS SQS | Message queues, job queues |
[message-queue] | AWS SQS | Messaging-specific queues, event streams |
[load-balancer] | AWS ELB | Load balancers, traffic distribution |
[api-gateway] | AWS API Gateway | API gateways, edge routers |
Icon Overrides
Override the default AWS icon with icon=aws:<key> inside the bracket:
cdn [service icon=aws:cloudfront]
auth [service icon=aws:cognito]
storage [database icon=aws:s3]
Edge Labels
Edge labels explain relationships, not just connections:
api --reads--> cache
api --writes--> db
client --HTTP--> api
queue --consumes--> worker
Good labels are verbs or protocols. Bad labels are redundant: api --connects--> db (of course it connects—the arrow says that).
Groups
Use groups to show architectural boundaries:
{Backend: api, db, cache}
{Frontend: client, cdn}
Groups visually cluster nodes, making the architecture scannable.
Layout Selection
| Layout | Best for | Example |
|--------|----------|---------||
| force | General architectures, any graph | System diagrams, network topologies |
| tree | Hierarchies, dependency trees | Component trees, org charts |
| ring | Cycles, small balanced graphs | State machines, circular dependencies |
| grid | Matrix-like structures | Grid computations, 2D data |
| bipartite | Two-sided relationships | Matching problems, client-server |
Data Structure Design
General Sizing Rules
| Structure type | Sweet spot | Upper limit | Notes |
|---|
| Array / Stack / Queue | 4-8 elements | 16 | Beyond 16, cells become illegible at 1x |
| Linked list | 3-6 nodes | 10 | Chain gets wide fast |
| Tree | 7-15 nodes | 31 (5 levels) | Deeper than 5 levels needs pan/zoom |
| B-tree | 3-7 nodes | 12 | Wide nodes eat horizontal space |
| Trie | 8-15 nodes | 25 | Radix tree with compressed edges stays compact |
| Hash map | 4-8 buckets | 12 | Chains of 2-3 per bucket maximum |
| Graph | 4-8 nodes | 12 | Force layout degrades beyond 12 |
| Bit array | 8-16 cells | 32 | Smaller cells than arrays, but still limited |
| Matrix | 3×3 to 5×5 | 8×8 | Cell text must remain readable |
Trees
The n-ary tree layout handles any branching factor. Use indentation format for general trees and array format for heaps:
Indentation (n-ary):
(nav)
(a:Home)
(a:About)
(a:Contact)
Array (binary heap):
Annotated (red-black, AVL):
(7:B)
(3:R)
(1:B)
(5:B)
(10:R)
Tree depth beyond 5 levels requires zoom choreography. Pan to reach distant subtrees.
Hash Maps
Vertical bucket column on the left, horizontal chains extending right. Show bucket indices. Keep chains short (2-3 per bucket) for readability:
0: (alice 555-1234) -> (bob 555-5678)
1:
2: (charlie 555-9012)
3:
Empty buckets are visible but dimmed—they show the sparseness.
Scene Flow
The Shape of a Step
Every step follows a pattern:
- Enter — show the primary visualization (typewriter for code, grow for data)
- Explore — walk through regions with focus, zoom, annotate
- Build — show additional blocks (split for comparison, new blocks for context)
- Conclude — zoom out, clear focus, summarize
- Exit — clear the scene for the next step
Not every step has all five phases, but the general shape holds.
Within a Section: Show/Hide/Focus
Use show/hide/focus for changes within a conceptual section. These imply continuity:
{{show: hash-fn}} Here's the function.
{{focus: signature}} The signature.
{{focus: loop}} The loop.
{{focus: none}} The whole thing.
No clears. The block persists. Focus shifts within it. The learner feels continuity.
Between Sections: Clear
Use clear for hard breaks between ideas:
{{focus: none}} {{zoom: 1x}} That's the hash function.
{{clear: slide}}
{{show: collision-demo grow 0.5s spring}} But what happens when two keys collide?
The clear tells the learner: done with that idea, new idea starting. The slide animation provides directional momentum.
Pacing
One trigger every 2-5 seconds of narration. For a 30-second paragraph, that’s 6-15 triggers. For a 10-second paragraph, that’s 2-5 triggers.
If you have a 15-second paragraph with one trigger, the screen is static for too long. Break it up with more focus shifts, zooms, or annotations.