suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Patterns19
  • Overview
  • Action Bar
  • Action Menu
  • App Shell
  • Command Palette
  • Confirm
  • Context Actions
  • Date Picker
  • Flyout
  • Modal
  • Navbar
  • Paged List
  • Panel
  • Picker
  • Preview
  • Search Picker
  • Segment Picker
  • Split View
  • Table List
  • Trail

suiss/ui · Pattern

Flyout

A non-modal panel anchored to a trigger on desktop that becomes a bottom drawer on mobile.

Flyout is an open-source pattern from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Flyout shows supplementary content, such as a small form or a share link, next to the control that opened it. On desktop it renders a Popover positioned against the trigger. Below the 48rem breakpoint it renders a bottom Drawer with a swipe handle, and each part switches to its drawer counterpart: FlyoutHeader, FlyoutTitle and FlyoutDescription map to the drawer's header, title and description, and FlyoutBody gains padding.

Use it for quick settings or filters that do not need a full modal.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/flyout

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add patterns flyout

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react
  1. Add the pieces it builds on: Drawer, Popover, use-mobile.

  2. Copy the source into components/patterns/flyout.tsx:

  1. Update the import paths to match your project.

Usage

import { Flyout, FlyoutTrigger, FlyoutContent, FlyoutHeader, FlyoutTitle, FlyoutDescription, FlyoutBody, FlyoutClose } 
<































<































The flyout is uncontrolled with defaultOpen, or controlled with open and onOpenChange. align, alignOffset, side and sideOffset on FlyoutContent position the desktop popover and are ignored on mobile. Put interactive content in FlyoutBody, and use FlyoutClose for a button that closes the flyout after an action.

Examples

With close button

Adds an icon close button to the mobile drawer with showCloseButton.

With close label

Adds a text close button labelled Done to the mobile drawer with closeLabel.

With close

Uses FlyoutClose on a Copy link button so the action also closes the flyout.

Floating

Insets the mobile drawer from the screen edges with floating.

API Reference

Anatomy8 parts
<Flyout>
<FlyoutTrigger />
<FlyoutContent>
<FlyoutHeader>
<FlyoutTitle />
<FlyoutDescription />
</FlyoutHeader>
<FlyoutBody
01

Flyout

The root that renders a Popover on desktop and a Drawer on mobile.

PropTypeDefaultDescription
onOpenChange(open: boolean) => void-Called when the flyout opens or closes.
floatingboolean-Insets the mobile drawer from the screen edges with fully rounded corners.

Also accepts the props of React.ComponentProps<typeof Popover>.

02

FlyoutTrigger

The button that opens the flyout.

PropTypeDefaultDescription
renderReact.ReactElement-Replaces the default button element with your own element, such as a Button.

Also accepts the props of React.ButtonHTMLAttributes<HTMLButtonElement>.

On the radix base, set asChild and pass your element as the child instead of render.

03

FlyoutContent

The popup; a PopoverContent on desktop and a DrawerContent on mobile.

PropTypeDefaultDescription
showCloseButtonboolean-Shows a close button in the mobile drawer.
closeLabelstring-Replaces the drawer's close icon with this text; requires showCloseButton.

Also accepts the props of React.HTMLAttributes<HTMLElement>, React.ComponentProps<typeof PopoverContent>.

04

FlyoutHeader

Groups the title and description.

Also accepts the props of React.ComponentProps<"div">.

05

FlyoutTitle

The flyout's title, used as its accessible name.

Also accepts the props of React.HTMLAttributes<HTMLHeadingElement>.

06

FlyoutDescription

Supporting text, used as the flyout's accessible description.

Also accepts the props of React.HTMLAttributes<HTMLParagraphElement>.

07

FlyoutBody

The main content area, padded on mobile.

Also accepts the props of React.ComponentProps<"div">.

08

FlyoutClose

A button that closes the flyout.

PropTypeDefaultDescription
renderReact.ReactElement-Replaces the default button element with your own element, such as a Button.

Also accepts the props of React.ButtonHTMLAttributes<HTMLButtonElement>.

On the radix base, set asChild and pass your element as the child instead of render.

