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 { Drawer, DrawerClose as MorphosDrawerClose, DrawerContent as MorphosDrawerContent, DrawerDescription as MorphosDrawerDescription, DrawerTitle as MorphosDrawerTitle, DrawerTrigger, type DrawerCloseProps as MorphosDrawerCloseProps, type DrawerContentProps as MorphosDrawerContentProps, type DrawerDescriptionProps as MorphosDrawerDescriptionProps, type DrawerTitleProps as MorphosDrawerTitleProps, type DrawerProps, type DrawerTriggerProps} from "@morphos/overlays";import { cn } from "@/lib/utils";// Re-exported directly, same reason as `Dialog` in `dialog.tsx`: `new Drawer()` must keep// `.isOpen`/`.openDrawer()`, which a wrapping component class would not have.export { Drawer, DrawerTrigger, type DrawerProps, type DrawerTriggerProps };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 { Drawer, DrawerClose as MorphosDrawerClose, DrawerContent as MorphosDrawerContent, DrawerDescription as MorphosDrawerDescription, DrawerTitle as MorphosDrawerTitle, DrawerTrigger, type DrawerCloseProps as MorphosDrawerCloseProps, type DrawerContentProps as MorphosDrawerContentProps, type DrawerDescriptionProps as MorphosDrawerDescriptionProps, type DrawerTitleProps as MorphosDrawerTitleProps, type DrawerProps, type DrawerTriggerProps} 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 — `.media()` doesn't compose with `.on()` here. .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 });}// Re-exported directly, same reason as `Dialog` in `dialog.tsx`: `new Drawer()` must keep// `.isOpen`/`.openDrawer()`, which a wrapping component class would not have.export { Drawer, DrawerTrigger, type DrawerProps, type DrawerTriggerProps };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.
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.