Components

Getting started

Components154

More

"Show 4 more" that opens in place: the hidden part eases open through a blur while the button's words morph to "Show less".

Used forLong listsDescriptionsAdvanced settingsChangelogs

Install with the shadcn CLI

$ npx shadcn@latest add https://hairlineui.com/r/collapsible.json

Templates

Need the whole page?

43 Next.js landing pages, each designed from scratch. Try any of them live; one is $39, all of them $99, and 5 are free.

All 43 templates

Where it’s used

  • Long lists

    The first five, then "Show 12 more".

  • Descriptions

    A clamp that opens to the full text.

  • Advanced settings

    Rarely used fields folded away.

  • Changelogs

    Older entries under one button.

What it handles for you

The button's words should change with the state; most collapsibles leave "Show more" on screen after it has shown more.

Instead of wiring upshow more buttondisclosure
  1. Morphing label

    TextMorph between your closed and open words.

  2. Same fold as the accordion

    Height by grid rows, content through a blur.

  3. Either side

    placement="top" or "bottom".

  4. Accessible

    aria-expanded, aria-controls, folded content inert.

Overview

"Show more" buttons usually snap the rest of a list in and keep the same words. Here the hidden part eases open and its content rises out of a blur, while the button's label morphs ("Show 4 more" → "Show less") and its chevron turns. Put the button above for details, below for lists.

States

open
Height eases (420ms); the label morphs; the chevron turns.
close
The reverse.

Usage

usage.tsx
import { Collapsible } from "@/components/ui/collapsible";<Collapsible labels={{ closed: "Show 4 more", open: "Show less" }}>  {rest.map((row) => <Row key={row.id} {...row} />)}</Collapsible>

Works with

Props

PropTypeDescription
childrenReactNodeThe hidden part.
labels{ closed, open }default { "Show more", "Show less" }Button words.
open / defaultOpen / onOpenChangeboolean / (open) => voidState.
placement"top" | "bottom"default "bottom"Button above or below.

Accessibility and motion

  • Installs accordion for the shared fold and chevron.
  • Reduced motion: opens in place.

Requirements

React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

The CLI also installs Accordion, Text morph, ui-interaction-motion.

Source

components/collapsible.tsx
"use client";import * as React from "react";import { Chevron, Fold } from "./accordion";import { TextMorph } from "./text-morph";import { INTERACTION_FOCUS, INTERACTION_PRESS, useInteractionReducedMotion } from "./ui-interaction-motion";/* ───────────────────────────────────────────────────────── * COLLAPSIBLE: "Show 4 more" that opens in place * *   open     the hidden part's height eases open while its content *            rises out of a blur; the button's words morph *            ("Show 4 more" → "Show less") and its chevron turns *   close    the reverse; the button stays where your finger is *            when it sits above the content (`placement="top"`) * * One disclosure: aria-expanded on the button, the content a * region it controls, folded content inert. * ───────────────────────────────────────────────────────── */export interface CollapsibleProps {  children: React.ReactNode;  /** Words on the button when closed and open. */  labels?: { closed: string; open: string };  open?: boolean;  defaultOpen?: boolean;  onOpenChange?: (open: boolean) => void;  /** Button above the hidden part (details) or below it ("show more" lists). */  placement?: "top" | "bottom";  className?: string;}export function Collapsible({ children, labels = { closed: "Show more", open: "Show less" }, open: controlled, defaultOpen = false, onOpenChange, placement = "bottom", className = "" }: CollapsibleProps) {  const reduced = useInteractionReducedMotion();  const id = React.useId();  const [own, setOwn] = React.useState(defaultOpen);  const open = controlled ?? own;  const button = (    <button      type="button"      aria-expanded={open}      aria-controls={id}      onClick={() => {        if (controlled === undefined) setOwn(!open);        onOpenChange?.(!open);      }}      className={`-ml-3 inline-flex h-8 items-center gap-1.5 self-start rounded-full px-3 text-[13px] font-medium text-foreground cursor-pointer hover:bg-accent ${INTERACTION_PRESS} ${INTERACTION_FOCUS}`}    >      <TextMorph>{open ? labels.open : labels.closed}</TextMorph>      <Chevron open={open} reduced={reduced} />    </button>  );  return (    <div className={`flex w-full flex-col gap-1 ${className}`}>      {placement === "top" && button}      <Fold open={open} reduced={reduced} id={id}>        {children}      </Fold>      {placement === "bottom" && button}    </div>  );}export default Collapsible;

Registry item: /r/collapsible.json