Accessibility

  • The popup has the dialog role, named by FlyoutTitle and described by FlyoutDescription.
  • Opening moves focus into the flyout.
  • Escape closes it and returns focus to the trigger; pressing outside also closes it.

Notes

Parts carry data-slot values from flyout through flyout-close. showCloseButton and closeLabel only affect the mobile drawer; on desktop, add a FlyoutClose if the popover needs an explicit close control.

NextModalPreviousDate Picker

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • With close button
    • With close label
    • With close
    • Floating
  4. 04API Reference
    • Flyout
    • FlyoutTrigger
    • FlyoutContent
    • FlyoutHeader
    • FlyoutTitle
    • FlyoutDescription
    • FlyoutBody
    • FlyoutClose
  5. 05Accessibility
  6. 06Notes
from
"@/components/patterns/flyout"
div
className
=
"flex min-h-svh items-center justify-center"
>
<Flyout>
<FlyoutTrigger render={<Button variant="outline" />}>
Dimensions
</FlyoutTrigger>
<FlyoutContent>
<FlyoutHeader>
<FlyoutTitle>Dimensions</FlyoutTitle>
<FlyoutDescription>
Set the dimensions for the layer.
</FlyoutDescription>
</FlyoutHeader>
<FlyoutBody className="grid gap-3">
<div className="grid grid-cols-3 items-center gap-3">
<Label htmlFor="flyout-width">Width</Label>
<Input
id="flyout-width"
defaultValue="100%"
className="col-span-2"
/>
</div>
<div className="grid grid-cols-3 items-center gap-3">
<Label htmlFor="flyout-height">Height</Label>
<Input
id="flyout-height"
defaultValue="25px"
className="col-span-2"
/>
</div>
</FlyoutBody>
</FlyoutContent>
</Flyout>
</div>
div
className
=
"flex min-h-svh items-center justify-center"
>
<Flyout>
<FlyoutTrigger asChild>
<Button variant="outline">Dimensions</Button>
</FlyoutTrigger>
<FlyoutContent>
<FlyoutHeader>
<FlyoutTitle>Dimensions</FlyoutTitle>
<FlyoutDescription>
Set the dimensions for the layer.
</FlyoutDescription>
</FlyoutHeader>
<FlyoutBody className="grid gap-3">
<div className="grid grid-cols-3 items-center gap-3">
<Label htmlFor="flyout-width">Width</Label>
<Input
id="flyout-width"
defaultValue="100%"
className="col-span-2"
/>
</div>
<div className="grid grid-cols-3 items-center gap-3">
<Label htmlFor="flyout-height">Height</Label>
<Input
id="flyout-height"
defaultValue="25px"
className="col-span-2"
/>
</div>
</FlyoutBody>
</FlyoutContent>
</Flyout>
</div>
>
<FlyoutClose />
</FlyoutBody>
</FlyoutContent>
</Flyout>
import {
  Flyout,
  FlyoutBody,
  FlyoutContent,
  FlyoutDescription,
  FlyoutHeader,
  FlyoutTitle,
  FlyoutTrigger,
} from "@/components/patterns/flyout"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function FlyoutDemo() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Flyout>
        <FlyoutTrigger render={<Button variant="outline" />}>
          Dimensions
        </FlyoutTrigger>
        <FlyoutContent>
          <FlyoutHeader>
            <FlyoutTitle>Dimensions</FlyoutTitle>
            <FlyoutDescription>
              Set the dimensions for the layer.
            </FlyoutDescription>
          </FlyoutHeader>
          <FlyoutBody className="grid gap-3">
            <div className="grid grid-cols-3 items-center gap-3">
              <Label htmlFor="flyout-width">Width</Label>
              <Input
                id="flyout-width"
                defaultValue="100%"
                className="col-span-2"
              />
            </div>
            <div className="grid grid-cols-3 items-center gap-3">
              <Label htmlFor="flyout-height">Height</Label>
              <Input
                id="flyout-height"
                defaultValue="25px"
                className="col-span-2"
              />
            </div>
          </FlyoutBody>
        </FlyoutContent>
      </Flyout>
    </div>
  )
}
components/patterns/flyout.tsx
"use client"

