Getting Started
App Providerglobal.d.tsThemesTypes
cn(...args)Text ParserUnits Converters
AnchorAvatarBreadcrumbBurgerButtonCardCarouselCheckerCodeColor PickerCommandConfettiCopyButtonDouble Helix WordsFloating IndicatorGroupHighlight TextIndicatorInputKbdLabelLoaderPaginationPassword RequirementPolymorphic SlotProgressProseRatingRunning AreaScroll AreaSheetsSkeletonSliderStackSvgTableTabsTextareaTimelineTimesToasterTooltipTyping WordsTypography
useClickOutsideuseClipboarduseDeviceInfouseDialoguseDidUpdateuseDirectionuseDisclosureuseDocumentTitleuseDocumentVisibilityuseElementInfouseEyeDropperuseFetchuseFullscreenuseGeoLocationuseHotkeysuseHoveruseIduseImagePopupuseInputStateuseIntersectionuseIntervaluseIsomorphicEffectuseListStateuseLocalStorageuseMeasureScrollbaruseMediaQueryuseMergedRefuseMouseuseMoveuseMutationObserveruseNetworkuseOpenStateuseOrientationuseOSusePaginationusePWAInstalleruseRandomColorsuseReducedMotionuseReloaduseResizeObserveruseScrollIntoViewuseStateHistoryuseTimeoutuseTouchuseTriggeruseUncontrolleduseValidatedStateuseViewportSizeuseWindowEventuseWindowScroll
Docs
Web
Components
Sheets
Scroll Area

A customizable scrollable container for displaying overflow content elegantly.

Skeleton

A placeholder UI used to indicate loading states by imitating content structure.


Edit this page on GitHub
  • Started
  • Utilities
  • Configuration
  • Components
  • Hooks
  • Examples
  • Github
  • Contributing
⌘+J

© 2025 oeri rights MIT


Designed in Earth-616

Built by oeri

Sheets

A utility set for building modal dialogs, dropdowns, drawers, collapsibles, and accordions, with support for nested interactions.


Sheets is a flexible and composable UI abstraction that unifies multiple interaction patterns — including Accordion, Collapsible, Dialog, Drawer, and Dropdown — into a single, consistent system. Designed to simplify complex layouts and improve development efficiency, Sheets enables you to manage visibility, transitions, and nested structures seamlessly across different UI contexts.

It offers variants suited for various use cases, such as content organization (Accordion), simple toggling (Collapsible), modal workflows (Dialog), off-canvas navigation (Drawer), and compact selection lists (Dropdown). With Sheets, you can build modular, accessible, and highly customizable user experiences with minimal boilerplate.


Usages

Various examples of Sheets Implementation for various component patterns. Sheets offers a high moment to create interactive components that can be developed as needed.

Sheets Variant Accordion

Display collapsible content organized into accordion-style sections. This variant allows expanding one section at a time for a clean and structured user experience.

Accessibility items that can be changed.
You can find out more here
Change the theme, color, and font style according to your preferences
Find the latest news about us here.
// Independent Components
import { Sheets, SheetsItem, SheetsContent, SheetsTrigger } from "@/ui/sheets";
import { ChevronIcon } from "@/icons/*";
import { Typography } from "@/ui/typography";

const data = [
  { title: "Accessibility", description: "Accessibility items that can be changed." },
  { title: "Terms and Conditions", description: "You can find out more here" },
  { title: "Preferences", description: "Change the theme, color, and font style according to your preferences" },
  { title: "Updates", description: "Find the latest news about us here." }
];

export function SheetsAccordionDemo() {
  return (
    <Sheets variant="accordion" defaultOpen="accessibility" className="w-full max-w-96 rounded-2xl border px-6">
      {data.map((i, index) => (
        <SheetsItem
          key={index}
          value={String(i.title.replace(/\s/g, "-").toLowerCase())}
          className="group relative flex h-auto select-none flex-col border-b last-of-type:border-b-transparent"
        >
          <SheetsTrigger>
            {i.title}
            <ChevronIcon chevron="down" data-sheets="chevron" />
          </SheetsTrigger>

          <SheetsContent className="text-sm">
            <Typography prose="muted" className="pb-8">
              {i.description}
            </Typography>
          </SheetsContent>
        </SheetsItem>
      ))}
    </Sheets>
  );
}

Sheets Variant Collapsible

Simple collapsible component for toggling the visibility of content. Useful for hiding and revealing sections without restricting to only one open at a time.

@sheets/collapsible
@sheets/accordion@sheets/dialog@sheets/drawer@sheets/dropdown
// Compound Components
"use client";
import { Sheets } from "@/ui/sheets";
import { ChevronIcon } from "@/icons/*";
import { Typography } from "@/ui/typography";

export function SheetsCollapsibleDemo() {
  return (
    <Sheets variant="collapsible" className="m-auto w-full max-w-80 space-y-2">
      <Sheets.Trigger className="w-full justify-between bg-background font-mono text-sm text-muted-foreground data-[state=open]:text-constructive">
        Select your &lt;Sheets /&gt;
        <Typography className="rounded-md border p-1 transition-colors group-hover:bg-muted/90 group-data-[state=open]:border-constructive">
          <ChevronIcon chevron="up-down" />
        </Typography>
      </Sheets.Trigger>

      <Typography
        el="a"
        data-ignore-clickoutside
        href="#sheets-variant-collapsible"
        className="mt-4 w-full justify-start rounded-md border px-4 py-2 font-mono text-sm shadow-sm hover:bg-muted/60"
      >
        @sheets/collapsible
      </Typography>

      <Sheets.Content className="space-y-2">
        {["accordion", "dialog", "drawer", "dropdown"].map(i => (
          <Typography
            el="a"
            key={i}
            href={`#sheets-variant-${i}`}
            className="w-full justify-start rounded-md border px-4 py-2 font-mono text-sm shadow-sm hover:bg-muted/60"
          >
            @sheets/{i}
          </Typography>
        ))}
      </Sheets.Content>
    </Sheets>
  );
}
Long ago, during the era of Gol D. Roger, the Pirate King, the seas were filled with tales of adventure, danger, and mystery. His final words before execution ignited the "Great Pirate Era": "My treasure? It's yours if you want it. I left everything I gathered in one place." These words inspired countless pirates, including a young boy named Monkey D. Luffy, who dreamed of finding the legendary One Piece and becoming the next Pirate King. With a straw hat gifted by the infamous Shanks and the power of the Gomu Gomu no Mi, a mysterious Devil Fruit, Luffy set out on a journey that would bring together an unforgettable crew and face unimaginable challenges. From battling fierce Warlords to uncovering secrets of the Void Century, their adventure is a testament to loyalty, courage, and the unbreakable bonds of friendship.
// Only Root Component
"use client";
import { useState } from "react";
import { Sheets } from "@/ui/sheets";
import { Button } from "@/ui/button";
import { Stack } from "@/ui/stack";
import { Typography } from "@/ui/typography";

export function SheetsCollapsibleRootDemo() {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  return (
    <Stack gap={8}>
      <Button onClick={() => setIsOpen(o => !o)} className="w-max">{isOpen ? "Close" : "Read"}</Button>
      <Sheets variant="collapsible" open={isOpen} onOpenChange={setIsOpen} className="w-96 text-justify text-sm max-w-full">
        <Typography prose="span" className="max-w-full text-wrap">
          Long ago, during the era of Gol D. Roger, the Pirate King, the seas were filled with tales of adventure, danger, and mystery. His final words
          before execution ignited the "Great Pirate Era": "My treasure? It's yours if you want it. I left everything I gathered in one place." These words 
          inspired countless pirates, including a young boy named Monkey D. Luffy, who dreamed of finding the legendary <em>One Piece</em> and becoming the 
          next Pirate King. With a straw hat gifted by the infamous Shanks and the power of the Gomu Gomu no Mi, a mysterious Devil Fruit, Luffy set out on 
          a journey that would bring together an unforgettable crew and face unimaginable challenges. From battling fierce Warlords to uncovering secrets of 
          the Void Century, their adventure is a testament to loyalty, courage, and the unbreakable bonds of friendship.
        </Typography>
        <Button variant="link" onClick={() => setIsOpen(false)}>
          close
        </Button>
      </Sheets>
    </Stack>
  );
}

Sheets Variant Dialog

Modal dialog component for user interactions requiring focus. Dialogs are commonly used for confirmation prompts, forms, or important notices.

// Independent Components
import { Sheets, SheetsContent, SheetsTrigger, SheetsClose } from "@/ui/sheets";
import { Typography } from "@/ui/typography";
import { Stack } from "@/ui/stack";

export function SheetsDialogDemo() {
  return (
    <Sheets variant="dialog">
      <SheetsTrigger>
        <Typography
          el="span"
          data-labelopen="Open Dialog"
          data-labelclosed="Close Dialog"
          className="group-data-[state=closed]/st:before:content-[attr(data-labelopen)] group-data-[state=open]/st:before:content-[attr(data-labelclosed)]"
        />
      </SheetsTrigger>

      <SheetsContent className="flex flex-col gap-4 overflow-hidden md:w-[528px] md:h-[438px] max-md:max-h-96">
        <Stack>
          <Typography el="h2" prose="large">
            The Power of Dreams
          </Typography>
          <Typography prose="muted">
            Explore the strength of ambitions and the stories of those who dare to dream beyond the horizon.
          </Typography>
        </Stack>
        <Typography prose="p" className="flex size-full flex-col overflow-y-auto">
          Every great journey begins with a dream. It is the courage to pursue the unknown, the resilience to face challenges, and the belief in oneself 
          that turns visions into reality. Whether it’s the quest for knowledge, the pursuit of adventure, or the desire to leave a lasting legacy, dreams 
          shape the world we live in. From the innovators who redefined technology to explorers who mapped uncharted territories, their stories remind us 
          that nothing is impossible when driven by purpose. So, what’s your dream, and how far will you go to achieve it?
        </Typography>

        <SheetsClose />
      </SheetsContent>
    </Sheets>
  );
}

Dialog Nested

Example of nesting dialogs within dialogs. Useful when complex workflows require multiple layers of modal interactions.

"use client";
import { Sheets } from "@/ui/sheets";
import { Typography } from "@/ui/typography";
import { Button, buttonStyle } from "@/ui/button";
import { Stack } from "@/ui/stack";
import { Input } from "@/ui/input";
import { Group } from "@/ui/group";

export function SheetsDialogDemoNested() {
  return (
    <Sheets variant="dialog" multipleOpen>
      <Sheets.Trigger id="dialog-demo-1">Edit profile</Sheets.Trigger>

      <Sheets.Content value="dialog-demo-1" className="flex flex-col justify-between md:w-full max-w-md">
        <Typography el="p" prose="h4">
          Edit profile
        </Typography>
        <Typography prose="muted">Make changes to your profile here. Click continue to the next step.</Typography>

        <Stack>
          <Input.Wrapper label="Name">
            <Input placeholder="Edit your name" />
          </Input.Wrapper>
          <Input.Wrapper label="Username">
            <Input placeholder="Edit Username" />
          </Input.Wrapper>
        </Stack>

        <Group align="stretch" grow>
          <Sheets.Close unstyled className={buttonStyle({ variant: "destructive" })}>
            Cancel
          </Sheets.Close>
          <Sheets.Trigger id="dialog-demo-2" unstyled className={buttonStyle({ variant: "default" })}>
            Continue
          </Sheets.Trigger>
        </Group>

        <Sheets.Close />
      </Sheets.Content>

      <Sheets.Content value="dialog-demo-2" side="bottom" className="flex flex-col justify-between md:w-full max-w-md">
        <Typography el="p" prose="h1">
          Dialog 2
        </Typography>

        <Sheets.Trigger id="dialog-demo-3" className="absolute bottom-4">
          Open Dialog 3
        </Sheets.Trigger>

        <Sheets.Close />
      </Sheets.Content>

      <Sheets.Content value="dialog-demo-3" side="bottom" className="flex flex-col justify-between md:w-full max-w-md">
        <Typography el="p" prose="h1">
          Dialog 3
        </Typography>

        <Sheets.Trigger id="dialog-demo-4" className="absolute bottom-4">
          Open Dialog 4
        </Sheets.Trigger>

        <Sheets.Close />
      </Sheets.Content>

      <Sheets.Content value="dialog-demo-4" className="flex flex-col justify-between md:w-full max-w-md">
        <Typography el="p" prose="h1">
          Dialog 4
        </Typography>

        <Sheets.Close />
      </Sheets.Content>
    </Sheets>
  );
}

