Reveal

  • Easy integration with any ReactNode content
  • Conditionally renders the button based on content height
  • Customizable maximum height for flexible content display
  • Smoothly handles dynamic content changes with ResizeObserver
  • Offers multiple button styles for different UI needs

Installation

Add the following Soul components

The reveal component uses the animated-underline and button components. Make sure you have added them to your project.

Install the following dependencies

npm install clsx

Copy and paste the following code into your project

primitives/reveal/index.tsx

'use client';import { clsx } from 'clsx';import { ReactNode, useEffect, useRef, useState } from 'react';import { AnimatedUnderline } from '@/vibes/soul/primitives/animated-underline';import { Button } from '@/vibes/soul/primitives/button';export interface RevealProps {  variant?: 'underline' | 'button';  showLabel?: string;  hideLabel?: string;  defaultOpen?: boolean;  children: ReactNode;  maxHeight?: string;}export function Reveal({  variant = 'underline',  showLabel = 'Show more',  hideLabel = 'Show less',  defaultOpen = false,  maxHeight = '10rem',  children,}: RevealProps) {  const [isOpen, setIsOpen] = useState(defaultOpen);  const [hasOverflow, setHasOverflow] = useState(true);  const contentRef = useRef<HTMLDivElement>(null);  function convertToPixels(value: string): number {    const num = parseFloat(value);    if (value.endsWith('rem')) {      return num * 16; // Convert rem to pixels (1rem = 16px)    }    if (value.endsWith('px')) {      return num;    }    return num;  }  useEffect(() => {    function checkHeight() {      if (contentRef.current) {        const contentHeight = contentRef.current.scrollHeight;        const maxHeightPx = convertToPixels(maxHeight);        setHasOverflow(contentHeight > maxHeightPx);      }    }    checkHeight();    const resizeObserver = new ResizeObserver(checkHeight);    if (contentRef.current) {      resizeObserver.observe(contentRef.current);    }    return () => {      resizeObserver.disconnect();    };  }, [maxHeight]);  return (    <div className="relative">      <div        ref={contentRef}        className={clsx(          hasOverflow &&            !isOpen &&            '[mask-image:linear-gradient(to_top,transparent,black_50px,black_calc(100%-50px))]',          'overflow-hidden',        )}        style={{ maxHeight: isOpen ? 'none' : maxHeight }}      >        {children}      </div>      {hasOverflow && (        <div className={clsx('flex w-full items-end pt-4')}>          {variant === 'underline' && (            <button              className="group/underline text-sm focus:outline-none"              onClick={() => setIsOpen(!isOpen)}              type="button"            >              <AnimatedUnderline>{isOpen ? hideLabel : showLabel}</AnimatedUnderline>            </button>          )}          {variant === 'button' && (            <Button              variant="tertiary"              size="x-small"              onClick={() => setIsOpen(!isOpen)}              type="button"            >              {isOpen ? hideLabel : showLabel}            </Button>          )}        </div>      )}    </div>  );}

Usage

import { Reveal } from '@/vibes/soul/primitives/reveal';export default function Preview() {return (  <Reveal>    <p>      To begin your return, simply log into your account on our website or contact our dedicated      customer service team. Once your return is authorized, we&apos;ll provide you with a prepaid      shipping label for domestic returns.    </p>  </Reveal>);}

API Reference

RevealProps

PropTypeDefault
children*
ReactNode
defaultOpen
boolean
false
hideLabel
string
'Show less'
maxHeight
string
'10rem'
showLabel
string
'Show more'
variant
'underline' | 'button'
'underline'