Skip to contents

A dock_board exposes a single argument that controls panel arrangement: the layouts. This vignette walks through every shape layouts accepts, shows what UI each shape produces, and ends with patterns for the layouts you’ll build most often.

The big picture

Internally, every board carries a dock_layouts collection. This is a named list of one or more views (the global tabs you see at the top of the app). A view’s content is a dock_layout: the per-view panel arrangement, stored as a tree of block / extension IDs. Panel content is derived from the board’s blocks and extensions on demand — only the arrangement is stored.

Single-page boards are the degenerate case: a dock_layouts with one auto-named view called "Page". The view-nav dropdown is always present; it just has one entry until you add more.

When you pass a layouts = argument to new_dock_board(), the constructor normalises it to a dock_layouts:

You pass Treated as Final shape
A named list(A = ..., B = ...) multi-view (each name is a view) dock_layouts collection
A dock_layout single page wrapped as Page slot
A raw list (grid spec) single-page raw grid, resolved on the way in wrapped as Page slot

The dispatch is name-driven: a list with top-level names becomes multi-view (one slot per name); an unnamed list (or a dock_layout) is treated as a single-page grid spec. To get multiple views, name the top-level entries.

So you only ever need to think about two things: the grid syntax for arranging panels inside a view, and named-list syntax for having more than one view.

Starting from scratch

The simplest possible board: no blocks, no extensions, no layouts. Since new_dock_board() defaults to layouts = default_layout(blocks, extensions) — which returns an empty dock_layout here — and the empty layout gets wrapped as a single Page view, the resulting collection is a dock_layouts with one empty Page slot:

┌────────────────────────────┐
│  Page  [+]                 │
├────────────────────────────┤
│                            │
│             ⊕              │
│                            │
│   Start by adding a panel  │
│                            │
│       [ Add panel ]        │
│                            │
└────────────────────────────┘

You get a single auto-named Page view holding the watermark prompt. Clicking Add panel would normally open the panel picker, but here the board has no blocks or extensions to choose from, so the picker says so and points you to add a block first. From this empty starting point you can grow the board interactively.

When you do supply blocks or extensions but still no layouts, the default Page is auto-populated by default_layout(): blocks alone become a single row of panels; blocks together with extensions become the familiar sidebar-and-main shape (extensions on the left, blocks on the right). Pass layouts = only to override that.

Single-page raw grid

The simplest layout: a list (or character vector) of block/extension IDs. The shape of the list determines the grid.

The two rules to internalise:

  1. List nesting alternates orientation. The top level lays its children out horizontally; one level of nesting flips to vertical; another level flips back to horizontal, and so on.
  2. Character vectors create tabs. A vector of IDs lives inside one DockView panel; a list of IDs gets split into multiple panels.

One panel

A single ID gives one panel filling the whole view:

new_dock_board(
  blocks = c(a = new_dataset_block()),
  layouts = list("a")
)
┌────────────────────────────┐
│  Page  [+]                 │
├────────────────────────────┤
│                            │
│            a               │
│                            │
└────────────────────────────┘

Two panels side by side

Two top-level entries → two columns split horizontally:

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  layouts = list("a", "b")
)
┌────────────────────────────┐
│  Page  [+]                 │
├─────────────┬──────────────┤
│             │              │
│      a      │      b       │
│             │              │
└─────────────┴──────────────┘

Two panels stacked vertically

Wrap the entries in one extra layer of list() to introduce a vertical split:

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  layouts = list(list("a", "b"))
)
┌────────────────────────────┐
│  Page  [+]                 │
├────────────────────────────┤
│            a               │
├────────────────────────────┤
│            b               │
└────────────────────────────┘

The outer list still describes a horizontal split, but with only one child that “split” is a single full-width column. The inner list("a", "b") is at depth 1, so it splits vertically: a stacks on top of b.

Tabs (multiple views in one panel)

Use a character vector (not a list) to put multiple panels in the same DockView panel as tabs:

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  layouts = list(c("a", "b"))
)
┌────────────────────────────┐
│  Page  [+]                 │
├────────────────────────────┤
│ ┌──────┐┌──────┐           │
│ │  a   ││  b   │           │
│ └──────┘└──────┘           │
│                            │
│  (a is shown, b is a tab)  │
│                            │
└────────────────────────────┘

list("a", "b") (panels split) and list(c("a", "b")) (panels tabbed) look almost identical in source but produce very different UIs. The list/vector distinction flips between split a panel and tabify a panel.

Nested grids

Combine the two rules to build any layout.

Two columns, the right one stacked

new_dock_board(
  blocks = c(
    a = new_dataset_block(),
    b = new_head_block(),
    c = new_head_block()
  ),
  layouts = list("a", list("b", "c"))
)
┌────────────────────────────┐
│  Page  [+]                 │
├─────────────┬──────────────┤
│             │      b       │
│      a      ├──────────────┤
│             │      c       │
└─────────────┴──────────────┘

