Disclosure
The Disclosure component allows users to toggle the visibility of content, either collapsed or expanded.
Examples
Basic Disclosure
Disclosure with custom animation
Image from Les others studio
Code
'use client';
import * as React from 'react';
import {
AnimatePresence,
motion,
MotionConfig,
Transition,
Variant,
Variants,
} from 'framer-motion';
import { createContext, useContext, useState, useId, useEffect } from 'react';
import { cn } from '@/lib/utils';
type DisclosureContextType = {
open: boolean;
toggle: () => void;
variants?: { expanded: Variant; collapsed: Variant };
};
const DisclosureContext = createContext<DisclosureContextType | undefined>(
undefined
);
type DisclosureProviderProps = {
children: React.ReactNode;
open: boolean;
onOpenChange?: (open: boolean) => void;
variants?: { expanded: Variant; collapsed: Variant };
};
function DisclosureProvider({
children,
open: openProp,
onOpenChange,
variants,
}: DisclosureProviderProps) {
const [internalOpenValue, setInternalOpenValue] = useState<boolean>(openProp);
useEffect(() => {
setInternalOpenValue(openProp);
}, [openProp]);
const toggle = () => {
const newOpen = !internalOpenValue;
setInternalOpenValue(newOpen);
if (onOpenChange) {
onOpenChange(newOpen);
}
};
return (
<DisclosureContext.Provider
value={{
open: internalOpenValue,
toggle,
variants,
}}
>
{children}
</DisclosureContext.Provider>
);
}
function useDisclosure() {
const context = useContext(DisclosureContext);
if (!context) {
throw new Error('useDisclosure must be used within a DisclosureProvider');
}
return context;
}
type DisclosureProps = {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children: React.ReactNode;
className?: string;
variants?: { expanded: Variant; collapsed: Variant };
transition?: Transition;
};
export function Disclosure({
open: openProp = false,
onOpenChange,
children,
className,
transition,
variants,
}: DisclosureProps) {
return (
<MotionConfig transition={transition}>
<div className={className}>
<DisclosureProvider
open={openProp}
onOpenChange={onOpenChange}
variants={variants}
>
{React.Children.toArray(children)[0]}
{React.Children.toArray(children)[1]}
</DisclosureProvider>
</div>
</MotionConfig>
);
}
export function DisclosureTrigger({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
const { toggle, open } = useDisclosure();
return (
<>
{React.Children.map(children, (child) => {
return React.isValidElement(child)
? React.cloneElement(child, {
onClick: toggle,
role: 'button',
'aria-expanded': open,
tabIndex: 0,
onKeyDown: (e: { key: string; preventDefault: () => void }) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggle();
}
},
className: cn(
className,
(child as React.ReactElement).props.className
),
...(child as React.ReactElement).props,
})
: child;
})}
</>
);
}
export function DisclosureContent({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
const { open, variants } = useDisclosure();
const uniqueId = useId();
const BASE_VARIANTS: Variants = {
expanded: {
height: 'auto',
opacity: 1,
},
collapsed: {
height: 0,
opacity: 0,
},
};
const combinedVariants = {
expanded: { ...BASE_VARIANTS.expanded, ...variants?.expanded },
collapsed: { ...BASE_VARIANTS.collapsed, ...variants?.collapsed },
};
return (
<div className={cn('overflow-hidden', className)}>
<AnimatePresence initial={false}>
{open && (
<motion.div
id={uniqueId}
initial='collapsed'
animate='expanded'
exit='collapsed'
variants={combinedVariants}
>
{children}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default {
Disclosure,
DisclosureProvider,
DisclosureTrigger,
DisclosureContent,
};
Please add:
Component API
Disclosure
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | false | Controls the visibility of the content. |
onOpenChange | (open: boolean) => void | Callback function to handle changes in the open state. | |
children | ReactNode | The content to be toggled, typically includes DisclosureTrigger and DisclosureContent . | |
className | string | Additional CSS classes for the container. | |
variants | { expanded: Variant; collapsed: Variant } | Custom animation variants for expanded and collapsed states. | |
transition | Transition | Custom transition settings for the motion animations. |
DisclosureTrigger
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | The trigger element that will toggle the disclosure. | |
className | string | Additional CSS classes for the trigger element. |
DisclosureContent
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | The content that will be shown or hidden by the disclosure. | |
className | string | Additional CSS classes for the content container. |