Sheet
Extends the Dialog component to display content that complements the main content of the screen.
import { css } from 'remix/component'
import { Button, buttonVariants } from '#app/ui/button.tsx'
import { Input } from '#app/ui/input.tsx'
import { Label } from '#app/ui/label.tsx'
import {import { css } from 'remix/component'
import { Button, buttonVariants } from '#app/ui/button.tsx'
import { Input } from '#app/ui/input.tsx'
import { Label } from '#app/ui/label.tsx'
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '#app/ui/sheet.tsx'
const field = css({ display: 'grid', gap: '0.75rem' })
export function SheetDemo() {
return () => (
<Sheet>
<SheetTrigger mix={buttonVariants({ variant: 'outline' })}>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<div
mix={css({
display: 'grid',
flex: '1 1 0%',
gridAutoRows: 'min-content',
gap: '1.5rem',
paddingInline: '1rem',
})}
>
<div mix={field}>
<Label htmlFor="sheet-demo-name">Name</Label>
<Input id="sheet-demo-name" defaultValue="Pedro Duarte" />
</div>
<div mix={field}>
<Label htmlFor="sheet-demo-username">Username</Label>
<Input id="sheet-demo-username" defaultValue="@peduarte" />
</div>
</div>
<SheetFooter>
<Button type="submit">Save changes</Button>
<SheetClose mix={buttonVariants({ variant: 'outline' })}>Close</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/sheet.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/sheet.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/sheet.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/sheet.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, ref, type CSSMixinDescriptor, type Handle, type Props } from 'remix/component'
import { Dialog, DialogClose, DialogOverlay, DialogPortal, DialogTrigger, type DialogProps } from './dialog.tsx'
import { XIcon } from './icons.tsx'
export type SheetSide = 'top' | 'right' | 'bottom' | 'left'
export function Sheet(handle: Handle<DialogProps>) {
return () => <Dialog {...handle.props} />
}
export function SheetTrigger(handle: Handle<Props<'button'>>) {
return () => <DialogTrigger data-slot="sheet-trigger" {...handle.props} />
}
export function SheetClose(handle: Handle<Props<'button'>>) {
return () => <DialogClose data-slot="sheet-close" {...handle.props} />
}
function SheetPortal(handle: Handle<Props<'dialog'>>) {
return () => <DialogPortal data-slot="sheet-portal" {...handle.props} />
}
function SheetOverlay(handle: Handle<Props<'div'>>) {
return () => <DialogOverlay data-slot="sheet-overlay" {...handle.props} />
}
const easeInOut = 'cubic-bezier(0.4, 0, 0.2, 1)'
const contentStyles = css({
position: 'fixed',
zIndex: 50,
display: 'flex',
flexDirection: 'column',
gap: '1rem',
backgroundColor: 'var(--background)',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
transitionProperty:
'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, translate, scale, rotate, filter, backdrop-filter, display, content-visibility, overlay, pointer-events',
transitionTimingFunction: easeInOut,
transitionDuration: '150ms',
'&[data-state="open"]': {
transitionDuration: '500ms',
animation: `enter 500ms ${easeInOut}`,
},
'&[data-state="closed"]': {
transitionDuration: '300ms',
animation: `exit 300ms ${easeInOut}`,
animationFillMode: 'forwards',
},
})
const sides: Record<SheetSide, CSSMixinDescriptor> = {
right: css({
top: 0,
bottom: 0,
right: 0,
height: '100%',
width: '75%',
borderLeftWidth: '1px',
'--tw-enter-translate-x': '100%',
'--tw-exit-translate-x': '100%',
'@media (min-width: 40rem)': { maxWidth: '24rem' },
}),
left: css({
top: 0,
bottom: 0,
left: 0,
height: '100%',
width: '75%',
borderRightWidth: '1px',
'--tw-enter-translate-x': '-100%',
'--tw-exit-translate-x': '-100%',
'@media (min-width: 40rem)': { maxWidth: '24rem' },
}),
top: css({
left: 0,
right: 0,
top: 0,
height: 'auto',
borderBottomWidth: '1px',
'--tw-enter-translate-y': '-100%',
'--tw-exit-translate-y': '-100%',
}),
bottom: css({
left: 0,
right: 0,
bottom: 0,
height: 'auto',
borderTopWidth: '1px',
'--tw-enter-translate-y': '100%',
'--tw-exit-translate-y': '100%',
}),
}
const closeButtonStyles = css({
position: 'absolute',
top: '1rem',
right: '1rem',
borderRadius: '0.125rem',
opacity: 0.7,
transitionProperty: 'opacity',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
'&:hover': { opacity: 1 },
'&:focus': {
boxShadow: '0 0 0 2px var(--background), 0 0 0 4px var(--ring)',
outlineStyle: 'none',
},
'&:disabled': { pointerEvents: 'none' },
})
const iconSize = css({ width: '1rem', height: '1rem' })
const srOnly = css({
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
borderWidth: 0,
})
export interface SheetContentProps extends Props<'div'> {
/** The edge of the screen the sheet slides in from. */
side?: SheetSide
/** Render the close (X) button in the top-right corner. */
showCloseButton?: boolean
}
export function SheetContent(handle: Handle<SheetContentProps>) {
let dialog = handle.context.get(Dialog)
return () => {
let { side = 'right', showCloseButton = true, mix, children, ...props } = handle.props
return (
<SheetPortal>
<SheetOverlay />
<div
data-slot="sheet-content"
data-state={dialog.open ? 'open' : 'closed'}
{...props}
mix={[
contentStyles,
sides[side],
ref((node, signal) => {
dialog.content = node
signal.addEventListener('abort', () => {
if (dialog.content === node) dialog.content = null
})
}),
mix,
]}
>
{children}
{showCloseButton && (
<SheetClose mix={closeButtonStyles}>
<XIcon class="size-4" mix={iconSize} />
<span mix={srOnly}>Close</span>
</SheetClose>
)}
</div>
</SheetPortal>
)
}
}
const headerStyles = css({ display: 'flex', flexDirection: 'column', gap: '0.375rem', padding: '1rem' })
export function SheetHeader(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="sheet-header" {...props} mix={[headerStyles, mix]} />
}
}
const footerStyles = css({
marginTop: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
padding: '1rem',
})
export function SheetFooter(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="sheet-footer" {...props} mix={[footerStyles, mix]} />
}
}
const titleStyles = css({ fontWeight: 600, color: 'var(--foreground)' })
export function SheetTitle(handle: Handle<Props<'h2'>>) {
let dialog = handle.context.get(Dialog)
return () => {
let { mix, ...props } = handle.props
return <h2 id={dialog.titleId} data-slot="sheet-title" {...props} mix={[titleStyles, mix]} />
}
}
const descriptionStyles = css({
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
color: 'var(--muted-foreground)',
})
export function SheetDescription(handle: Handle<Props<'p'>>) {
let dialog = handle.context.get(Dialog)
return () => {
let { mix, ...props } = handle.props
return (
<p id={dialog.descriptionId} data-slot="sheet-description" {...props} mix={[descriptionStyles, mix]} />
)
}
}Update the import paths to match your project setup.
Usage
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '#app/ui/sheet.tsx'<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>This action cannot be undone.</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>Examples
Side
Use the side prop on SheetContent to set the edge of the screen where the sheet appears. Values are top, right, bottom, or left.
import { css } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import { Input } from '#app/ui/input.tsx'
import { Label } from '#app/ui/label.tsx'
import {import { css } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import { Input } from '#app/ui/input.tsx'
import { Label } from '#app/ui/label.tsx'
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '#app/ui/sheet.tsx'
const SHEET_SIDES = ['top', 'right', 'bottom', 'left'] as const
const row = css({
display: 'grid',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
alignItems: 'center',
gap: '1rem',
})
const labelRight = css({ textAlign: 'right' })
const span3 = css({ gridColumn: 'span 3 / span 3' })
export function SheetSide() {
return () => (
<div mix={css({ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '0.5rem' })}>
{SHEET_SIDES.map((side) => (
<Sheet key={side}>
<SheetTrigger mix={buttonVariants({ variant: 'outline' })}>{side}</SheetTrigger>
<SheetContent side={side}>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<div mix={css({ display: 'grid', gap: '1rem', paddingBlock: '1rem' })}>
<div mix={row}>
<Label htmlFor="name" mix={labelRight}>
Name
</Label>
<Input id="name" value="Pedro Duarte" mix={span3} />
</div>
<div mix={row}>
<Label htmlFor="username" mix={labelRight}>
Username
</Label>
<Input id="username" value="@peduarte" mix={span3} />
</div>
</div>
<SheetFooter>
<SheetClose type="submit" mix={buttonVariants()}>
Save changes
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
))}
</div>
)
}API Reference
Sheet
| Prop | Type | Default |
|---|---|---|
open | boolean | - |
defaultOpen | boolean | - |
onOpenChange | ((open: boolean) => void) | - |
SheetTrigger
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
SheetClose
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
SheetContent
| Prop | Type | Default |
|---|---|---|
side | SheetSide | "right" |
showCloseButton | boolean | true |
mix | MixValue | - |
SheetHeader
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
SheetFooter
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
SheetTitle
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
SheetDescription
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |