> Markdown version of [Master-Detail Layout](https://vaadin.com/docs/v24/components/master-detail-layout). Section index: [llms.txt](https://vaadin.com/docs/v24/components/llms.txt)

# Master-Detail Layout

Master-Detail Layout is component for building UIs with a horizontally or vertically split pair consisting of a master area and a detail area that can responsively switch to an overlay.

> **Note: Preview Feature**
>
> This is a preview version of Master-Detail Layout. You need to enable it with the [feature flag](https://vaadin.com/docs/v24/flow/configuration/feature-flags.md) `com.vaadin.experimental.masterDetailLayoutComponent`. Preview versions may lack some planned features, and breaking changes may be introduced in any Vaadin version. We encourage you to try it out and provide feedback to help us improve it.

> **Important: Scaled down examples**
>
> The examples on this page are scaled down so that their viewport-size-dependent behavior can be demonstrated. Some examples also change their behavior based on your browser viewport size.

In the example below, clicking a row in the table reveals the detail area which is rendered next to the master area by default. Drag the splitter to change the width of the layout: the detail area will switch to an overlay when the master area’s minimum width is reached.

`master-detail-layout-basic.tsx`

```typescript
link:include::../../../frontend/demo/component/master-detail-layout/react/master-detail-layout-basic.tsx[]
```

**Flow** — `MasterDetailLayoutBasic.java`

```java
MasterDetailLayout layout = new MasterDetailLayout();
/* Sets the minimum size of the flexible master area to 450px,
   below which the detail area will be rendered as an overlay:*/
layout.setMasterMinSize("450px");
/* Sets the detail area size to a static size: */
layout.setDetailSize("250px");
/* The detail area is revealed when it's populated with a component,
 * and hidden when the component is removed: */
personList.getGrid().asSingleSelect().addValueChangeListener(event -> {
    Person selectedPerson = event.getValue();
    if (selectedPerson != null) {
        personDetail.setPerson(selectedPerson);
        layout.setDetail(personDetail);
    } else {
        layout.setDetail(null);
    }
});

personDetail.addCloseListener(event -> personList.getGrid().deselectAll());
```

**Flow** — `PersonList.java`

```java
package com.vaadin.demo.component.masterdetaillayout;

import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;

import java.util.List;

public class PersonList extends VerticalLayout {
    private final Grid<Person> grid;

    public PersonList(List<Person> people) {
        Paragraph paragraph = new Paragraph("Select a person to view their details:");
        grid = new Grid<>();
        grid.addColumn(Person::getFirstName).setHeader("First Name");
        grid.addColumn(Person::getLastName).setHeader("Last Name");
        grid.addColumn(Person::getEmail).setHeader("Email");
        grid.addColumn(Person::getProfession).setHeader("Profession");
        grid.setItems(people);
        grid.setHeightFull();

        add(paragraph, grid);
        setHeightFull();
    }

    public Grid<Person> getGrid() {
        return grid;
    }
}
```

**Flow** — `PersonDetail.java`

```java
package com.vaadin.demo.component.masterdetaillayout;

import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.shared.Registration;

public class PersonDetail extends VerticalLayout {
    private final FormLayout formLayout;
    private final TextField firstNameField;
    private final TextField lastNameField;
    private final EmailField emailField;
    private final TextField professionField;

    public PersonDetail() {
        formLayout = new FormLayout();

        firstNameField = new TextField("First Name");
        firstNameField.setReadOnly(true);
        lastNameField = new TextField("First Name");
        lastNameField.setReadOnly(true);
        emailField = new EmailField("Email");
        emailField.setReadOnly(true);
        professionField = new TextField("Profession");
        professionField.setReadOnly(true);

        formLayout.add(firstNameField, lastNameField, emailField, professionField);

        Button closeButton = new Button("Close");
        closeButton.addClickListener(event -> fireEvent(new CloseEvent(this, false)));

        add(formLayout, closeButton);
        setPadding(true);
    }

    public void setPerson(Person person) {
        if (person != null) {
            firstNameField.setValue(person.getFirstName());
            lastNameField.setValue(person.getLastName());
            emailField.setValue(person.getEmail());
            professionField.setValue(person.getProfession());
        } else {
            firstNameField.clear();
            lastNameField.clear();
            emailField.clear();
            professionField.clear();
        }
    }

    public static class CloseEvent extends ComponentEvent<PersonDetail> {
        public CloseEvent(PersonDetail source, boolean fromClient) {
            super(source, fromClient);
        }
    }

    public Registration addCloseListener(ComponentEventListener<CloseEvent> listener) {
        return addListener(CloseEvent.class, listener);
    }
}
```

**React** — `master-detail-layout-basic.tsx`

```tsx
{/* The master area's size is flexible, and will shrink down to 450px,
    below which the fixed-size detail area will be rendered as an overlay: */}
<MasterDetailLayout masterMinSize="450px" detailSize="250px">
  <MasterDetailLayout.Master>
    <PersonList
      people={items.value}
      selectedPerson={selectedPerson.value}
      onSelect={(person) => {
        selectedPerson.value = person;
      }}
    />
  </MasterDetailLayout.Master>
  <MasterDetailLayout.Detail>
    {/* The detail area is revealed when it's populated with a component,
        and hidden when the component is removed: */}
    {selectedPerson.value ? (
      <PersonDetail
        person={selectedPerson.value}
        onClose={() => {
          selectedPerson.value = null;
        }}
      />
    ) : null}
  </MasterDetailLayout.Detail>
</MasterDetailLayout>
```

**React** — `PersonList.tsx`

```tsx
import React from 'react';
import { Grid, GridColumn, VerticalLayout } from '@vaadin/react-components';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

interface PersonListProps {
  people: Person[];
  selectedPerson: Person | null;

  onSelect(person: Person | null): void;
}

function PersonList({ people, selectedPerson, onSelect }: PersonListProps) {
  return (
    <VerticalLayout style={{ height: '100%', border: '1px solid var(--lumo-contrast-20pct)' }}>
      <div style={{padding:'var(--lumo-space-m)', fontWeight:'bold'}}>Select a person to view their details:</div>
      <Grid
        theme='no-border'
        items={people}
        style={{ height: '100%' }}
        selectedItems={selectedPerson ? [selectedPerson] : []}
        onActiveItemChanged={(e) => {
          onSelect(e.detail.value ?? null);
        }}
      >
        <GridColumn path="firstName" />
        <GridColumn path="lastName" />
        <GridColumn path="email" />
        <GridColumn path="profession" />
      </Grid>
    </VerticalLayout>
  );
}

export default PersonList;
```

**React** — `PersonDetail.tsx`

```tsx
import React from 'react';
import {
  Button,
  EmailField,
  FormLayout,
  TextField,
  VerticalLayout,
} from '@vaadin/react-components';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

interface PersonDetailProps {
  person: Person | null;
  onClose(): void;
}

function PersonDetail({ person, onClose }: PersonDetailProps) {
  return (
    <VerticalLayout theme="padding spacing">
      <FormLayout>
        <TextField label="First Name" value={person?.firstName ?? ''} readonly />
        <TextField label="Last Name" value={person?.lastName ?? ''} readonly />
        <EmailField label="Email" value={person?.email ?? ''} readonly />
        <TextField label="Profession" value={person?.profession ?? ''} readonly />
      </FormLayout>
      <Button onClick={onClose}>Close</Button>
    </VerticalLayout>
  );
}

export default PersonDetail;
```

## <a id="setting-the-overlay-breakpoint"></a>Setting the Overlay Breakpoint

For each area, either a *fixed* size or a *minimum* size can be specified. The area with a minimum size takes whatever space is available next to the fixed size area. If the available space is smaller than the minimum size, the detail area is rendered as an overlay.

**Flow**

```Java
layout.setMasterMinSize("600px");
layout.setDetailSize("300px");
```

**React**

```tsx
<MasterDetailLayout
  masterMinSize="600px"
  detailSize="300px"
/>
```

The two most common configurations are:

- A flexible master area with a minimum size, next to a fixed-size detail area;

- A fixed-size master area, next to a flexible detail area with a minimum size.

If both areas are configured to be flexible, whichever reaches its minimum size first determines the overlay breakpoint.

If neither a minimum or a fixed size is explicitly set, the layout tries to determine an appropriate minimum size for each area automatically based on its content.

## <a id="stack-mode"></a>Stack Mode

By default, the detail overlay is rendered as a drawer that opens from the right-hand side of the layout. By switching to *stack mode*, the two areas are rendered "stacked" on top of each other, with the detail area fully covering the master area, when rendered as an overlay.

**Flow**

```Java
layout.setOverlayMode(MasterDetailLayout.OverlayMode.STACK);
```

**React**

```tsx
<MasterDetailLayout stackOverlay />
```

## <a id="forced-overlay-mode"></a>Forced Overlay Mode

The layout can be configured to *always* render the detail area as an overlay (either as drawer or stack), regardless of minimum sizes:

**Flow**

```Java
layout.setForceOverlay(true);
```

**React**

```tsx
<MasterDetailLayout forceOverlay />
```

## <a id="overlay-containment-modes"></a>Overlay Containment Modes

The overlay can be configured to render in two different ways, called *containment modes*:

- **Layout**: The overlay only covers the master area (default);

- **Viewport**: The overlay covers the entire viewport (i.e. page).

**Flow**

```Java
layout.setContainment(MasterDetailLayout.Containment.VIEWPORT);
```

**React**

```tsx
<MasterDetailLayout containment="viewport" />
```

> **Note: Limited Modality with Viewport-Containment**
>
> Due to browser support constraints, the modality of the detail overlay in viewport containment mode is currently limited to pointer interactions. Elements behind the overlay can be focused by keyboard. This limitation is planned to be addressed in V25.

## <a id="orientation"></a>Orientation

By default, the Master-Detail Layout is split horizontally. This can be changed to a vertical split.

`master-detail-layout-vertical.tsx`

```typescript
import React, { useEffect } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import { MasterDetailLayout, SplitLayout } from '@vaadin/react-components';
import PersonDetail from 'Frontend/demo/component/master-detail-layout/react/PersonDetail';
import PersonList from 'Frontend/demo/component/master-detail-layout/react/PersonList';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

function Example() {
  const items = useSignal<Person[]>([]);
  const selectedPerson = useSignal<Person | null>(null);
  useEffect(() => {
    getPeople().then(({ people }) => {
      items.value = people;
    });
  }, []);

  return (
    <SplitLayout orientation="vertical" style={{ height: '100%' }}>
      {/* tag::snippet[] */}
      <MasterDetailLayout
        orientation="vertical"
        masterMinSize="150px"
        detailSize="250px"
      >
        {/* end::snippet[] */}
        <MasterDetailLayout.Master>
          <PersonList
            people={items.value}
            selectedPerson={selectedPerson.value}
            onSelect={(person) => {
              selectedPerson.value = person;
            }}
          />
        </MasterDetailLayout.Master>
        <MasterDetailLayout.Detail>
          {selectedPerson.value ? (
            <PersonDetail
              person={selectedPerson.value}
              onClose={() => {
                selectedPerson.value = null;
              }}
            />
          ) : null}
        </MasterDetailLayout.Detail>
        {/* tag::snippet[] */}
      </MasterDetailLayout>
      {/* end::snippet[] */}
      <div style={{ flex: '0 0 auto', backgroundColor: 'var(--lumo-contrast-5pct)', textAlign: 'center' }}>
        <span style={{fontWeight: 'bold', minHeight: '1.75em'}}>Drag to resize</span>
      </div>
    </SplitLayout>
  );
}
```

**Flow** — `MasterDetailLayoutVertical.java`

```java
MasterDetailLayout layout = new MasterDetailLayout();
layout.setOrientation(MasterDetailLayout.Orientation.VERTICAL);
```

**Flow** — `PersonList.java`

```java
package com.vaadin.demo.component.masterdetaillayout;

import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;

import java.util.List;

public class PersonList extends VerticalLayout {
    private final Grid<Person> grid;

    public PersonList(List<Person> people) {
        Paragraph paragraph = new Paragraph("Select a person to view their details:");
        grid = new Grid<>();
        grid.addColumn(Person::getFirstName).setHeader("First Name");
        grid.addColumn(Person::getLastName).setHeader("Last Name");
        grid.addColumn(Person::getEmail).setHeader("Email");
        grid.addColumn(Person::getProfession).setHeader("Profession");
        grid.setItems(people);
        grid.setHeightFull();

        add(paragraph, grid);
        setHeightFull();
    }

    public Grid<Person> getGrid() {
        return grid;
    }
}
```

**Flow** — `PersonDetail.java`

```java
package com.vaadin.demo.component.masterdetaillayout;

import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.shared.Registration;

public class PersonDetail extends VerticalLayout {
    private final FormLayout formLayout;
    private final TextField firstNameField;
    private final TextField lastNameField;
    private final EmailField emailField;
    private final TextField professionField;

    public PersonDetail() {
        formLayout = new FormLayout();

        firstNameField = new TextField("First Name");
        firstNameField.setReadOnly(true);
        lastNameField = new TextField("First Name");
        lastNameField.setReadOnly(true);
        emailField = new EmailField("Email");
        emailField.setReadOnly(true);
        professionField = new TextField("Profession");
        professionField.setReadOnly(true);

        formLayout.add(firstNameField, lastNameField, emailField, professionField);

        Button closeButton = new Button("Close");
        closeButton.addClickListener(event -> fireEvent(new CloseEvent(this, false)));

        add(formLayout, closeButton);
        setPadding(true);
    }

    public void setPerson(Person person) {
        if (person != null) {
            firstNameField.setValue(person.getFirstName());
            lastNameField.setValue(person.getLastName());
            emailField.setValue(person.getEmail());
            professionField.setValue(person.getProfession());
        } else {
            firstNameField.clear();
            lastNameField.clear();
            emailField.clear();
            professionField.clear();
        }
    }

    public static class CloseEvent extends ComponentEvent<PersonDetail> {
        public CloseEvent(PersonDetail source, boolean fromClient) {
            super(source, fromClient);
        }
    }

    public Registration addCloseListener(ComponentEventListener<CloseEvent> listener) {
        return addListener(CloseEvent.class, listener);
    }
}
```

**React** — `master-detail-layout-vertical.tsx`

```tsx
<MasterDetailLayout
  orientation="vertical"
  masterMinSize="150px"
  detailSize="250px"
>
</MasterDetailLayout>
```

**React** — `PersonList.tsx`

```tsx
import React from 'react';
import { Grid, GridColumn, VerticalLayout } from '@vaadin/react-components';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

interface PersonListProps {
  people: Person[];
  selectedPerson: Person | null;

  onSelect(person: Person | null): void;
}

function PersonList({ people, selectedPerson, onSelect }: PersonListProps) {
  return (
    <VerticalLayout style={{ height: '100%', border: '1px solid var(--lumo-contrast-20pct)' }}>
      <div style={{padding:'var(--lumo-space-m)', fontWeight:'bold'}}>Select a person to view their details:</div>
      <Grid
        theme='no-border'
        items={people}
        style={{ height: '100%' }}
        selectedItems={selectedPerson ? [selectedPerson] : []}
        onActiveItemChanged={(e) => {
          onSelect(e.detail.value ?? null);
        }}
      >
        <GridColumn path="firstName" />
        <GridColumn path="lastName" />
        <GridColumn path="email" />
        <GridColumn path="profession" />
      </Grid>
    </VerticalLayout>
  );
}

export default PersonList;
```

**React** — `PersonDetail.tsx`

```tsx
import React from 'react';
import {
  Button,
  EmailField,
  FormLayout,
  TextField,
  VerticalLayout,
} from '@vaadin/react-components';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';

interface PersonDetailProps {
  person: Person | null;
  onClose(): void;
}

function PersonDetail({ person, onClose }: PersonDetailProps) {
  return (
    <VerticalLayout theme="padding spacing">
      <FormLayout>
        <TextField label="First Name" value={person?.firstName ?? ''} readonly />
        <TextField label="Last Name" value={person?.lastName ?? ''} readonly />
        <EmailField label="Email" value={person?.email ?? ''} readonly />
        <TextField label="Profession" value={person?.profession ?? ''} readonly />
      </FormLayout>
      <Button onClick={onClose}>Close</Button>
    </VerticalLayout>
  );
}

export default PersonDetail;
```

## <a id="hiding-the-detail-area"></a>Hiding the Detail Area

The detail area is automatically shown or hidden when content is added or removed from it. There is no explicit property to control the visibility of the detail area.

Master-Detail Layout exposes the following events that can be used to hide the detail area by removing the content from it:

- `backdrop-click`: Fired when the user clicks on the backdrop of the detail overlay when it is in drawer mode.

- `detail-escape-press`: Fired when the user presses `Escape` within the detail area.

**Flow**

```java
layout.addBackdropClickListener(event -> {
    // Hide the detail area by removing the content
    layout.setDetail(null);
});

layout.addDetailEscapePressListener(event -> {
    // Hide the detail area by removing the content
    layout.setDetail(null);
});
```

**React**

```tsx
const selectedPerson = useSignal<Person | null>(null);

const closeDetail = () => {
  // Clear selected person to not render the detail content
  selectedPerson.value = null;
};

<MasterDetailLayout onBackdropClick={closeDetail} onDetailEscapePress={closeDetail}>
  <MasterDetailLayout.Master>
    <PersonList onSelect={(person) => { selectedPerson.value = person}} />
  </MasterDetailLayout.Master>
  <MasterDetailLayout.Detail>
    {selectedPerson.value && <PersonDetail person={selectedPerson.value} />}
  </MasterDetailLayout.Detail>
</MasterDetailLayout>
```

## <a id="router-integration"></a>Router Integration

Master-Detail Layout can be used as a router layout (see [Flow](https://vaadin.com/docs/v24/flow/routing/layout.md)/[Hilla](https://vaadin.com/docs/v24/hilla/guides/routing.md#adding-layout-routes)), so that nested views are automatically rendered in the details area of the component. This allows showing nested views without having to manage the contents of the details area manually when the route changes, while providing the same benefits such as responsive behavior that the component normally provides.

The Flow `MasterDetailLayout` component implements the `RouterLayout` interface. When using a view class that extends from `MasterDetailLayout` as a layout for a nested view, that view is then automatically shown in the details area of the component.

The example below shows how to set up a master and a detail view. The master view is `ProductListView`, which would show a list of products, and the detail view is `ProductDetailView`, which shows information about a specific product. The `ProductListView` extends from `MasterDetailLayout`, so that it can be used as a route layout by the detail view. It also configures a `@Route` so that it can be navigated to by itself. Assuming there is a main layout for the application, for example one using `AppLayout`, it configures that as a parent layout. The `ProductDetailView` configures a `@Route`, using the `ProductListView` as the route layout.

With this setup, when navigating to `/products`, the layout would only show the product list. When navigating to a product detail, for example `/products/1`, it would then also show the product details next to, or on top of, the product list.

**Flow**

```java
@ParentLayout(MainLayout.class)
@Route(value = "products", layout = MainLayout.class)
public class ProductListView extends MasterDetailLayout { ... }

@Route(value = "products/:productId", layout = ProductListView.class)
public class ProductDetailView extends VerticalLayout { ... }
```

The `MasterDetailLayout` React component can be integrated with Hilla’s routing mechanism, so that any nested views automatically show up in the details area of the component.

The example below shows how to set up a master and a detail view. The master view is `ProductListView`, which would show a list of products, and the detail view is `ProductDetailView`, which shows information about a specific product. `ProductListView` is a layout component that renders a `MasterDetailLayout` component. The layout component retrieves the nested child view using `useOutlet` and renders it into the details area. Depending on whether a nested child view is present, `MasterDetailLayout` will automatically show or hide the details area. `ProductDetailView` is just a regular component, there is no specific configuration necessary to make it work. `EmptyProductDetailView` returns `null` so that the detail area is closed by default.

With this setup, when navigating to `/products`, the layout would only show the product list. When navigating to a product detail, for example `/products/1`, it would then also show the product details next to, or on top of, the product list.

**Hilla**

```tsx
// frontend/views/products/@layout.tsx
import { useOutlet } from 'react-router';

export default function ProductListView() {
  const childView = useOutlet();

  return (
    <MasterDetailLayout>
      <MasterDetailLayout.Master>
        <ProductList />
      </MasterDetailLayout.Master>
      <MasterDetailLayout.Detail>
        { childView }
      </MasterDetailLayout.Detail>
    </MasterDetailLayout>
  );
}

// frontend/views/products/@index.tsx
export default function EmptyProductDetailView() {
  return null
}

// frontend/views/products/{productId}/@index.tsx
export default function ProductDetailView() { ... }
```

## <a id="related-components"></a>Related Components

| Component                                                              | Usage Recommendation                                                           |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [Split Layout](https://vaadin.com/docs/v24/components/split-layout.md) | A component with two content areas and a draggable resize handle between them. |
