Read More
ReadMore is an expandable text component that truncates long content to a configurable number of lines and reveals the full text with a smooth height animation.
Installation
Install with CLI
Choose your package manager.
npx @praveenlodhi/elixir-ui add read-morepnpm dlx @praveenlodhi/elixir-ui add read-moreyarn dlx @praveenlodhi/elixir-ui add read-morebun x @praveenlodhi/elixir-ui add read-moreInstall dependencies
npm install motion lucide-react class-variance-authority clsxpnpm add motion lucide-react class-variance-authority clsxyarn add motion lucide-react class-variance-authority clsxbun add motion lucide-react class-variance-authority clsxAdd ReadMore component
Create a file at components/ui/read-more.tsx and paste the source.
"use client";
import { useState } from "react";
import * as motion from "motion/react-client";
import clsx from "clsx";
import { cn } from "../lib/utils";
import { ActionButton } from "./action-button";
export interface ReadMoreProps {
children: React.ReactNode;
lines?: number;
className?: string;
duration?: number;
}
export function ReadMore({
children,
lines = 4,
className,
duration = 0.35,
}: ReadMoreProps) {
const [open, setOpen] = useState(false);
// ✅ FIXED: always convert to string
const textContent = Array.isArray(children)
? children.join("")
: String(children);
const content = textContent.split("\n\n");
return (
<div className={cn(clsx("w-full", "space-y-5"), className)}>
<motion.div
initial={false}
animate={{
height: open ? "auto" : `${lines * 1.5}em`,
}}
transition={{ duration, ease: "easeInOut" }}
className={cn("overflow-hidden")}
>
<div className={cn(clsx("whitespace-pre-line", "space-y-4"))}>
{content.map((para, i) => (
<p key={i}>{para}</p>
))}
</div>
</motion.div>
<ActionButton
variant="link"
onClick={() => setOpen(!open)}
className={cn(
clsx("cursor-pointer", "rounded-none", "p-0", "text-blue-500")
)}
>
{open ? "Read less" : "Read more"}
</ActionButton>
</div>
);
}Add ActionButton dependency
ReadMore uses ActionButton for the toggle control. Create a file at
components/ui/action-button.tsx and paste the source.
"use client";
import Link from "next/link";
import { cva, type VariantProps } from "class-variance-authority";
import clsx from "clsx";
import { ChevronDown } from "lucide-react";
import { motion } from "motion/react";
import { cn } from "../lib/utils";
const actionButtonVariants = cva(
"group relative flex items-center justify-between overflow-hidden rounded-lg px-5 py-3 cursor-pointer text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface ActionButtonProps {
children?: React.ReactNode;
icon?: React.ReactNode;
href?: string;
className?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}
export function ActionButton({
children = "Button Name",
icon = <ChevronDown size={20} />,
variant = "default",
href,
className,
onClick,
}: ActionButtonProps & VariantProps<typeof actionButtonVariants>) {
const content = (
<>
{/* TEXT */}
<div className="relative h-5 overflow-hidden">
{/* original */}
<motion.span
variants={{
rest: { y: 0 },
hover: { y: "-100%" },
}}
transition={{ duration: 0.15, ease: "easeOut" }}
className="block"
>
{children}
</motion.span>
{/* replica */}
<motion.span
variants={{
rest: { y: "100%" },
hover: { y: "0%" },
}}
transition={{ duration: 0.12, ease: "easeOut" }}
className={cn(
"absolute top-0 left-0 block",
variant === "link" && "underline"
)}
>
{children}
</motion.span>
</div>
{/* ICON */}
{icon && (
<div className="relative ml-4 h-6 w-6 overflow-hidden">
{/* original */}
<motion.div
variants={{
rest: { y: 0 },
hover: { y: "100%" },
}}
transition={{ duration: 0.18, ease: "easeOut" }}
className="flex h-full w-full items-center justify-center"
>
{icon}
</motion.div>
{/* replica */}
<motion.div
variants={{
rest: { y: "-100%" },
hover: { y: "0%" },
}}
transition={{ duration: 0.18, ease: "easeOut" }}
className="absolute top-0 left-0 flex h-full w-full items-center justify-center"
>
{icon}
</motion.div>
</div>
)}
</>
);
if (href) {
return (
<motion.div
initial="rest"
whileHover="hover"
animate="rest"
data-variant={variant}
className={cn(
clsx(actionButtonVariants({ variant })),
!icon && "justify-center",
className
)}
>
<Link href={href} className="flex w-full items-center justify-between">
{content}
</Link>
</motion.div>
);
}
return (
<motion.button
initial="rest"
whileHover="hover"
animate="rest"
onClick={onClick}
data-variant={variant}
className={cn(
clsx(actionButtonVariants({ variant })),
!icon && "justify-center",
className
)}
>
{content}
</motion.button>
);
}Usage
import { ReadMore } from "@workspace/ui/components/read-more";
export function Demo() {
return (
<ReadMore lines={3} duration={0.3}>
{`ReadMore keeps long content readable by default and lets users expand when
they need full details. You can tune line count and animation duration to
fit the tone of your interface.
Use it for product descriptions, changelogs, and anywhere you want to balance brevity
with context. The smooth height animation makes the transition feel natural, so
users can focus on the content instead of the mechanics of expanding and collapsing.`}
</ReadMore>
);
}Props
Prop
Type
Action Button
ActionButton is an animated CTA component with sliding text and icon transitions, designed for high-emphasis actions with multiple visual variants.
Quantity Stepper
A compact, animated quantity control that toggles between an ADD button and a stepper interface with increment and decrement actions. Built with smooth transitions using Motion Library. Designed as a reusable UI component for selecting item quantities in modern applications.