import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
import * as React from "react"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import {
  Popover,
  PopoverContent,
import {
  Flyout,
  FlyoutBody,
  FlyoutContent,
  FlyoutDescription,
  FlyoutHeader,
  FlyoutTitle,
  FlyoutTrigger,
} from "@/components/patterns/flyout"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function FlyoutWithCloseButton() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Flyout>
        <FlyoutTrigger render={<Button variant="outline" />}>
          Filters
        </FlyoutTrigger>
        <FlyoutContent showCloseButton>
          <FlyoutHeader>
            <FlyoutTitle>Filters</FlyoutTitle>
            <FlyoutDescription>Narrow down the orders list.</FlyoutDescription>
          </FlyoutHeader>
          <FlyoutBody className="grid gap-2">
            <Label htmlFor="flyout-search">Customer</Label>
            <Input id="flyout-search" placeholder="Search by name" />
          </FlyoutBody>
        </FlyoutContent>
      </Flyout>
    </div>
  )
}
import {
  Flyout,
  FlyoutBody,
  FlyoutContent,
  FlyoutDescription,
  FlyoutHeader,
  FlyoutTitle,
  FlyoutTrigger,
} from "@/components/patterns/flyout"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function FlyoutWithCloseLabel() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Flyout>
        <FlyoutTrigger render={<Button variant="outline" />}>
          Filters
        </FlyoutTrigger>
        <FlyoutContent showCloseButton closeLabel="Done">
          <FlyoutHeader>
            <FlyoutTitle>Filters</FlyoutTitle>
            <FlyoutDescription>Narrow down the orders list.</FlyoutDescription>
          </FlyoutHeader>
          <FlyoutBody className="grid gap-2">
            <Label htmlFor="flyout-search">Customer</Label>
            <Input id="flyout-search" placeholder="Search by name" />
          </FlyoutBody>
        </FlyoutContent>
      </Flyout>
    </div>
  )
}
import {
  Flyout,
  FlyoutBody,
  FlyoutClose,
  FlyoutContent,
  FlyoutDescription,
  FlyoutHeader,
  FlyoutTitle,
  FlyoutTrigger,
} from "@/components/patterns/flyout"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export default function FlyoutWithClose() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Flyout>
        <FlyoutTrigger render={<Button variant="outline" />}>
          Share
        </FlyoutTrigger>
        <FlyoutContent>
          <FlyoutHeader>
            <FlyoutTitle>Share link</FlyoutTitle>
            <FlyoutDescription>
              Anyone with the link can view this project.
            </FlyoutDescription>
          </FlyoutHeader>
          <FlyoutBody className="grid gap-3">
            <Input
              readOnly
              aria-label="Project link"
              defaultValue="https://example.com/p/1042"
            />
            <FlyoutClose render={<Button />}>Copy link</FlyoutClose>
          </FlyoutBody>
        </FlyoutContent>
      </Flyout>
    </div>
  )
}
import {
  Flyout,
  FlyoutBody,
  FlyoutContent,
  FlyoutDescription,
  FlyoutHeader,
  FlyoutTitle,
  FlyoutTrigger,
} from "@/components/patterns/flyout"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function FlyoutFloating() {
  return (
    <div className="flex min-h-svh items-center justify-center">
      <Flyout floating>
        <FlyoutTrigger render={<Button variant="outline" />}>
          Dimensions
        </FlyoutTrigger>
        <FlyoutContent>
          <FlyoutHeader>
            <FlyoutTitle>Dimensions</FlyoutTitle>
            <FlyoutDescription>
              Set the dimensions for the layer.
            </FlyoutDescription>
          </FlyoutHeader>
          <FlyoutBody className="grid gap-3">
            <div className="grid grid-cols-3 items-center gap-3">
              <Label htmlFor="flyout-width">Width</Label>
              <Input
                id="flyout-width"
                defaultValue="100%"
                className="col-span-2"
              />
            </div>
            <div className="grid grid-cols-3 items-center gap-3">
              <Label htmlFor="flyout-height">Height</Label>
              <Input
                id="flyout-height"
                defaultValue="25px"
                className="col-span-2"
              />
            </div>
          </FlyoutBody>
        </FlyoutContent>
      </Flyout>
    </div>
  )
}