Headless UI Float

website·Indexed 30 days ago

https://headlessui-float.vercel.app/

A package designed to simplify the positioning of floating elements within Headless UI components. It provides a <Float> component for React and Vue, featuring support for portals, transitions, arrows, and virtual elements (v0.11+). The library includes a Composable Mode for more flexible integration and options for adaptive width and custom render wrappers.

Tokens
14K
Snippets
89
Records
94
Agent score
72%

What's inside Headless UI Float

  1. Create a reusable High-Order Float component in Vue

    To avoid repeating the same configuration (like offsets, transitions, and portal settings) across multiple instances of the <Float> component, you can create a High-Order Component (HOC) wrapper. This wrapper encapsulates default props while still allowing individual instances to override them.
    <!-- HighOrderFloat.vue -->
    <template>
      <Float
        v-slot="slotProps"
        :offset="8"
        flip
        :shift="6"
        portal
        enter="transition duration-200 ease-out"
        enter-from="scale-95 opacity-0"
        enter-to="scale-100 opacity-100"
        leave="transition duration-150 ease-in"
        leave-from="scale-100 opacity-100"
        leave-to="scale-95 opacity-0"
        tailwindcss-origin-class
      >
        <slot v-bind="slotProps"></slot>
      </Float>
    </template>
    
    <script setup>
    import { Float } from '@headlessui-float/vue'
    </script>
  2. Build a custom virtual element with Float.Virtual

    Use <Float.Virtual> to create a floating element positioned relative to a non-DOM 'virtual' element.

    Key implementation details:

    1. Use the onInitial prop to access setShow, refs, and show.
    2. Call refs.setPositionReference() and provide an object with a getBoundingClientRect() method. This method must return the coordinates (x, y, top, left, etc.) where the element should appear.
    3. Use the portal prop to render the element at the bottom of the <body> to avoid z-index issues.
    4. Use useOutsideClick from @headlessui-float/react to handle closing the element when clicking away.
    import { useEffect } from 'react'
    import { Float, useOutsideClick } from '@headlessui-float/react'
    
    function ContextMenu() {
      function onInitial({ setShow, refs }) {
        function onContextMenu(e) {
          e.preventDefault()
    
          // Define the virtual position
          refs.setPositionReference({
            getBoundingClientRect() {
              return {
                width: 0,
                height: 0,
                x: e.clientX,
                y: e.clientY,
                top: e.clientY,
                left: e.clientX,
                right: e.clientX,
                bottom: e.clientY,
              }
            },
          })
    
          setShow(true)
        }
    
        useEffect(() => {
          document.addEventListener('contextmenu', onContextMenu)
          return () => document.removeEventListener('contextmenu', onContextMenu)
        }, [])
    
        // Close menu on outside click
        useOutsideClick(refs.floating, () => {
          setShow(false)
        })
      }
    
      return (
        <Float.Virtual portal onInitial={onInitial}>
          <div>Custom Context Menu Content</div>
        </Float.Virtual>
      )
    }
  3. Use Floating UI for complex positioning requirements

    Headless UI Float is built on top of Floating UI. If your project has complex positioning requirements that Headless UI Float cannot satisfy, you should use the Floating UI library directly.
  4. Install and configure Headless UI Float for Nuxt

    To use Headless UI Float in a Nuxt project, install the @headlessui-float/nuxt module and add it to your nuxt.config.ts file. Once configured, <Float> components are automatically available globally, eliminating the need for explicit imports in your Vue components.
    # Using npm
    npm i -D @headlessui-float/nuxt
    
    # Using yarn
    yarn add -D @headlessui-float/nuxt
    
    # Using pnpm
    pnpm add -D @headlessui-float/nuxt
    // nuxt.config.ts
    export default defineNuxtConfig({
      modules: ['@headlessui-float/nuxt'],
    })
    
  5. Install @headlessui-float/react

    The @headlessui-float/react package requires React v18, React DOM, and @headlessui/react v1 to be installed first.
    # Using npm
    npm i @headlessui-float/react
    
    # Using yarn
    yarn add @headlessui-float/react
    
    # Using pnpm
    pnpm add @headlessui-float/react
  6. Trap focus inside a Popover panel

    To automatically focus on internal links or form elements and lock focus within a popover when it opens, wrap the content of the PopoverPanel in the <FocusTrap> component from @headlessui/vue. This ensures that keyboard navigation remains contained within the popover until it is closed.
    <template>
      <Popover>
        <Float :offset="4">
          <PopoverButton class="px-3 py-1.5 flex justify-center items-center bg-rose-50 hover:bg-rose-100 text-rose-500 rounded">
            Button
          </PopoverButton>
    
          <PopoverPanel class="w-[240px] h-[70px] p-3 bg-white text-rose-500 border border-gray-200 rounded-md shadow-lg focus:outline-none">
            <FocusTrap>
              <button type="button" class="focus:ring">link 1</button>
              <button type="button" class="focus:ring">link 2</button>
            </FocusTrap>
          </PopoverPanel>
        </Float>
      </Popover>
    </template>
    
    <script setup>
    import {
      FocusTrap,
      Popover, PopoverButton, PopoverPanel,
    } from '@headlessui/vue'
    import { Float } from '@headlessui-float/vue'
    </script>
  7. Build a custom virtual element with FloatVirtual

    Use <FloatVirtual> to create floating elements positioned relative to a non-DOM 'virtual' element.

    Key implementation details:

    1. Initialization: Use the @initial event to access show (visibility), reference (the virtual anchor), and floating (the floating element).
    2. Defining the Anchor: Set reference.value to an object containing a getBoundingClientRect() method. This method must return an object with width, height, x, y, top, left, right, and bottom properties.
    3. Visibility: Toggle show.value = true to display the element.
    4. Portal: Use the portal prop to render the element at the bottom of the <body> to avoid z-index conflicts.
    5. Closing: Use the useOutsideClick hook provided by @headlessui-float/vue to close the menu when clicking away.
    <template>
      <FloatVirtual portal @initial="onInitial">
        <div class="bg-white shadow-md">context menu content</div>
      </FloatVirtual>
    </template>
    
    <script setup>
    import { computed, watchEffect } from 'vue'
    import { FloatVirtual, useOutsideClick } from '@headlessui-float/vue'
    
    function onInitial({ show, reference, floating }) {
      function onContextMenu(e) {
        e.preventDefault()
    
        // Define the virtual element's position
        reference.value = {
          getBoundingClientRect() {
            return {
              width: 0, height: 0,
              x: e.clientX, y: e.clientY,
              top: e.clientY, left: e.clientX,
              right: e.clientX, bottom: e.clientY,
            }
          },
        }
    
        show.value = true
      }
    
      watchEffect(onInvalidate => {
        document.addEventListener('contextmenu', onContextMenu)
        onInvalidate(() => document.removeEventListener('contextmenu', onContextMenu))
      })
    
      useOutsideClick(floating, () => {
        show.value = false
      }, computed(() => show.value))
    }
    </script>
  8. Use Floating UI for complex positioning requirements

    Headless UI Float is built on top of Floating UI. If your project requires complex positioning logic that Headless UI Float cannot achieve, it is recommended to use the Floating UI library directly.
  9. Add a visual arrow to a floating element

    To add an arrow to a floating element, place the <FloatArrow> component inside the floating element (e.g., a PopoverPanel).

    To ensure the arrow is positioned correctly and does not overlap the content:

    1. Use the :arrow prop on the <Float> component to set the padding between the arrow and the floating element container.
    2. Use the :offset prop on the <Float> component to set the distance between the arrow and the reference element.
    3. Wrap the floating element's content in a div with position: relative and a background color to prevent the arrow from appearing on top of the text.
    <template>
      <Popover>
        <Float
          placement="bottom-start"
          :offset="15"
          :arrow="5"
        >
          <PopoverButton class="px-5 py-2 bg-rose-50 hover:bg-rose-100 text-rose-500 rounded">
            Button
          </PopoverButton>
    
          <PopoverPanel class="w-[240px] h-[70px] bg-white border border-gray-200 rounded-md shadow-lg focus:outline-none">
            <!-- 1. The Arrow Component -->
            <FloatArrow class="absolute bg-white w-5 h-5 rotate-45 border border-gray-200" />
    
            <!-- 2. Content wrapper with relative positioning and background -->
            <div class="relative h-full bg-white p-3 text-rose-500 rounded-md">
              Popover & arrow, content...
            </div>
          </PopoverPanel>
        </Float>
      </Popover>
    </template>
    
    <script setup>
    import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
    import { Float, FloatArrow } from '@headlessui-float/vue'
    </script>
  10. Use Composable Mode to decouple floating elements

    By default, <Float> requires the reference and floating elements to be direct children. Enabling composable mode allows you to wrap the reference and floating elements in <FloatReference> and <FloatContent> respectively. This allows these elements to be placed anywhere within the <Float> component's DOM tree, providing greater flexibility for complex layouts.
    <template>
      <Popover>
        <Float composable>
          <!-- Reference element can now be nested deep in the DOM -->
          <div>
            <FloatReference>
              <PopoverButton>Options</PopoverButton>
            </FloatReference>
          </div>
    
          <!-- Floating element can also be nested -->
          <div>
            <FloatContent>
              <PopoverPanel>...</PopoverPanel>
            </FloatContent>
          </div>
        </Float>
      </Popover>
    </template>
    
    <script setup>
    import { Float, FloatContent, FloatReference } from '@headlessui-float/vue'
    </script>
  11. Position Headless UI components using the Float component

    The <Float> component from @headlessui-float/react allows you to easily position floating elements (like those in <Menu>, <Listbox>, <Popover>, or <Combobox>) using Floating UI.

    To use it, wrap the reference element (e.g., <Menu.Button>) and the floating element (e.g., <Menu.Items>) inside the <Float> component. The <Float> component must contain exactly two child elements: the first is the reference and the second is the floating element.

    When using <Float>, you should remove manual positioning classes (such as absolute, right-0, or mt-2) from the floating element and instead use the placement and offset props.

    import { Menu } from '@headlessui/react'
    import { Float } from '@headlessui-float/react'
    
    function Example() {
      return (
        <Menu>
          <Float placement="bottom-end" offset={4}>
            <Menu.Button className="...">
              Options
            </Menu.Button>
    
            <Menu.Items className="...">
              {/* Menu items content */}
            </Menu.Items>
          </Float>
        </Menu>
      )
    }