Master Detail
Responsive list/detail pattern: side by side on wide viewports, one pane at a time on narrow ones.
| Package | @vizejs/ui/master-detail |
| Maturity | stable |
| Own the source | vize lib pull master-detail |
| Requires | controllable-state |
| Aliases | list detail, split view, two pane layout, adaptive list |
| Covers | Material list-detail canonical layout, Apple split view, Fluent master-detail pattern |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { MasterDetail } from "@vizejs/ui/master-detail";
Or copy the source into your project with vize lib pull master-detail (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Mailbox list with a detail pane side by side on wide screens and one pane at a time on narrow ones. -->
<script setup lang="ts">
import { ref } from "vue";
import { MasterDetail } from "@vizejs/ui/master-detail";
type Mailbox = "inbox" | "sent" | "archive";
const mailboxes: readonly Mailbox[] = ["inbox", "sent", "archive"];
const selected = ref<Mailbox | null>(null);
</script>
<template>
<MasterDetail
v-model:selected="selected"
:ssr-width="1024"
master-label="Mailboxes"
detail-label="Messages"
>
<template #master="{ select, selected: current }">
<ul>
<li v-for="mailbox in mailboxes" :key="mailbox">
<button
type="button"
:aria-current="current === mailbox ? 'true' : undefined"
@click="() => select(mailbox)"
>
{{ mailbox }}
</button>
</li>
</ul>
</template>
<template #detail="{ selected: current, back }">
<h2>{{ current }}</h2>
<button type="button" @click="back">Back to mailboxes</button>
</template>
<template #empty>
<p>Choose a mailbox to read its messages.</p>
</template>
</MasterDetail>
</template>
API
MasterDetail<Key extends string | number>
Source: src/families/layout/master-detail/master-detail.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
selected |
Key | null |
undefined |
Controlled selection (v-model:selected). undefined selects uncontrolled behavior. |
defaultSelected |
Key | null |
null |
Initial uncontrolled selection. |
splitAt |
number |
768 |
Minimum viewport width in CSS pixels for the side-by-side layout. |
ssrWidth |
number |
undefined |
Viewport width assumed during server rendering and hydration. Without it the stacked layout is rendered until mount. |
masterSize |
string |
"minmax(16rem, 1fr)" |
CSS grid track of the master pane in split layout. |
detailSize |
string |
"2fr" |
CSS grid track of the detail pane in split layout. |
masterLabel |
string |
undefined |
Accessible name of the master region. |
detailLabel |
string |
undefined |
Accessible name of the detail region. |
Events
| Event | Payload | Description |
|---|---|---|
update:selected |
[selected: Key | null] |
Fired with the next selection (null when going back). |
layoutChange |
[layout: MasterDetailLayout] |
Fired when the layout switches between split and stacked, including the post-mount switch away from the SSR layout. |
Slots
| Slot | Slot props | Description |
|---|---|---|
master |
MasterDetailMasterSlotState<Key> |
The list. Receives the selection, layout, and a select action. |
detail |
MasterDetailDetailSlotState<Key> |
The selected item's detail. Receives the selection, layout, and a back action. |
empty |
MasterDetailEmptySlotState |
Placeholder shown in split layout while nothing is selected. |
Exposed
| Member | Type | Description |
|---|---|---|
layout |
— | |
select |
— | |
back |
— | |
selected |
— |
Behavior
Normative state x input -> outcome table for master-detail.vue
(@vizejs/ui/master-detail). Rows are proven by master-detail.test.ts and
master-detail-ssr.test.ts; compile-only assertions live in
master-detail.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| MD1 | wide viewport | render, select | split grid shows master and detail (or empty); selecting emits update:selected |
split layout shows both panes and an empty placeholder |
| MD2 | narrow viewport | select, Escape, Back | one pane at a time; focus moves to the detail on select and back to the list on Escape/Back | stacked layout swaps panes, moves focus, and returns with Back or Escape |
| MD3 | controlled / resize | click, set prop, resize | controlled selection follows the prop; layoutChange reports split/stacked switches |
controlled selection and layout changes are reported |
| MD4 | SSR with ssrWidth |
isolated requests | byte-identical split or stacked markup; stacked is used without ssrWidth |
renders byte-identical split or stacked markup from ssrWidth |
| MD5 | hydration | mount over server markup | no mismatch warnings | hydrates the ssrWidth layout without mismatch warnings |
| MD6 | DOM/SSR/Vapor | compile | the SFC compiles in every renderer lane | scripts/check-renderers.ts |
Both panes are labelled section regions with tabindex="-1" so focus can be
moved to them programmatically without adding tab stops.