Sheets Variant Drawer

Slide-in panel component typically used for sidebars, menus, or additional contextual content. Drawers provide an off-canvas UI that is accessible and intuitive.

// Independent Components
import { Sheets, SheetsContent, SheetsTrigger, SheetsClose } from "@/ui/sheets";
import { Typography } from "@/ui/typography";
import { buttonVariants } from "@/ui/button";
import { Stack } from "@/ui/stack";

export function SheetsDrawerDemo() {
  return (
    <Sheets variant="drawer">
      <SheetsTrigger id="drawer" className={buttonVariants({ variant: "outline" })}>
        <Typography
          el="span"
          data-labelopen="Open Drawer"
          data-labelclosed="Close Drawer"
          className="group-data-[state=closed]/st:before:content-[attr(data-labelopen)] group-data-[state=open]/st:before:content-[attr(data-labelclosed)]"
        />
      </SheetsTrigger>

      <SheetsContent className="flex flex-col gap-4">
        <Stack>
          <Typography el="h2" prose="large">
            Lorem ipsum
          </Typography>
          <Typography prose="muted">Tenetur fugiat aspernatur aut quas ex praesentium molestias officiis. repudiandae.</Typography>
        </Stack>
        <Typography prose="p" className="flex size-full flex-col overflow-y-auto">
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tenetur fugiat aspernatur aut quas ex praesentium molestias officiis fugit accusamus
          expedita alias repudiandae, exercitationem maiores velit quos reiciendis recusandae, quod iusto earum? Fugiat fuga dolor atque nobis esse dignissimos
          temporibus vel incidunt maxime provident ut dolorem hic explicabo corrupti, praesentium.
        </Typography>

        <SheetsClose />
      </SheetsContent>
    </Sheets>
  );
}

Drawer Nested

Example of nesting drawers inside drawers. Helpful for creating multi-level navigation structures or deep contextual workflows.

"use client";
import { Sheets } from "@/ui/sheets";
import { Typography } from "@/ui/typography";
import { buttonVariants } from "@/ui/button";

export function SheetsDrawerDemoNested() {
  return (
    <Sheets variant="drawer" multipleOpen>
      <Sheets.Trigger id="drawer-demo-1" className={buttonVariants({ variant: "outline" })}>Open Drawer 1</Sheets.Trigger>

      <Sheets.Content value="drawer-demo-1" side="right">
        <Typography el="p" prose="h1">
          Drawer 1
        </Typography>
        <Sheets.Trigger id="drawer-demo-2" className="absolute bottom-8">
          Open Drawer 2
        </Sheets.Trigger>
        <Sheets.Close />
      </Sheets.Content>

      <Sheets.Content value="drawer-demo-2" side="right">
        <Typography el="p" prose="h1">
          Drawer 2
        </Typography>
        <Sheets.Trigger id="drawer-demo-3" className="absolute bottom-8">
          Open Drawer 3
        </Sheets.Trigger>
        <Sheets.Close />
      </Sheets.Content>

      <Sheets.Content value="drawer-demo-3" side="left">
        <Typography el="p" prose="h1">
          Drawer 3
        </Typography>
        <Sheets.Trigger id="drawer-demo-4" className="absolute bottom-8">
          Open Drawer 4
        </Sheets.Trigger>
        <Sheets.Close />
      </Sheets.Content>

      <Sheets.Content value="drawer-demo-4" side="top">
        <Typography el="p" prose="h2">
          Drawer 4
        </Typography>
        <Sheets.Close />
      </Sheets.Content>
    </Sheets>
  );
}

