Linear Modal Standalone
Linear Modal Standalone is a React and Tailwind CSS overlay component from UI Layouts, licensed MIT. Copy it and paste it into your project.
UI Layouts
MIT
overlays
What it is
Linear Modal Standalone is a React and Tailwind CSS overlay component from UI Layouts, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion lucide-react
Licence
This component comes from UI Layouts and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Linear Modal Standalone
'use client';
import { Plus, X } from 'lucide-react';
import { AnimatePresence, MotionConfig, type Transition, motion } from 'motion/react';
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
// Card item type
interface CardItem {
id: number;
url: string;
title: string;
description: string;
tags: string[];
}
// Animation config
const transition: Transition = {
type: 'spring',
bounce: 0.05,
duration: 0.3,
};
const LinearCardDialog: React.FC = () => {
const [index, setIndex] = useState<number>(0);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [carouselWidth, setCarouselWidth] = useState<number>(0);
const carousel = useRef<HTMLDivElement>(null);
// Default items (standalone, fixed)
const items: CardItem[] = [
{
id: 1,
url: 'https://images.unsplash.com/photo-1757672242146-a6a7897bcc80?q=80&w=1171&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
title: 'Accordion',
description: 'Immerse yourself in our cutting-edge interactive gallery...',
tags: ['Sunrise', 'Mountains', 'Golden', 'Scenic', 'Inspiring'],
},
{
id: 2,
url: 'https://images.unsplash.com/photo-1756806983725-977bb2308d4e?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
title: 'Globe Section',
description:
'Embark on a virtual journey around the world with our state-of-the-art 3D globe feature...',
tags: ['Misty', 'Path', 'Mysterious', 'Serene', 'Rugged'],
},
{
id: 3,
url: 'https://images.unsplash.com/photo-1756806983832-1f056cf24182?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
title: 'Image Mouse Trail',
description:
'Transform your browsing experience with our mesmerizing Image Mouse Trail feature...',
tags: ['Pathway', 'Adventure', 'Peaks', 'Challenging', 'Breathtaking'],
},
];
useEffect(() => {
if (isOpen) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape' && isOpen) {
handleCloseDialog();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.classList.remove('overflow-hidden');
};
}, [isOpen]);
useEffect(() => {
if (carousel.current) {
const scrollWidth = carousel.current.scrollWidth;
const offsetWidth = carousel.current.offsetWidth;
setCarouselWidth(scrollWidth - offsetWidth);
}
}, [items]);
const handleCardClick = (itemIndex: number): void => {
setIndex(itemIndex);
setIsOpen(true);
};
const handleCloseDialog = (): void => {
setIsOpen(false);
};
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>): void => {
if (e.target === e.currentTarget) {
handleCloseDialog();
}
};
const currentItem = items[index];
return (
<div className='relative h-full p-4'>
<MotionConfig transition={transition}>
<motion.div
ref={carousel}
drag='x'
dragElastic={0.2}
dragConstraints={{ right: 0, left: -carouselWidth }}
dragTransition={{ bounceDamping: 30 }}
className='flex w-full gap-4 py-10'
>
{items.map((item, i) => (
<motion.div
key={item.id}
className='shrink-0 flex relative flex-col overflow-hidden border dark:bg-black bg-neutral-100 hover:bg-neutral-200 dark:hover:bg-neutral-950 cursor-pointer transition-colors'
layoutId={`dialog-${item.id}`}
style={{ width: '250px', borderRadius: '12px' }}
tabIndex={i}
onClick={() => handleCardClick(i)}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleCardClick(i);
}
}}
role='button'
aria-label={`Open ${item.title} details`}
>
<motion.div layoutId={`dialog-img-${item.id}`}>
<img
src={item.url}
alt={item.title}
className='w-full h-48 object-cover'
loading='lazy'
/>
</motion.div>
<div className='flex grow flex-row items-end justify-between p-4'>
<div className='flex-1'>
<motion.h3
layoutId={`dialog-title-${item.id}`}
className='dark:text-white text-black font-semibold text-sm truncate'
>
{item.title}
</motion.h3>
</div>
<button
className='absolute bottom-2 right-2 p-2 dark:bg-neutral-800 bg-neutral-300 hover:bg-neutral-400 dark:hover:bg-neutral-700 rounded-xl transition-colors'
aria-label={`Open ${item.title}`}
tabIndex={-1}
>
<Plus className='w-4 h-4' />
</button>
</div>
</motion.div>
))}
</motion.div>
{isOpen &&
currentItem &&
createPortal(
<AnimatePresence initial={false} mode='sync'>
<motion.div
key={`backdrop-${currentItem.id}`}
className='fixed inset-0 h-full w-full dark:bg-black/50 bg-black/25 backdrop-blur-xs z-40'
variants={{ open: { opacity: 1 }, closed: { opacity: 0 } }}
initial='closed'
animate='open'
exit='closed'
onClick={handleBackdropClick}
/>
<motion.div
key='dialog'
className='pointer-events-none fixed inset-0 flex items-center justify-center z-50 p-4'
>
<motion.div
className='pointer-events-auto relative flex h-auto w-full max-w-[500px] flex-col overflow-hidden dark:bg-neutral-900 bg-white border shadow-2xl max-h-[90vh] overflow-y-auto'
layoutId={`dialog-${currentItem.id}`}
tabIndex={-1}
style={{ borderRadius: '24px' }}
role='dialog'
aria-labelledby={`dialog-title-${currentItem.id}`}
aria-describedby={`dialog-description-${currentItem.id}`}
>
<motion.div layoutId={`dialog-img-${currentItem.id}`}>
<img
src={currentItem.url}
alt={currentItem.title}
className='h-64 w-full object-cover'
/>
</motion.div>
<div className='p-6'>
<motion.h2
layoutId={`dialog-title-${currentItem.id}`}
className='text-2xl font-bold text-zinc-950 dark:text-zinc-50 mb-4'
id={`dialog-title-${currentItem.id}`}
>
{currentItem.title}
</motion.h2>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ delay: 0.1 }}
className='text-zinc-700 dark:text-zinc-300 leading-relaxed'
id={`dialog-description-${currentItem.id}`}
>
{currentItem.description}
</motion.div>
{currentItem.tags.length > 0 && (
<div className='flex flex-wrap gap-2 mt-4'>
{currentItem.tags.map((tag, tagIndex) => (
<span
key={`${tag}-${tagIndex}`}
className='px-2 py-1 text-xs bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 rounded-full'
>
{tag}
</span>
))}
</div>
)}
</div>
<button
onClick={handleCloseDialog}
className='absolute right-4 top-4 p-2 bg-black dark:bg-white rounded-lg text-primary-foreground border cursor-pointer transition-colors'
type='button'
aria-label={`Close ${currentItem.title} dialog`}
>
<X size={20} />
</button>
</motion.div>
</motion.div>
</AnimatePresence>,
document.body
)}
</MotionConfig>
</div>
);
};
export default LinearCardDialog;
'use client';
import { cn } from '@/lib/utils';
import { XIcon } from 'lucide-react';
import { AnimatePresence, MotionConfig, type Transition, type Variant, motion } from 'motion/react';
import React, { useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
interface DialogContextType {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
uniqueId: string;
triggerRef: React.RefObject<HTMLDivElement>;
}
const DialogContext = React.createContext<DialogContextType | null>(null);
function useDialog() {
const context = useContext(DialogContext);
if (!context) {
throw new Error('useDialog must be used within a DialogProvider');
}
return context;
}
type DialogProviderProps = {
children: React.ReactNode;
transition?: Transition;
};
function DialogProvider({ children, transition }: DialogProviderProps) {
const [isOpen, setIsOpen] = useState(false);
const uniqueId = useId();
const triggerRef = useRef<HTMLDivElement>(null);
const contextValue = useMemo(
() => ({ isOpen, setIsOpen, uniqueId, triggerRef }),
[isOpen, uniqueId]
);
return (
<DialogContext.Provider
//@ts-expect-error
value={contextValue}
>
<MotionConfig transition={transition}>{children}</MotionConfig>
</DialogContext.Provider>
);
}
type DialogProps = {
children: React.ReactNode;
transition?: Transition;
};
function Dialog({ children, transition }: DialogProps) {
return (
<DialogProvider>
<MotionConfig transition={transition}>{children}</MotionConfig>
</DialogProvider>
);
}
type DialogTriggerProps = {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
triggerRef?: React.RefObject<HTMLDivElement>;
};
function DialogTrigger({ children, className, style, triggerRef }: DialogTriggerProps) {
const { setIsOpen, isOpen, uniqueId } = useDialog();
const handleClick = useCallback(() => {
setIsOpen(!isOpen);
}, [isOpen, setIsOpen]);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setIsOpen(!isOpen);
}
},
[isOpen, setIsOpen]
);
return (
<motion.div
ref={triggerRef}
layoutId={`dialog-${uniqueId}`}
className={cn('relative cursor-pointer', className)}
onClick={handleClick}
onKeyDown={handleKeyDown}
style={style}
role='button'
aria-haspopup='dialog'
aria-expanded={isOpen}
aria-controls={`dialog-content-${uniqueId}`}
>
{children}
</motion.div>
);
}
type DialogContent = {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
};
function DialogContent({ children, className, style }: DialogContent) {
const { setIsOpen, isOpen, uniqueId, triggerRef } = useDialog();
const containerRef = useRef<HTMLDivElement>(null);
const [firstFocusableElement, setFirstFocusableElement] = useState<HTMLElement | null>(null);
const [lastFocusableElement, setLastFocusableElement] = useState<HTMLElement | null>(null);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
if (event.key === 'Tab') {
if (!firstFocusableElement || !lastFocusableElement) return;
if (event.shiftKey) {
if (document.activeElement === firstFocusableElement) {
event.preventDefault();
lastFocusableElement.focus();
}
} else {
if (document.activeElement === lastFocusableElement) {
event.preventDefault();
firstFocusableElement.focus();
}
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [setIsOpen, firstFocusableElement, lastFocusableElement]);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
const focusableElements = containerRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements && focusableElements.length > 0) {
setFirstFocusableElement(focusableElements[0] as HTMLElement);
setLastFocusableElement(focusableElements[focusableElements.length - 1] as HTMLElement);
// Delay focus slightly to allow animation to start
requestAnimationFrame(() => {
(focusableElements[0] as HTMLElement).focus();
});
}
if (containerRef.current) {
containerRef.current.scrollTop = 0;
}
} else {
document.body.style.overflow = '';
triggerRef.current?.focus();
}
}, [isOpen, triggerRef]);
return (
<>
<motion.div
ref={containerRef}
layoutId={`dialog-${uniqueId}`}
className={cn('overflow-hidden', className)}
style={{
...style,
willChange: 'transform, opacity', // GPU acceleration
}}
role='dialog'
aria-modal='true'
aria-labelledby={`dialog-title-${uniqueId}`}
aria-describedby={`dialog-description-${uniqueId}`}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
transition={{
type: 'spring',
damping: 25,
stiffness: 300,
mass: 0.8,
}}
>
{children}
</motion.div>
</>
);
}
type DialogContainerProps = {
children: React.ReactNode;
className?: string;
overlayClassName?: string;
style?: React.CSSProperties;
};
function DialogContainer({ children, className, overlayClassName }: DialogContainerProps) {
const { isOpen, setIsOpen, uniqueId } = useDialog();
const [mounted, setMounted] = useState(false);
useEffect(() => {
const drawerWrapper = document.querySelectorAll('[drawer-wrapper]');
if (isOpen) {
document.body.classList.add('overflow-hidden');
drawerWrapper.forEach((wrapper) => wrapper?.classList.add('open'));
} else {
document.body.classList.remove('overflow-hidden');
drawerWrapper.forEach((wrapper) => wrapper?.classList.remove('open'));
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen]);
useEffect(() => {
setMounted(true);
return () => {
setMounted(false);
};
}, []);
if (!mounted) return null;
return createPortal(
<AnimatePresence initial={false} mode='wait'>
{isOpen && (
<>
<motion.div
key={`backdrop-${uniqueId}`}
data-lenis-prevent
className={cn(
'fixed inset-0 h-full z-50 w-full backdrop-blur-xl dark:bg-[radial-gradient(125%_125%_at_50%_10%,#050505_40%,#243aff_100%)] bg-[radial-gradient(125%_125%_at_50%_10%,#ffffff_40%,#243aff_100%)]',
overlayClassName
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.2,
ease: [0.4, 0.0, 0.4, 1],
}}
onClick={() => setIsOpen(false)}
></motion.div>
<motion.div
className={cn(`fixed inset-0 z-50 w-fit mx-auto`, className)}
style={{ willChange: 'transform' }} // GPU acceleration for transforms
>
{children}
</motion.div>
</>
)}
</AnimatePresence>,
document.body
);
}
type DialogTitleProps = {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
};
function DialogTitle({ children, className, style }: DialogTitleProps) {
const { uniqueId } = useDialog();
return (
<motion.h1
layoutId={`dialog-title-container-${uniqueId}`}
className={className}
style={style}
layout
>
{children}
</motion.h1>
);
}
type DialogSubtitleProps = {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
};
function DialogSubtitle({ children, className, style }: DialogSubtitleProps) {
const { uniqueId } = useDialog();
return (
<motion.div
layoutId={`dialog-subtitle-container-${uniqueId}`}
className={className}
style={style}
>
{children}
</motion.div>
);
}
type DialogDescriptionProps = {
children: React.ReactNode;
className?: string;
disableLayoutAnimation?: boolean;
variants?: {
initial: Variant;
animate: Variant;
exit: Variant;
};
};
function DialogDescription({
children,
className,
variants,
disableLayoutAnimation,
}: DialogDescriptionProps) {
const { uniqueId } = useDialog();
return (
<motion.div
key={`dialog-description-${uniqueId}`}
layoutId={disableLayoutAnimation ? undefined : `dialog-description-content-${uniqueId}`}
variants={variants}
className={className}
initial='initial'
animate='animate'
exit='exit'
id={`dialog-description-${uniqueId}`}
>
{children}
</motion.div>
);
}
type DialogImageProps = {
src: string;
alt: string;
className?: string;
style?: React.CSSProperties;
};
function DialogImage({ src, alt, className, style }: DialogImageProps) {
const { uniqueId } = useDialog();
return (
<motion.img
src={src}
alt={alt}
className={cn(className)}
layoutId={`dialog-img-${uniqueId}`}
style={style}
/>
);
}
type DialogCloseProps = {
children?: React.ReactNode;
className?: string;
variants?: {
initial: Variant;
animate: Variant;
exit: Variant;
};
};
function DialogClose({ children, className, variants }: DialogCloseProps) {
const { setIsOpen, uniqueId } = useDialog();
const handleClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
return (
<motion.button
onClick={handleClose}
type='button'
aria-label='Close dialog'
key={`dialog-close-${uniqueId}`}
className={cn('absolute right-6 top-6 text-white', className)}
initial='initial'
animate='animate'
exit='exit'
variants={variants}
>
{children || <XIcon size={24} />}
</motion.button>
);
}
export {
Dialog,
DialogClose,
DialogContainer,
DialogContent,
DialogDescription,
DialogImage,
DialogSubtitle,
DialogTitle,
DialogTrigger,
};