Components
Drawer
A side panel sliding in from any edge, wrapping Morphos's Drawer primitive.
Installation
npx kosmesis add drawerpnpm dlx kosmesis add draweryarn dlx kosmesis add drawerbunx kosmesis add drawerInstall the following dependencies:
npm install @morphos/overlayspnpm add @morphos/overlaysyarn add @morphos/overlaysbun add @morphos/overlaysCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { DrawerClose as MorphosDrawerClose, DrawerContent as MorphosDrawerContent, DrawerDescription as MorphosDrawerDescription, DrawerTitle as MorphosDrawerTitle, type DrawerCloseProps as MorphosDrawerCloseProps, type DrawerContentProps as MorphosDrawerContentProps, type DrawerDescriptionProps as MorphosDrawerDescriptionProps, type DrawerTitleProps as MorphosDrawerTitleProps} from "@morphos/overlays";import { cn } from "@/lib/utils";/** * `Drawer` and `DrawerTrigger` are re-exported directly — see the note in `dialog.tsx` for why: * `new Drawer()` must produce a real instance with `.isOpen`/`.openDrawer()`, which a wrapping * component class would not have. */export { Drawer, DrawerTrigger, type DrawerProps, type DrawerTriggerProps } from "@morphos/overlays";export type DrawerContentProps = MorphosDrawerContentProps;@Component()export class DrawerContent extends StatelessComponent<DrawerContentProps> { render() { const { class: cls, ...rest } = this.props; return ( <MorphosDrawerContent class={cn( "fixed z-50 flex flex-col gap-4 bg-background shadow-lg outline-none", "data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:border-b", "data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:border-t", "data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l sm:data-[side=right]:max-w-sm", "data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r sm:data-[side=left]:max-w-sm", "p-6", "data-open:animate-in", cls, )} {...rest} /> ); }}export interface DrawerSlotProps { class?: string; children?: Children;}@Component()export class DrawerHeader extends StatelessComponent<DrawerSlotProps> { render() { const { class: cls, children } = this.props; return <div class={cn("flex flex-col gap-1.5", cls)}>{children}</div>; }}@Component()export class DrawerFooter extends StatelessComponent<DrawerSlotProps> { render() { const { class: cls, children } = this.props; return <div class={cn("mt-auto flex flex-col gap-2", cls)}>{children}</div>; }}export type DrawerTitleProps = MorphosDrawerTitleProps;@Component()export class DrawerTitle extends StatelessComponent<DrawerTitleProps> { render() { const { class: cls, ...rest } = this.props; return <MorphosDrawerTitle class={cn("font-semibold text-foreground", cls)} {...rest} />; }}export type DrawerDescriptionProps = MorphosDrawerDescriptionProps;@Component()export class DrawerDescription extends StatelessComponent<DrawerDescriptionProps> { render() { const { class: cls, ...rest } = this.props; return <MorphosDrawerDescription class={cn("text-sm text-muted-foreground", cls)} {...rest} />; }}export type DrawerCloseProps = MorphosDrawerCloseProps;@Component()export class DrawerClose extends StatelessComponent<DrawerCloseProps> { render() { return <MorphosDrawerClose {...this.props} />; }}Install the following dependencies:
npm install @morphos/overlays @praxisjs/csspnpm add @morphos/overlays @praxisjs/cssyarn add @morphos/overlays @praxisjs/cssbun add @morphos/overlays @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { DrawerClose as MorphosDrawerClose, DrawerContent as MorphosDrawerContent, DrawerDescription as MorphosDrawerDescription, DrawerTitle as MorphosDrawerTitle, type DrawerCloseProps as MorphosDrawerCloseProps, type DrawerContentProps as MorphosDrawerContentProps, type DrawerDescriptionProps as MorphosDrawerDescriptionProps, type DrawerTitleProps as MorphosDrawerTitleProps} from "@morphos/overlays";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class DrawerStyles extends Stylesheet { $content = this.css({ position: "fixed", zIndex: 50, display: "flex", flexDirection: "column", gap: "1rem", backgroundColor: t.background, padding: "1.5rem", boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)", outline: "none", }) .on('&[data-side="top"]', { insetInline: "0", top: "0", borderBottom: `1px solid ${t.border}` }) .on('&[data-side="bottom"]', { insetInline: "0", bottom: "0", borderTop: `1px solid ${t.border}` }) // Capped at 24rem regardless of viewport — a simpler equivalent of Tailwind's // `sm:max-w-sm` (only capping above the `sm` breakpoint), since the effect is barely // distinguishable in practice and this avoids a media query nested inside a data-attribute // selector, which the CSS builder's `.media()` doesn't compose with `.on()` for. .on('&[data-side="right"]', { insetBlock: "0", right: "0", height: "100%", width: "75%", maxWidth: "24rem", borderLeft: `1px solid ${t.border}` }) .on('&[data-side="left"]', { insetBlock: "0", left: "0", height: "100%", width: "75%", maxWidth: "24rem", borderRight: `1px solid ${t.border}` }); $header = this.css({ display: "flex", flexDirection: "column", gap: "0.375rem" }); $footer = this.css({ marginTop: "auto", display: "flex", flexDirection: "column", gap: "0.5rem" }); $title = this.css({ fontWeight: 600, color: t.foreground }); $description = this.css({ fontSize: "0.875rem", color: t.mutedForeground });}/** * `Drawer` and `DrawerTrigger` are re-exported directly — see the note in `dialog.tsx` for why: * `new Drawer()` must produce a real instance with `.isOpen`/`.openDrawer()`, which a wrapping * component class would not have. */export { Drawer, DrawerTrigger, type DrawerProps, type DrawerTriggerProps } from "@morphos/overlays";export type DrawerContentProps = MorphosDrawerContentProps;@Component()export class DrawerContent extends StatelessComponent<DrawerContentProps> { @Styled(DrawerStyles) $s!: DrawerStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosDrawerContent class={cx(this.$s.$content, cls)} {...rest} />; }}export interface DrawerSlotProps { class?: string; children?: Children;}@Component()export class DrawerHeader extends StatelessComponent<DrawerSlotProps> { @Styled(DrawerStyles) $s!: DrawerStyles; render() { const { class: cls, children } = this.props; return <div class={cx(this.$s.$header, cls)}>{children}</div>; }}@Component()export class DrawerFooter extends StatelessComponent<DrawerSlotProps> { @Styled(DrawerStyles) $s!: DrawerStyles; render() { const { class: cls, children } = this.props; return <div class={cx(this.$s.$footer, cls)}>{children}</div>; }}export type DrawerTitleProps = MorphosDrawerTitleProps;@Component()export class DrawerTitle extends StatelessComponent<DrawerTitleProps> { @Styled(DrawerStyles) $s!: DrawerStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosDrawerTitle class={cx(this.$s.$title, cls)} {...rest} />; }}export type DrawerDescriptionProps = MorphosDrawerDescriptionProps;@Component()export class DrawerDescription extends StatelessComponent<DrawerDescriptionProps> { @Styled(DrawerStyles) $s!: DrawerStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosDrawerDescription class={cx(this.$s.$description, cls)} {...rest} />; }}export type DrawerCloseProps = MorphosDrawerCloseProps;@Component()export class DrawerClose extends StatelessComponent<DrawerCloseProps> { render() { return <MorphosDrawerClose {...this.props} />; }}Examples
Usage
import { buttonVariants } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
@Component()
class MobileNav extends StatefulComponent {
@State() drawer = new Drawer({ side: "left" });
render() {
return (
<>
<DrawerTrigger drawer={this.drawer} class={buttonVariants({ variant: "outline" })}>
Open menu
</DrawerTrigger>
<DrawerContent drawer={this.drawer} aria-labelledby="nav-title">
<DrawerHeader>
<DrawerTitle id="nav-title">Navigation</DrawerTitle>
<DrawerDescription>Browse the site.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose drawer={this.drawer} class={buttonVariants({ variant: "outline" })}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</>
);
}
}Props
Drawer
Re-exported directly from @morphos/overlays — instantiate it in state (@State() drawer = new Drawer()) and pass it to every part below.
| Prop | Type | Default |
|---|---|---|
open | boolean | — controlled |
defaultOpen | boolean | — |
onOpenChange | (open: boolean) => void | — |
closeOnEscape | boolean | true |
closeOnBackdropClick | boolean | true |
side | "top" | "right" | "bottom" | "left" | "right" |
side is which edge the drawer slides in from. See Sheet for the
shadcn/ui-named alias.
DrawerTrigger
| Prop | Type | Default |
|---|---|---|
drawer | Drawer | — required |
DrawerContent
| Prop | Type | Default |
|---|---|---|
drawer | Drawer | — required |
aria-label | string | — |
aria-labelledby | string | — |
aria-describedby | string | — |
DrawerTitle
| Prop | Type | Default |
|---|---|---|
as | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | — |
DrawerClose
| Prop | Type | Default |
|---|---|---|
drawer | Drawer | — required |
DrawerHeader, DrawerFooter, and DrawerDescription take no props beyond class/children.