Agents (llms.txt)
Octocat

Disclosure

A mostly unstyled vertical set of interactive elements that each reveal a section of content when clicked

Two hearts pump blood to the gills, while the third one circulates it to the rest of the body. When they swim, their third heart actually stops beating - which is why they tend to crawl more than swim!
The Turritopsis dohrnii jellyfish can technically live forever! When stressed, it can transform back into a juvenile form by turning its existing cells into different cell types. It's like having a reset button for aging!
import { Disclosure } from "@/components/disclosure";

export default function DisclosurePreview() {
  return (
    <div className="w-90 text-sm">
      <Disclosure>
        <Disclosure.Trigger>Did you know that octopuses have three hearts?</Disclosure.Trigger>
        <Disclosure.Content className="text-foreground-secondary">
          Two hearts pump blood to the gills, while the third one circulates it to the rest of the
          body. When they swim, their third heart actually stops beating - which is why they tend to
          crawl more than swim!
        </Disclosure.Content>
      </Disclosure>
      <Disclosure>
        <Disclosure.Trigger>Want to hear about immortal jellyfish?</Disclosure.Trigger>
        <Disclosure.Content className="text-foreground-secondary">
          The Turritopsis dohrnii jellyfish can technically live forever! When stressed, it can
          transform back into a juvenile form by turning its existing cells into different cell
          types. It&apos;s like having a reset button for aging!
        </Disclosure.Content>
      </Disclosure>
    </div>
  );
}

Dependencies

Source Code

import { CaretDownIcon } from "@phosphor-icons/react/dist/ssr";
import { createContext, use, useId, useState } from "react";

import { Slot } from "@/components/slot";
import { cn } from "@/lib/utils/classnames";

interface DisclosureGroupContext {
  open: string | null;
  setOpen: (id: string | null) => void;
}

const DisclosureGroupContext = createContext<DisclosureGroupContext | null>(null);

const useDisclosureGroupContext = () => use(DisclosureGroupContext);

const DisclosureGroup = ({ children }: { children: React.ReactNode }) => {
  const [open, setOpen] = useState<string | null>(null);

  return <DisclosureGroupContext value={{ open, setOpen }}>{children}</DisclosureGroupContext>;
};

interface DisclosureContext {
  id: string;
  open: boolean;
  setOpen: (open: boolean) => void;
}

const DisclosureContext = createContext<DisclosureContext | null>(null);

const useDisclosureContext = () => {
  const context = use(DisclosureContext);

  if (!context) throw new Error("Disclosure components must be used within an Disclosure");

  return context;
};

interface DisclosureProps extends React.ComponentPropsWithRef<"div"> {
  defaultOpen?: boolean;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  children: React.ReactNode;
}

const Disclosure = ({
  defaultOpen,
  open: propsOpen,
  onOpenChange,
  children,
  ...props
}: DisclosureProps) => {
  const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);

  const generatedId = useId();
  const id = props.id ?? generatedId;

  const group = useDisclosureGroupContext();

  let open = propsOpen ?? internalOpen;

  let setOpen = (open: boolean) => {
    setInternalOpen(open);
    onOpenChange?.(open);
  };

  if (group) {
    open = group.open === id;

    setOpen = (open: boolean) => {
      group.setOpen(open ? id : null);
    };
  }

  return (
    <DisclosureContext value={{ open, setOpen, id }}>
      <DisclosureGroupContext value={null}>
        <div {...props}>{children}</div>
      </DisclosureGroupContext>
    </DisclosureContext>
  );
};

interface DisclosureTriggerProps extends React.ComponentPropsWithRef<"button"> {
  asChild?: boolean;
}

const DisclosureTrigger = ({
  children,
  onClick,
  asChild,
  className,
  ...props
}: DisclosureTriggerProps) => {
  const { open, setOpen, id } = useDisclosureContext();

  const Comp = asChild ? Slot : "button";

  return (
    <Comp
      type={asChild ? undefined : "button"}
      onClick={(e) => {
        onClick?.(e);

        if (!e.defaultPrevented) setOpen(!open);
      }}
      aria-expanded={open}
      aria-controls={getContentId(id)}
      data-state={open ? "open" : "closed"}
      className={cn(
        "focus-visible:ring-(length:--ring-width) flex w-full items-center justify-between text-left outline-none ring-ring",
        className,
      )}
      {...props}
    >
      {children}
    </Comp>
  );
};

const getContentId = (id: string) => `${id}-Disclosure-content`;

// When interpolate-size / calc-size are Baseline, we can simply animate height: 0 to auto
const DisclosureContent = ({
  children,
  className,
  ...props
}: Omit<React.ComponentPropsWithRef<"div">, "id">) => {
  const { open, id } = useDisclosureContext();

  return (
    <div
      id={getContentId(id)}
      data-state={open ? "open" : "closed"}
      className={cn(
        "grid grid-rows-[0fr] transition-[grid-template-rows,visibility] duration-200 ease-out motion-reduce:transition-none",
        "data-[state=closed]:invisible data-[state=open]:grid-rows-[1fr]",
      )}
    >
      <div
        data-state={open ? "open" : "closed"}
        className={cn("min-h-0 overflow-hidden", className)}
        {...props}
      >
        {children}
      </div>
    </div>
  );
};

const DisclosureChevron = ({ className, ...props }: React.ComponentPropsWithRef<"span">) => {
  const { open } = useDisclosureContext();

  return (
    <span
      aria-hidden="true"
      className={cn(
        "p-1 transition-transform duration-200 ease-out motion-reduce:transition-none",
        open && "rotate-180",
        className,
      )}
      {...props}
    >
      <CaretDownIcon />
    </span>
  );
};

const CompoundDisclosure = Object.assign(Disclosure, {
  Group: DisclosureGroup,
  Trigger: DisclosureTrigger,
  Content: DisclosureContent,
  Chevron: DisclosureChevron,
});

export { CompoundDisclosure as Disclosure };

Features

  • Unstyled Core: Minimal styling for maximum flexibility
  • Smooth Animations: CSS height transitions
  • Group Support: Optional grouping to create exclusive sections
  • Controlled & Uncontrolled: Supports both modes of operation
  • Customizable Triggers: Flexible trigger elements with optional chevron

Anatomy

          <Disclosure.Group>
  <Disclosure>
    <Disclosure.Trigger>
      <Disclosure.Chevron />
    </Disclosure.Trigger>
    <Disclosure.Content />
  </Disclosure>
</Disclosure.Group>
        

API Reference

Disclosure

Extends the div element.

PropDefaultTypeDescription
defaultOpen-booleanWhether the element is open by default. Useful when used as an uncontrolled component.
open-booleanControls the open state directly in controlled mode.
onOpenChange-(open: boolean) => voidCallback fired when the open state changes.

Disclosure.Trigger

Extends the button element.

PropDefaultTypeDescription
asChild-booleanWhether to merge props onto the child element.

Disclosure.Content

Extends the div element.

The content that will be shown or hidden. Stays mounted while closed.

Disclosure.Chevron

Extends the span element.

Will render a caret that rotates when the disclosure is open.

Disclosure.Group

If you want to only allow one disclosure to be open at a time, you can wrap your disclosures in a Disclosure.Group.

Examples

Simple

Basic usage with default styling.

Two hearts pump blood to the gills, while the third one circulates it to the rest of the body. When they swim, their third heart actually stops beating - which is why they tend to crawl more than swim!
The Turritopsis dohrnii jellyfish can technically live forever! When stressed, it can transform back into a juvenile form by turning its existing cells into different cell types. It's like having a reset button for aging!
import { Disclosure } from "@/components/disclosure";

