Headless UI v1.4: The One With Tabs

Date

We just released Headless UI v1.4, which includes a brand new Tab component, and new APIs for manually closing Popover and Disclosure components more easily.

Headless UI v1.4

Tabs

Earlier this year we started working on Tailwind UI Ecommerce, and we realized pretty quickly we were going to need to support tabs in Headless UI to be able to build the new interfaces we were designing.

Product details interface design from Tailwind UI Ecommerce.

Here’s what we ended up with:

import { Tab } from '@headlessui/react'

function MyTabs() {
  return (
    <Tab.Group>
      <Tab.List>
        <Tab>Tab 1</Tab>
        <Tab>Tab 2</Tab>
        <Tab>Tab 3</Tab>
      </Tab.List>
      <Tab.Panels>
        <Tab.Panel>Content 1</Tab.Panel>
        <Tab.Panel>Content 2</Tab.Panel>
        <Tab.Panel>Content 3</Tab.Panel>
      </Tab.Panels>
    </Tab.Group>
  )
}
<template>
  <TabGroup>
    <TabList>
      <Tab>Tab 1</Tab>
      <Tab>Tab 2</Tab>
      <Tab>Tab 3</Tab>
    </TabList>
    <TabPanels>
      <TabPanel>Content 1</TabPanel>
      <TabPanel>Content 2</TabPanel>
      <TabPanel>Content 3</TabPanel>
    </TabPanels>
  </TabGroup>
</template>

<script>
  import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'

  export default {
    components: {
      TabGroup,
      TabList,
      Tab,
      TabPanels,
      TabPanel,
    },
  }
</script>

And yep, those are tabs!

Like all Headless UI components, this totally abstracts away stuff like keyboard navigation for you so you can create custom tabs in a completely declarative way, without having to think about any of the tricky accessibility details.

Check out the documentation to learn more.

Closing Disclosures and Popovers

Up until now, there was no way to close a Disclosure without clicking the actual button used to open it. For typical disclosure use cases this isn’t a big deal, but it often makes sense to use disclosures for things like mobile navigation, where you want to close it when someone clicks a link inside of it.

Now you can use Disclosure.Button or (DisclosureButton in Vue) within your disclosure panel to close the panel, making it easy to wrap up things like links or other buttons so the panel doesn’t stay open:

import { Disclosure } from '@headlessui/react'
import MyLink from './MyLink'

function MyDisclosure() {
  return (
    <Disclosure>
      <Disclosure.Button>Open mobile menu</Disclosure.Button>
      <Disclosure.Panel>
        <Disclosure.Button as={MyLink} href="/home">
          Home
        </Disclosure.Button>
        {/* ... */}
      </Disclosure.Panel>
    </Disclosure>
  )
}
<template>
  <Disclosure>
    <DisclosureButton>Open mobile menu</DisclosureButton>
    <DisclosurePanel>
      <DisclosureButton :as="MyLink" href="/home">Home</DisclosureButton>
      <!-- ... -->
    </DisclosurePanel>
  </Disclosure>
</template>

<script>
  import {
    Disclosure,
    DisclosureButton,
    DisclosurePanel,
  } from '@headlessui/vue'
  import MyLink from './MyLink'

  export default {
    components: { Disclosure, DisclosureButton, DisclosurePanel, MyLink },
  }
</script>

The same thing works with Popover components, too:

import { Popover } from '@headlessui/react'
import MyLink from './MyLink'

function MyPopover() {
  return (
    <Popover>
      <Popover.Button>Solutions</Popover.Button>
      <Popover.Panel>
        <Popover.Button as={MyLink} href="/insights">
          Insights
        </Popover.Button>
        {/* ... */}
      </Popover.Panel>
    </Popover>
  )
}
<template>
  <Popover>
    <PopoverButton>Solutions</PopoverButton>

    <PopoverPanel>
      <PopoverButton :as="MyLink" href="/insights">Insights</PopoverButton>
      <!-- ... -->
    </PopoverPanel>
  </Popover>
</template>

<script>
  import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
  import MyLink from './MyLink'

  export default {
    components: { Popover, PopoverButton, PopoverPanel, MyLink },
  }
</script>

If you need finer control, we also pass a close function via the render prop/scoped slot, so you can imperatively close the panel when you need to:

import { Popover } from '@headlessui/react'

function MyPopover() {
  return (
    <Popover>
      <Popover.Button>Terms</Popover.Button>
      <Popover.Panel>
        {({ close }) => (
          <button
            onClick={async () => {
              await fetch('/accept-terms', { method: 'POST' })
              close()
            }}
          >
            Read and accept
          </button>
        )}
      </Popover.Panel>
    </Popover>
  )
}
<template>
  <Popover>
    <PopoverButton>Solutions</PopoverButton>

    <PopoverPanel v-slot="{ close }">
      <button @click="accept(close)">Read and accept</button>
    </PopoverPanel>
  </Popover>
</template>

<script>
  import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'

  export default {
    components: { Popover, PopoverButton, PopoverPanel },
    setup() {
      return {
        accept: async (close) => {
          await fetch('/accept-terms', { method: 'POST' })
          close()
        },
      }
    },
  }
</script>

For more details, check out the updated Popover and Disclosure documentation.

Try it out

Headless UI v1.4 is a minor update so there are no breaking changes. To upgrade, just install the latest version via npm:

# For React
npm install @headlessui/react

# For Vue
npm install @headlessui/vue

Check out the official website for the latest documentation, and check out Tailwind UI if you want to play tons of styled examples.

Ready to try it out? Visit the Headless UI website →