Two columns, both stacked

new_dock_board(
  blocks = c(
    a = new_dataset_block(),
    b = new_head_block(),
    c = new_head_block(),
    d = new_head_block()
  ),
  layouts = list(list("a", "b"), list("c", "d"))
)
┌────────────────────────────┐
│  Page  [+]                 │
├─────────────┬──────────────┤
│      a      │      c       │
├─────────────┼──────────────┤
│      b      │      d       │
└─────────────┴──────────────┘

Three rows in one column

Add a third level of nesting to flip back to horizontal inside the vertical stack. Useful when you want a row that holds two panels side-by-side:

new_dock_board(
  blocks = c(
    a = new_dataset_block(),
    b = new_head_block(),
    c = new_head_block()
  ),
  layouts = list(list("a", list("b", "c")))
)
┌────────────────────────────┐
│  Page  [+]                 │
├────────────────────────────┤
│            a               │
├─────────────┬──────────────┤
│      b      │      c       │
└─────────────┴──────────────┘

A common dashboard shape: a narrow column on the left holding an extension, and a tabbed panel on the right with several blocks:

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  extensions = new_edit_board_extension(),
  layouts = list("edit_board_extension", c("a", "b"))
)
┌────────────────────────────┐
│  Page  [+]                 │
├─────────┬──────────────────┤
│         │ ┌────┐┌────┐     │
│  edit   │ │ a  ││ b  │     │
│         │ └────┘└────┘     │
│         │                  │
│         │   (tabs)         │
└─────────┴──────────────────┘

This is also what default_layout() produces when both blocks and extensions are present: extensions on the left, blocks on the right.

Multiple views (pages)

Pass a named list. Each named entry becomes a separate page in the view-nav dropdown:

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  extensions = new_edit_board_extension(),
  layouts = list(
    Analysis = list("a", "b"),
    Editor = list("edit_board_extension")
  )
)

Analysis view (active by default, since the first view wins):

┌────────────────────────────┐
│ Analysis  [+]   ← view nav │
├────────────────────────────┤
│      a       │      b      │
└────────────────────────────┘

Switching to Editor via the dropdown:

┌────────────────────────────┐
│ Editor  [+]                │
├────────────────────────────┤
│                            │
│           edit             │
│                            │
└────────────────────────────┘

Each slot value follows exactly the same grid syntax as the single-page form: character vectors for tabs, nested lists for splits.

Choosing the initially active view

By default, the first layout is active. To start on a different one, construct that view’s content with dock_layout(..., active = TRUE):

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  extensions = new_edit_board_extension(),
  layouts = list(
    Analysis = list("a", "b"),
    Editor = dock_layout("edit_board_extension", active = TRUE)
  )
)
┌────────────────────────────┐
│ Editor  [+]   ← starts here│
├────────────────────────────┤
│           edit             │
└────────────────────────────┘

dock_layout(..., active = TRUE) attaches an active attribute to the layout. The validator rejects more than one layout marked active. If none is marked, the first one wins.

Empty views

A view with no panels is fine. Switching to it shows the same watermark prompt as the empty default board (see Starting from scratch), scoped to that tab.

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  layouts = list(
    Analysis = list("a", "b"),
    Empty = list()
  )
)

Putting it all together

A pot-pourri exercising every feature: multiple views, nested grids, tabbed panels, an extension as a sidebar, and an explicit active view.

new_dock_board(
  blocks = c(
    raw = new_dataset_block(),
    cleaned = new_head_block(),
    summary = new_head_block(),
    plot1 = new_scatter_block(),
    plot2 = new_scatter_block()
  ),
  extensions = new_edit_board_extension(),
  links = list(
    new_link("raw", "cleaned", "data"),
    new_link("cleaned", "summary", "data"),
    new_link("cleaned", "plot1", "data"),
    new_link("cleaned", "plot2", "data")
  ),
  layouts = list(
    Data = dock_layout(
      "edit_board_extension",
      panels("raw", "cleaned", active = "cleaned"),
      sizes = c(0.25, 0.75)
    ),
    Analysis = dock_layout(
      group("summary", "plot1", sizes = c(0.4, 0.6)),
      "plot2",
      sizes = c(0.55, 0.45)
    ),
    Charts = dock_layout(panels("plot1", "plot2"), active = TRUE)
  )
)

The board has three views. Charts is marked active, so the user lands there first.

Charts (active on load): one tabbed panel with plot1 shown and plot2 selectable as a tab.

┌────────────────────────────┐
│  Charts  [+]               │
├────────────────────────────┤
│ ┌───────┐┌───────┐         │
│ │ plot1 ││ plot2 │         │
│ └───────┘└───────┘         │
│                            │
│  (plot1 shown, plot2 tab)  │
│                            │
└────────────────────────────┘