export default function DisclosurePreview() {
  return (
    <div className="w-90 text-sm">
      <Disclosure>
        <Disclosure.Trigger>Did you know that octopuses have three hearts?</Disclosure.Trigger>
        <Disclosure.Content className="text-foreground-secondary">
          Two hearts pump blood to the gills, while the third one circulates it to the rest of the
          body. When they swim, their third heart actually stops beating - which is why they tend to
          crawl more than swim!
        </Disclosure.Content>
      </Disclosure>
      <Disclosure>
        <Disclosure.Trigger>Want to hear about immortal jellyfish?</Disclosure.Trigger>
        <Disclosure.Content className="text-foreground-secondary">
          The Turritopsis dohrnii jellyfish can technically live forever! When stressed, it can
          transform back into a juvenile form by turning its existing cells into different cell
          types. It&apos;s like having a reset button for aging!
        </Disclosure.Content>
      </Disclosure>
    </div>
  );
}

Exclusive

Wraps your disclosures in a Disclosure.Group to ensure only one disclosure is open at a time.

Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000 years old and still perfectly edible! The unique chemical composition and low moisture content make it impossible for bacteria to grow in honey.
A hummingbird's heart beats up to 1,260 times per minute during flight! They have such a fast metabolism that they need to eat every 10-15 minutes and visit up to 2,000 flowers per day. At night, they enter a state called torpor where their metabolism slows down by 95% to survive.
import { Disclosure } from "@/components/disclosure";

export default function DisclosurePreview() {
  return (
    <div className="w-90 text-sm">
      <Disclosure.Group>
        <Disclosure>
          <Disclosure.Trigger>Did you know that honey never spoils?</Disclosure.Trigger>
          <Disclosure.Content className="text-foreground-secondary">
            Archaeologists have found pots of honey in ancient Egyptian tombs that are over 3,000
            years old and still perfectly edible! The unique chemical composition and low moisture
            content make it impossible for bacteria to grow in honey.
          </Disclosure.Content>
        </Disclosure>
        <Disclosure>
          <Disclosure.Trigger>Want to learn about hummingbird metabolism?</Disclosure.Trigger>
          <Disclosure.Content className="text-foreground-secondary">
            A hummingbird&apos;s heart beats up to 1,260 times per minute during flight! They have
            such a fast metabolism that they need to eat every 10-15 minutes and visit up to 2,000
            flowers per day. At night, they enter a state called torpor where their metabolism slows
            down by 95% to survive.
          </Disclosure.Content>
        </Disclosure>
      </Disclosure.Group>
    </div>
  );
}

With chevron

Using the built-in chevron component.

While most mammals can only hold their breath for a few minutes, sloths can slow their heart rates to one-third of its normal rate, allowing them to stay underwater for up to 40 minutes! This helps them escape predators and swim between islands.
Platypuses have electroreceptors in their bills that detect electrical signals from prey! They can sense the electrical fields produced by the muscular contractions of small aquatic animals. They're also one of the few mammals that produce venom.
import { Disclosure } from "@/components/disclosure";

export default function DisclosureChevronPreview() {
  return (
    <div className="w-90 text-sm">
      <Disclosure>
        <Disclosure.Trigger>
          Sloths can hold their breath for 40 minutes?
          <Disclosure.Chevron />
        </Disclosure.Trigger>
        <Disclosure.Content className="text-foreground-secondary">
          While most mammals can only hold their breath for a few minutes, sloths can slow their
          heart rates to one-third of its normal rate, allowing them to stay underwater for up to 40
          minutes! This helps them escape predators and swim between islands.
        </Disclosure.Content>
      </Disclosure>
      <Disclosure>
        <Disclosure.Trigger>
          Want to learn about platypus superpowers?
          <Disclosure.Chevron />
        </Disclosure.Trigger>
        <Disclosure.Content className="text-foreground-secondary">
          Platypuses have electroreceptors in their bills that detect electrical signals from prey!
          They can sense the electrical fields produced by the muscular contractions of small
          aquatic animals. They&apos;re also one of the few mammals that produce venom.
        </Disclosure.Content>
      </Disclosure>
    </div>
  );
}

Nested

Nesting a Disclosure.Group inside Disclosure.Content creates independent exclusive groups at each level.

Mercury, Venus, Earth, and Mars — rocky worlds close to the Sun, shaped by heat and geological activity.
Jupiter, Saturn, Uranus, and Neptune — gas and ice giants with rings, storms, and dozens of moons each.
The top 200 meters where light penetrates, supporting coral reefs, fish, and most visible marine life.
Below 1,000 meters, no sunlight reaches here. Creatures survive through bioluminescence and slow metabolisms.
import { Disclosure } from "@/components/disclosure";

export default function DisclosureNestedPreview() {
  return (
    <div className="w-72 text-sm">
      <Disclosure.Group>
        <Disclosure>
          <Disclosure.Trigger>
            Solar System
            <Disclosure.Chevron />
          </Disclosure.Trigger>
          <Disclosure.Content>
            <Disclosure>
              <Disclosure.Trigger className="pl-4 text-foreground-secondary">
                Inner Planets
                <Disclosure.Chevron />
              </Disclosure.Trigger>
              <Disclosure.Content className="pl-8 text-foreground-secondary">
                Mercury, Venus, Earth, and Mars — rocky worlds close to the Sun, shaped by heat and
                geological activity.
              </Disclosure.Content>
            </Disclosure>
            <Disclosure>
              <Disclosure.Trigger className="pl-4 text-foreground-secondary">
                Outer Planets
                <Disclosure.Chevron />
              </Disclosure.Trigger>
              <Disclosure.Content className="pl-8 text-foreground-secondary">
                Jupiter, Saturn, Uranus, and Neptune — gas and ice giants with rings, storms, and
                dozens of moons each.
              </Disclosure.Content>
            </Disclosure>
          </Disclosure.Content>
        </Disclosure>
        <Disclosure>
          <Disclosure.Trigger>
            Deep Ocean
            <Disclosure.Chevron />
          </Disclosure.Trigger>
          <Disclosure.Content>
            <Disclosure>
              <Disclosure.Trigger className="pl-4 text-foreground-secondary">
                Sunlight Zone
                <Disclosure.Chevron />
              </Disclosure.Trigger>
              <Disclosure.Content className="pl-8 text-foreground-secondary">
                The top 200 meters where light penetrates, supporting coral reefs, fish, and most
                visible marine life.
              </Disclosure.Content>
            </Disclosure>
            <Disclosure>
              <Disclosure.Trigger className="pl-4 text-foreground-secondary">
                Midnight Zone
                <Disclosure.Chevron />
              </Disclosure.Trigger>
              <Disclosure.Content className="pl-8 text-foreground-secondary">
                Below 1,000 meters, no sunlight reaches here. Creatures survive through
                bioluminescence and slow metabolisms.
              </Disclosure.Content>
            </Disclosure>
          </Disclosure.Content>
        </Disclosure>
      </Disclosure.Group>
    </div>
  );
}

Styled

A more styled example showing how to build an Accordion.

Butterflies have taste receptors on their feet that help them identify which plants to lay their eggs on. When they land on a plant, they can taste it to determine if it's suitable food for their caterpillars.
Tardigrades, also known as water bears, can survive in space! They can withstand extreme temperatures, pressure, radiation, and can even survive being completely dehydrated for years by entering a state of cryptobiosis.
The Turritopsis dohrnii jellyfish can technically live forever! When stressed or injured, it can transform back into its juvenile stage instead of dying, making it the only known animal capable of biological immortality.
import { Disclosure } from "@/components/disclosure";
import { cn } from "@/lib/utils/classnames";

const AccordionGroup = ({ children, className }: React.ComponentProps<"div">) => {
  return (
    <Disclosure.Group>
      <div className={cn("rounded-lg border", className)}>{children}</div>
    </Disclosure.Group>
  );
};