Sheets Variant Dropdown

Dropdown menu component for showing a list of actions or links. Dropdowns are compact, context-sensitive components for user choices.

Side offset
Align offset
"use client";
import { Sheets } from "@/ui/sheets";
import { ScrollArea } from "@/ui/scroll-area";

const TAGS = Array.from({ length: 58 }).map((_, i, a) => `v1.2.0-beta.${a.length - i}`);

export function SheetsDropdownDemo() {
  return (
    <Sheets align="center" sideOffset={4} alignOffset={0} variant="dropdown">
      <Sheets.Trigger id="dropdown" className="m-auto">
        <span
          data-labelopen="Dropdown"
          data-labelclosed="Dropdown"
          className="group-data-[state=closed]/st:before:content-[attr(data-labelopen)] group-data-[state=open]/st:before:content-[attr(data-labelclosed)]"
        />
      </Sheets.Trigger>

      <Sheets.Content className="h-[178px] w-44">
        <ScrollArea className="h-full p-4">
          {TAGS.map(tag => (
            <p key={tag} className="flex min-w-max items-center gap-3 border-b border-muted py-1 text-sm text-muted-foreground">
              <FileIcon /> {tag}
            </p>
          ))}
        </ScrollArea>
      </Sheets.Content>
    </Sheets>
  );
}

Dropdown Nested

Example of nesting dropdowns within other dropdowns. Ideal for building multi-level menu structures or advanced option trees.

"use client";
import { Sheets } from "@/ui/sheets";
import { ScrollArea } from "@/ui/scroll-area";
import { Button, buttonStyle } from "@/ui/button";
import { Stack } from "@/ui/stack";

export function SheetsDropdownDemoNested() {
  return (
    <Sheets variant="dropdown" sideOffset={2}>
      <Sheets.Trigger id="dropdown-nested-root" unstyled className={buttonStyle({ variant: "outline", size: "icon" })}>
        <DotsIcon />
        <span className="sr-only">Options</span>
      </Sheets.Trigger>

      <Sheets.Content value="dropdown-nested-root" className="rounded-2xl p-2 [&_button]:justify-start">
        <ScrollArea>
          <Stack gap={4}>
            <Button variant="outline">Menu 1</Button>
            <DrawerNested />
            <Button variant="outline">Menu 4</Button>

            <DropdownMoreOptions />
          </Stack>
        </ScrollArea>
      </Sheets.Content>
    </Sheets>
  );
}

function DropdownMoreOptions() {
  return (
    <Sheets variant="dropdown" side="left" align="start" sideOffset={10} clickOutsideToClose>
      <Sheets.Trigger id="dropdown-nested-child-1" unstyled className={buttonStyle({ variant: "outline" })}>
        More...
      </Sheets.Trigger>

      <Sheets.Content className="rounded-2xl p-2 [&_button]:justify-start">
        <ScrollArea>
          <Stack gap={4}>
            <Button variant="outline">Sub Menu 1</Button>
            <Button variant="outline">Sub Menu 3</Button>
            <Button variant="outline">Sub Menu 4</Button>
            <Button variant="outline">Sub Menu 5</Button>
          </Stack>
        </ScrollArea>
      </Sheets.Content>
    </Sheets>
  );
}

Depend on

  • useOpenState

Animation

The accordion and collapsible variant components use keyframe animation which by default is set using the tailwindcss class.

Add the following animation to your tailwind.config.js file:

import { PluginAPI, type Config } from "tailwindcss/types/config";
import plugin from "tailwindcss/plugin";
 