Data: a slim extension sidebar on the left holding the editor, and a wide right column with raw / cleaned tabbed. active = "cleaned" opens the cleaned tab by default; sizes = c(0.25, 0.75) carves out the narrow sidebar.

┌────────────────────────────┐
│  Data  [+]                 │
├──────┬─────────────────────┤
│      │┌─────┐┌─────────┐   │
│ edit ││ raw ││ cleaned │   │
│      │└─────┘└─────────┘   │
│      │  (cleaned shown)    │
└──────┴─────────────────────┘
  25%          75%

Analysis: two top-level columns at 55/45. The left column is a nested group() with a 40/60 vertical split (summary on top, plot1 below); the right column is a single panel (plot2).

┌────────────────────────────┐
│  Analysis  [+]             │
├─────────────┬──────────────┤
│   summary   │              │   40% / 55%
├─────────────┤    plot2     │
│             │              │
│    plot1    │              │   60% / 45%
│             │              │
└─────────────┴──────────────┘
     55%             45%

Custom split ratios and active tabs

The list-of-IDs sugar splits space evenly and opens the first panel in a tabbed group. For non-default ratios or a non-default open tab, escalate to the typed constructors:

  • dock_layout(..., sizes = c(...)) — sizes parallel to the root children. The numbers are relative; they don’t have to sum to 1.
  • group(..., sizes = c(...)) — same, for a nested branch (any level below the root).
  • panels(..., active = "...") — tabbed leaf with an explicit open tab. Equivalent to a character-vector child, except you also get to pick the active tab.

A 30/70 split

dock_layout(..., sizes =) runs parallel to the children passed in .... With two children the layout splits horizontally; with two sizes it does so unevenly:

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  layouts = list(
    Main = dock_layout("a", "b", sizes = c(0.3, 0.7))
  )
)
┌────────────────────────────┐
│  Main  [+]                 │
├──────────┬─────────────────┤
│          │                 │
│    a     │        b        │
│          │                 │
└──────────┴─────────────────┘
   30%             70%

Stacking with sizes

Same idea, vertical layout: pass orientation = "vertical" so the root split runs top-to-bottom instead of left-to-right.

new_dock_board(
  blocks = c(a = new_dataset_block(), b = new_head_block()),
  layouts = list(
    Main = dock_layout("a", "b",
                       orientation = "vertical",
                       sizes = c(0.25, 0.75))
  )
)
┌────────────────────────────┐
│  Main  [+]                 │
├────────────────────────────┤
│            a               │   25%
├────────────────────────────┤
│                            │
│            b               │   75%
│                            │
└────────────────────────────┘

Choosing the open tab

panels() builds a tabbed leaf. Without active, the first ID opens by default — same behaviour as a bare character vector. Pass active to open a different tab on load:

new_dock_board(
  blocks = c(
    a = new_dataset_block(),
    b = new_head_block(),
    c = new_head_block()
  ),
  layouts = list(
    Main = dock_layout(panels("a", "b", "c", active = "b"))
  )
)
┌────────────────────────────┐
│  Main  [+]                 │
├────────────────────────────┤
│ ┌────┐┌──────┐┌────┐       │
│ │ a  ││  b   ││ c  │       │
│ └────┘└──────┘└────┘       │
│         ↑                  │
│   b is open by default     │
│                            │
└────────────────────────────┘

Sizes inside a nested branch

group() is the equivalent of an inner list(...) but with its own sizes. Use it whenever you need ratios on a non-root branch:

new_dock_board(
  blocks = c(
    a = new_dataset_block(),
    b = new_head_block(),
    c = new_head_block()
  ),
  layouts = list(
    Main = dock_layout(
      "a",
      group("b", "c", sizes = c(0.6, 0.4)),
      sizes = c(0.3, 0.7)
    )
  )
)
┌────────────────────────────┐
│  Main  [+]                 │
├────────┬───────────────────┤
│        │      b            │   inner: 60% top
│        ├───────────────────┤
│   a    │      c            │   inner: 40% bottom
│        │                   │
│        │                   │
└────────┴───────────────────┘
   30%          70%

Outer sizes = c(0.3, 0.7) controls the root split (left / right); inner group(..., sizes = c(0.6, 0.4)) controls the right column’s internal stack.

Cheat-sheet

Goal Syntax
One panel list("a")
Two side-by-side panels list("a", "b")
Two stacked panels list(list("a", "b"))
Tabbed single panel list(c("a", "b"))
Sidebar + main list("ext", "main")
Two columns, both stacked list(list("a", "b"), list("c", "d"))
Custom split ratio dock_layout("a", "b", sizes = c(0.3, 0.7))
Custom open tab dock_layout(panels("a", "b", active = "b"))
Vertical top-level split dock_layout("a", "b", orientation = "vertical")
Multiple views list(A = ..., B = ...)
Start on view B list(A = ..., B = dock_layout(..., active = TRUE))
Empty starter view list(Page = list())

Where to go from here