Anchored Sheet
Categorystyled
ArchitectureReact
Client
Component
Dependencies
Usage
import { useState } from "react";
import { Button } from "@/components/react/styled/button";
import { AnchoredSheet } from "@/components/react/styled/anchoredSheet";
import { DemoWrapper } from "@/demos/react/demoWrapper";
export const AnchoredSheetDemo = ({ code }: { code: string }) => {
const [state, setState] = useState(false);
return (
<DemoWrapper code={code}>
<Button onClick={setState.bind(null, true)}>Open Anchored Sheet</Button>
<AnchoredSheet title="Anchored Sheet" state={state} setState={setState}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae commodi
inventore amet nisi ab! Itaque quas numquam repellendus, quo aliquid
nulla minus quos recusandae illo, hic distinctio nihil maxime.
Voluptatum. Blanditiis inventore expedita et aliquam obcaecati dicta
perspiciatis similique temporibus officia! Obcaecati minus aut
repudiandae, quasi quaerat fugit voluptates nam odit voluptatem est et
sit quo pariatur iste reiciendis sapiente. Eius possimus ex molestiae
sed consequatur ut voluptates numquam earum saepe dolor eum odio nostrum
repellendus, tempore accusantium, quod repudiandae ratione non facere?
Ab laborum ut, praesentium accusantium qui minus.
</AnchoredSheet>
</DemoWrapper>
);
};
Install Instructions
Verifying dependencies
Make sure the following dependencies are satisfied:
- dialog
- useTouchDialogDrag
Copy and paste the code into that file
import { Suspense, lazy } from "react";
// CVA
import { cva } from "class-variance-authority";
// Sub-Component
const Dialog = lazy(() =>
import("@/components/react/primitive/dialog").then((module) => ({
default: module.Dialog,
}))
);
import { Button } from "@/components/react/styled/button";
// Types
import type { DialogProps } from "@/components/react/primitive/dialog";
// Icon
import { XMarkIcon } from "@heroicons/react/24/solid";
// Hooks
import { useTouchDialogDrag } from "@/hooks/react/useTouchDialogDrag";
export interface AnchoredSheetProps
extends Omit<DialogProps, "className" | "ref"> {
title: string;
}
const anchoredSheetClassNames = cva([
"opacity-100",
"motion-reduce:opacity-0",
"w-[100%]",
"top-[calc(10dvh_+_env(safe-area-inset-top)_+_env(safe-area-inset-bottom))]",
"h-[calc(90dvh_-_env(safe-area-inset-top)_-_env(safe-area-inset-bottom))]",
"left-0",
"bottom-auto",
"p-6",
"md:p-12",
"bg-neutral-100",
"dark:bg-neutral-900",
"rounded-t-3xl",
"justify-start",
"motion-safe:translate-y-[100%]",
"data-[display=true]:translate-y-0",
"md:w-auto",
"md:h-[max(400px,_40vh)]",
"md:rounded-3xl",
"md:aspect-video",
"md:top-[50%]",
"md:left-[50%]",
"md:bottom-auto",
"md:m-0",
"md:translate-y-[-50%]",
"md:transition-all",
"md:translate-x-[-50%]",
"md:data-[display=false]:scale-105",
"md:data-[display=false]:opacity-0",
"md:data-[display=true]:opacity-1",
"md:data-[display=true]:translate-y-[-50%]",
"md:data-[display=true]:blur-0",
"md:data-[display=false]:blur-sm",
"md:duration-[var(--transition-duration)]",
"md:border",
"md:dark:border-neutral-700",
"md:border-neutral-300",
"standalone:iphone-portrait:rounded-t-[3rem]",
"standalone:iphone-portrait:pl-[calc(theme(space.6)_-_env(safe-area-inset-left))]",
"standalone:iphone-portrait:pr-[calc(theme(space.6)_-_env(safe-area-inset-right))]",
"standalone:iphone-portrait:pb-[calc(theme(space.6)_-_env(safe-area-inset-bottom))]",
"[box-shadow:_0_75vh_0_50vh_theme(colors.neutral.100)]",
"dark:[box-shadow:_0_75vh_0_50vh_theme(colors.neutral.900)]",
"md:[box-shadow:_none_!important]",
]);
/**
* Anchored Sheets provide an elegant way to display information and actions
*/
export const AnchoredSheet = ({
children,
title,
setState,
...props
}: AnchoredSheetProps): JSX.Element => {
const { ref, onTouchEnd, onTouchMove } = useTouchDialogDrag({
onClose: setState?.bind(null, false),
});
return (
<Suspense fallback={null}>
<Dialog
ref={ref}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
className={anchoredSheetClassNames()}
transitionDuration={350}
setState={setState}
{...props}
>
<header
data-draggable={true}
className="mb-6 flex justify-end md:justify-between align-middle items-center w-[100%] relative"
>
<p
data-draggable={true}
className="absolute md:static top-[50%] left-[50%] md:top-auto md:left-auto translate-x-[-50%] translate-y-[-50%] md:translate-x-0 md:translate-y-0 p-0 m-0 text-center text-xl font-semibold"
>
{title}
</p>
<Button
square={true}
onClick={() => setState && setState(false)}
className="rounded-[100%]"
>
<XMarkIcon className="w-4 h-4" />
</Button>
</header>
{children && (
<div
data-draggable={true}
data-scrollable={true}
className="max-h-[100%] w-[100%] overflow-y-auto md:grow md:flex md:justify-center md:align-bottom md:items-end"
>
{children}
</div>
)}
</Dialog>
</Suspense>
);
};