export default {
  theme: {
    extend: {
      animation: {
        "collapse-open": "collapse-open 0.2s linear forwards",
        "collapse-closed": "collapse-closed 0.2s linear forwards"
      },
      keyframes: {
        "collapse-open": {
          from: { height: "0", opacity: "0" },
          to: { height: "var(--measure-available-h)" }
        },
        "collapse-closed": {
          from: { height: "var(--measure-available-h)" },
          "85%": { opacity: "0" },
          to: { height: "0", visibility: "hidden" }
        }
      }
    }
  },
  plugins: [
    require("tailwindcss-animate"),
    plugin(({ addBase, addUtilities }: PluginAPI) => {
      addBase({});
      addUtilities({});
    })
  ]
} satisfies Config;

API References

Props API

Props APITypeDefaultAnnotation
variant?SheetsVariant"accordion""dialog" | "accordion" | "collapsible" | "dropdown" | "drawer"
align?"center" | "start" | "end""center"only available on "dropdown" variant
side?"top" | "right" | "bottom" | "left""bottom"only available on ["dropdown", "drawer"] variant
open?booleanfalsechange the original open and onOpenChange state with own approach
onOpenChange?(value: boolean) => voidopenStatechange the original open and onOpenChange state with own approach
sideOffset?numberstrokedistance between trigger and content, only available on "dropdown" variant
aligneOffset?numberstrokedistance between trigger and content, only available on "dropdown" variant
clickOutsideToClose?booleanfalseopen=false when click outside content
defaultOpen?boolean | string | nullfalse | nullchange initial open=true, in case "accordion" variant is string | null
openId?string | nullundefinedused when you want to modify the open state based on key | id
onOpenChangeId?(value: string | null) => voidundefinedused when you want to modify the open state based on key | id
delay?open?: number; closed?: number0on development
multipleOpen?booleanfalseonly available on "accordion" variant
hotKeys?(string & {})""only available on ["dialog", "drawer"] variant
modal?booleantrue on ["dialog", "drawer"] varianthide scrollbar body when open=true
popstate?booleanfalseadded history.pushState() when open=true and history.back() when open=false

Source Codes

Full working code example, including necessary markup and styles. You can copy and paste this code directly to start using the component immediately.

sheets.tsx
globals.css
/* sheets */ @layer base { [data-sheets="trigger"] [data-sheets="chevron"] { position: relative; margin-left: var(--ml-icon, auto); color: hsl(var(--color) / 0.7); width: var(--sz-icon, 1.0625rem); min-width: var(--sz-icon, 1.0625rem); max-width: var(--sz-icon, 1.0625rem); height: var(--sz-icon, 1.0625rem); min-height: var(--sz-icon, 1.0625rem); max-height: var(--sz-icon, 1.0625rem); transform: rotate(var(--tw-rotate)); transition: transform 180ms cubic-bezier(0.4, 0, 0.2, 1); } [data-sheets="trigger"][data-state*="open"] [data-sheets="chevron"], [data-sheets="trigger"][data-side="top"] [data-sheets="chevron"], [data-sheets="trigger"][data-side="bottom"][data-state*="open"] [data-sheets="chevron"] { --tw-rotate: -180deg; } [data-sheets="trigger"][data-side="bottom"] [data-sheets*="chevron"], [data-sheets="trigger"][data-side="top"][data-state*="open"] [data-sheets="chevron"] { --tw-rotate: 0deg; } [data-sheets="trigger"][data-side="right"] [data-sheets="chevron"], [data-sheets="trigger"][data-side="left"][data-state*="open"] [data-sheets="chevron"] { --tw-rotate: 90deg; } [data-sheets="trigger"][data-state*="closed"] [data-sheets="chevron"] [data-sheets="trigger"][data-side="left"] [data-sheets="chevron"], [data-sheets="trigger"][data-side="right"][data-state*="open"] [data-sheets="chevron"] { --tw-rotate: -90deg; } }