const Accordion = ({ children, className }: React.ComponentProps<typeof Disclosure>) => {
  return <Disclosure className={cn("border-b last:border-b-0", className)}>{children}</Disclosure>;
};

const AccordionTrigger = ({
  children,
  className,
}: React.ComponentProps<typeof Disclosure.Trigger>) => {
  return (
    <Disclosure.Trigger
      className={cn(
        "flex cursor-pointer items-center justify-between gap-4 px-3 py-2 transition-colors hover:bg-foreground/5",
        className,
      )}
    >
      {children}
      <Disclosure.Chevron />
    </Disclosure.Trigger>
  );
};

const AccordionContent = ({
  children,
  className,
}: React.ComponentProps<typeof Disclosure.Content>) => {
  return (
    <Disclosure.Content>
      <div className={cn("border-t px-3 py-2", className)}>{children}</div>
    </Disclosure.Content>
  );
};

export default function DisclosureStyledPreview() {
  return (
    <AccordionGroup>
      <Accordion>
        <AccordionTrigger>Did you know that butterflies taste with their feet?</AccordionTrigger>
        <AccordionContent>
          Butterflies have taste receptors on their feet that help them identify which plants to lay
          their eggs on. When they land on a plant, they can taste it to determine if it&apos;s
          suitable food for their caterpillars.
        </AccordionContent>
      </Accordion>
      <Accordion>
        <AccordionTrigger>Want to learn about tardigrade superpowers?</AccordionTrigger>
        <AccordionContent>
          Tardigrades, also known as water bears, can survive in space! They can withstand extreme
          temperatures, pressure, radiation, and can even survive being completely dehydrated for
          years by entering a state of cryptobiosis.
        </AccordionContent>
      </Accordion>
      <Accordion>
        <AccordionTrigger>Have you heard about the immortal jellyfish?</AccordionTrigger>
        <AccordionContent>
          The Turritopsis dohrnii jellyfish can technically live forever! When stressed or injured,
          it can transform back into its juvenile stage instead of dying, making it the only known
          animal capable of biological immortality.
        </AccordionContent>
      </Accordion>
    </AccordionGroup>
  );
}

Building an Accordion component

The Disclosure component serves as a low-level building block that enables progressive disclosure functionality in any UI element that requires it.

You can easily use it to build a more opinionated Accordion component, like the example above.

          import { Disclosure } from "../disclosure";

const AccordionGroup = ({ children, className }: React.ComponentProps<"div">) => {
  return (
    <Disclosure.Group>
      <div className={cn("rounded-lg border", className)}>{children}</div>
    </Disclosure.Group>
  );
};

const Accordion = ({ children, className }: React.ComponentProps<typeof Disclosure>) => {
  return <Disclosure className={cn("border-b last:border-b-0", className)}>{children}</Disclosure>;
};

const AccordionTrigger = ({ children, className }: React.ComponentProps<typeof Disclosure.Trigger>) => {
  return (
    <Disclosure.Trigger
      className={cn(
        "hover:bg-foreground/5 flex cursor-pointer items-center justify-between gap-4 px-3 py-2 transition-colors",
        className
      )}
    >
      {children}
      <Disclosure.Chevron />
    </Disclosure.Trigger>
  );
};

const AccordionContent = ({ children, className }: React.ComponentProps<typeof Disclosure.Content>) => {
  return (
    <Disclosure.Content>
      <div className={cn("border-t px-3 py-2", className)}>{children}</div>
    </Disclosure.Content>
  );
};
        

Best Practices

  1. Content Organization:

    • Use clear, descriptive trigger labels
    • Keep content concise and focused
    • Consider the order of disclosures
  2. Mobile Considerations:

    • Ensure touch targets are large enough
    • Test animations on lower-end devices
    • Consider using Disclosure.Group on mobile
  3. Performance:

    • Content stays mounted while closed; lazy load heavy content yourself
    • Consider disabling animations on slower devices
    • Clean up resources when unmounting

Previous

Dialog

Next

Divider