Skip to main content
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

  1. Focus — the highlighted region draws the eye
  2. Zoom — enlarged content commands attention
  3. Annotations — floating text labels pull focus
  4. 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

PurposeLinesNotes
Problem example (the mess)30-80Show the scale of the problem. Don’t read it all—focus on the worst parts.
Solution code5-25Focused on the teaching point. Every line earns its place.
Type definitions5-15Compact. Type blocks are read, not walked through.
Walkthrough traces6-10Step-by-step execution. Short columns of state.
Full implementation20-40The “real thing.” Requires zoom choreography.
Config/boilerplate3-8Show 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:
TypeDefault iconRepresents
[service]AWS EC2Application components, microservices, API endpoints
[database]AWS RDSDatabases, persistent storage
[client]AWS ClientEnd users, browsers, mobile clients
[user]AWS UserUser silhouette, actors in a system
[server]AWS EC2Server instances, compute nodes
[cache]AWS ElastiCacheCaches, CDNs, in-memory stores
[queue]AWS SQSMessage queues, job queues
[message-queue]AWS SQSMessaging-specific queues, event streams
[load-balancer]AWS ELBLoad balancers, traffic distribution
[api-gateway]AWS API GatewayAPI 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 typeSweet spotUpper limitNotes
Array / Stack / Queue4-8 elements16Beyond 16, cells become illegible at 1x
Linked list3-6 nodes10Chain gets wide fast
Tree7-15 nodes31 (5 levels)Deeper than 5 levels needs pan/zoom
B-tree3-7 nodes12Wide nodes eat horizontal space
Trie8-15 nodes25Radix tree with compressed edges stays compact
Hash map4-8 buckets12Chains of 2-3 per bucket maximum
Graph4-8 nodes12Force layout degrades beyond 12
Bit array8-16 cells32Smaller cells than arrays, but still limited
Matrix3×3 to 5×58×8Cell 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):
[1, 3, 5, 7, 9, 8, 6]
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:
  1. Enter — show the primary visualization (typewriter for code, grow for data)
  2. Explore — walk through regions with focus, zoom, annotate
  3. Build — show additional blocks (split for comparison, new blocks for context)
  4. Conclude — zoom out, clear focus, summarize
  5. 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.

Build docs developers (and LLMs) love