Skip to main content
import { Flyout, Button } from 'reshaped';

function Example() {
  return (
    <Flyout position="bottom-start">
      <Flyout.Trigger>
        {(attributes) => <Button attributes={attributes}>Open Flyout</Button>}
      </Flyout.Trigger>
      
      <Flyout.Content>
        <View padding={4} backgroundColor="neutral" borderRadius="medium">
          <Text>Custom flyout content</Text>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

Usage

Flyout is a low-level component for building custom floating content. It handles positioning, animations, and interactions. Use Popover, DropdownMenu, or Tooltip for common use cases.

Props

position
string
Position relative to the trigger element.Options: "bottom", "bottom-start", "bottom-end", "top", "top-start", "top-end", "start", "start-top", "start-bottom", "end", "end-top", "end-bottom"Default: "bottom"
active
boolean
Controlled state for flyout visibility.
defaultActive
boolean
Initial active state for uncontrolled mode.
triggerType
string
Event that triggers the flyout.Options: "click", "hover", "focus"Default: "click"
width
string
Width of the flyout. Accepts CSS values or "trigger" to match trigger width.
<Flyout width="400px" />
<Flyout width="trigger" />
forcePosition
boolean
Force the specified position without fallbacks.
fallbackPositions
Position[] | false
Fallback positions when content doesn’t fit. Set to false to disable.
<Flyout fallbackPositions={['top', 'bottom-start']} />
<Flyout fallbackPositions={false} />
fallbackAdjustLayout
boolean
Adjust size and position when fallbacks don’t work.
fallbackMinHeight
string
Minimum height when adjusting layout.
trapFocusMode
string | false
Focus trap behavior and keyboard shortcuts.Options: "dialog", "action-menu", "selection-menu", false
<Flyout trapFocusMode="dialog" /> {/* Trap focus like a modal */}
<Flyout trapFocusMode={false} /> {/* No focus trap */}
disabled
boolean
Disable the flyout.
disableHideAnimation
boolean
Disable the hide animation.
disableContentHover
boolean
Ignore content hover events (for triggerType="hover").
disableCloseOnOutsideClick
boolean
Prevent closing when clicking outside.
autoFocus
boolean
default:"true"
Focus first focusable element when opened.
contentGap
number
Gap between trigger and content in pixels.
contentShift
number
Shift the content on the secondary axis.
contentMaxHeight
string
Maximum height for scrollable content.
contentMaxWidth
string
Maximum width for the content.
contentZIndex
number
Custom z-index for the content.
contentClassName
string
Additional CSS class for the content element.
contentAttributes
Attributes<'div'>
Additional HTML attributes for the content element.
onOpen
() => void
Callback when the flyout opens.
onClose
(args: { reason?: string }) => void
Callback when the flyout closes.Reasons: "escape-key", "outside-click", "item-selection", "close-button"
groupTimeouts
boolean
Remove display delay if another flyout is already active.
originCoordinates
Coordinates
Position coordinates when there’s no trigger element.
<Flyout originCoordinates={{ x: 100, y: 200 }}>
containerRef
React.RefObject<HTMLElement>
Container element for positioning.
positionRef
React.RefObject<HTMLElement>
Element to calculate position against.
initialFocusRef
React.RefObject<HTMLElement>
Element to focus when opened.
instanceRef
React.Ref<FlyoutInstance>
Ref to access flyout methods.
const flyoutRef = React.useRef();

// Methods:
flyoutRef.current.open();
flyoutRef.current.close();
flyoutRef.current.updatePosition();

Subcomponents

Flyout.Trigger

Render function that provides attributes for the trigger element.
<Flyout.Trigger>
  {(attributes) => <button attributes={attributes}>Trigger</button>}
</Flyout.Trigger>

Flyout.Content

Container for the flyout content.
<Flyout.Content>
  {/* Your custom content */}
</Flyout.Content>
className
string
Additional CSS class for the content element.
attributes
Attributes<'div'>
Additional HTML attributes for the content element.

Examples

Basic Flyout

<Flyout>
  <Flyout.Trigger>
    {(attributes) => <Button attributes={attributes}>Open</Button>}
  </Flyout.Trigger>
  
  <Flyout.Content>
    <View padding={3} backgroundColor="neutral" borderRadius="medium">
      <Text>Flyout content</Text>
    </View>
  </Flyout.Content>
</Flyout>

Custom Color Picker

function ColorPicker({ value, onChange }) {
  const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A'];

  return (
    <Flyout>
      <Flyout.Trigger>
        {(attributes) => (
          <Button attributes={attributes}>
            <View
              width="20px"
              height="20px"
              borderRadius="small"
              attributes={{ style: { backgroundColor: value } }}
            />
          </Button>
        )}
      </Flyout.Trigger>
      
      <Flyout.Content>
        <View
          padding={2}
          backgroundColor="neutral"
          borderRadius="medium"
          elevation="overlay"
        >
          <Grid columns="4" gap={1}>
            {colors.map((color) => (
              <Button
                key={color}
                variant="ghost"
                size="small"
                onClick={() => onChange(color)}
              >
                <View
                  width="32px"
                  height="32px"
                  borderRadius="small"
                  attributes={{ style: { backgroundColor: color } }}
                />
              </Button>
            ))}
          </Grid>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

Emoji Picker

function EmojiPicker({ onSelect }) {
  const emojis = ['😀', '😂', '😍', '🎉', '👍', '❤️', '🔥', '✨'];

  return (
    <Flyout>
      <Flyout.Trigger>
        {(attributes) => (
          <Button attributes={attributes} icon={IconSmile}>
            Add Reaction
          </Button>
        )}
      </Flyout.Trigger>
      
      <Flyout.Content>
        <View
          padding={2}
          backgroundColor="neutral"
          borderRadius="medium"
          elevation="overlay"
        >
          <View direction="row" gap={1}>
            {emojis.map((emoji) => (
              <Button
                key={emoji}
                variant="ghost"
                onClick={() => {
                  onSelect(emoji);
                }}
              >
                <Text variant="title-5">{emoji}</Text>
              </Button>
            ))}
          </View>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

Date Range Picker

function DateRangePicker({ startDate, endDate, onChange }) {
  return (
    <Flyout width="600px">
      <Flyout.Trigger>
        {(attributes) => (
          <Button attributes={attributes} icon={IconCalendar}>
            {startDate && endDate
              ? `${formatDate(startDate)} - ${formatDate(endDate)}`
              : 'Select dates'}
          </Button>
        )}
      </Flyout.Trigger>
      
      <Flyout.Content>
        <View
          padding={4}
          backgroundColor="neutral"
          borderRadius="medium"
          elevation="overlay"
        >
          <View direction="row" gap={3}>
            <Calendar
              value={startDate}
              onChange={(date) => onChange({ start: date, end: endDate })}
            />
            <Calendar
              value={endDate}
              onChange={(date) => onChange({ start: startDate, end: date })}
            />
          </View>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

Search with Results

function SearchFlyout() {
  const [query, setQuery] = React.useState('');
  const [results, setResults] = React.useState([]);

  return (
    <Flyout
      triggerType="focus"
      width="trigger"
      disableContentHover={false}
    >
      <Flyout.Trigger>
        {(attributes) => (
          <TextField
            attributes={attributes}
            value={query}
            onChange={(e) => {
              setQuery(e.target.value);
              // Fetch results...
            }}
            placeholder="Search..."
          />
        )}
      </Flyout.Trigger>
      
      <Flyout.Content>
        <View
          backgroundColor="neutral"
          borderRadius="medium"
          elevation="overlay"
          contentMaxHeight="400px"
        >
          <ScrollArea>
            <View gap={1} padding={2}>
              {results.map((result) => (
                <Button
                  key={result.id}
                  variant="ghost"
                  onClick={() => handleSelect(result)}
                >
                  <View direction="row" gap={2} align="center">
                    <Icon svg={result.icon} />
                    <Text>{result.title}</Text>
                  </View>
                </Button>
              ))}
            </View>
          </ScrollArea>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

Trigger Types

// Click (default)
<Flyout triggerType="click">

// Hover
<Flyout triggerType="hover">

// Focus
<Flyout triggerType="focus">

Controlled Flyout

function ControlledFlyout() {
  const [active, setActive] = React.useState(false);

  return (
    <Flyout
      active={active}
      onClose={() => setActive(false)}
    >
      <Flyout.Trigger>
        {(attributes) => (
          <Button
            attributes={attributes}
            onClick={() => setActive(!active)}
          >
            Toggle
          </Button>
        )}
      </Flyout.Trigger>
      
      <Flyout.Content>
        <View padding={3}>
          <Text>Content</Text>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

Without Trigger (Coordinates)

function ContextualFlyout({ x, y, active, onClose }) {
  return (
    <Flyout
      active={active}
      onClose={onClose}
      originCoordinates={{ x, y }}
    >
      <Flyout.Content>
        <View padding={3} backgroundColor="neutral" borderRadius="medium">
          <Text>Content at {x}, {y}</Text>
        </View>
      </Flyout.Content>
    </Flyout>
  );
}

useFlyout Hook

Access flyout context for advanced use cases:
import { useFlyout } from 'reshaped';

function CustomFlyoutContent() {
  const flyout = useFlyout();

  return (
    <Button onClick={() => flyout.close()}>
      Close Flyout
    </Button>
  );
}

When to Use

Use Flyout for:
  • Custom floating UI that doesn’t fit Popover/DropdownMenu/Tooltip patterns
  • Building domain-specific components (date pickers, color pickers, etc.)
  • Advanced positioning requirements
Don’t use Flyout for:
  • Standard menus (use DropdownMenu)
  • Simple tooltips (use Tooltip)
  • Form popovers (use Popover)
  • Modal dialogs (use Modal)

Accessibility

  • Provide appropriate ARIA attributes via contentAttributes
  • Set trapFocusMode based on your content type
  • Ensure keyboard accessibility for trigger elements
  • Use semantic HTML for content
  • Test with screen readers
  • Consider providing keyboard shortcuts

Build docs developers (and LLMs) love