Kosmesis
Components

Drawer

A side panel sliding in from any edge, wrapping Morphos's Drawer primitive.

Installation

npx kosmesis add drawer
pnpm dlx kosmesis add drawer
yarn dlx kosmesis add drawer
bunx kosmesis add drawer

Install the following dependencies:

npm install @morphos/overlays
pnpm add @morphos/overlays
yarn add @morphos/overlays
bun add @morphos/overlays

Copy and paste the following code into your project.

drawer.tsx
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/css
pnpm add @morphos/overlays @praxisjs/css
yarn add @morphos/overlays @praxisjs/css
bun add @morphos/overlays @praxisjs/css

Copy and paste the following code into your project.

drawer.tsx
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.

PropTypeDefault
openboolean— controlled
defaultOpenboolean
onOpenChange(open: boolean) => void
closeOnEscapebooleantrue
closeOnBackdropClickbooleantrue
side"top" | "right" | "bottom" | "left""right"

side is which edge the drawer slides in from. See Sheet for the shadcn/ui-named alias.

DrawerTrigger

PropTypeDefault
drawerDrawer— required

DrawerContent

PropTypeDefault
drawerDrawer— required
aria-labelstring
aria-labelledbystring
aria-describedbystring

DrawerTitle

PropTypeDefault
as"h1" | "h2" | "h3" | "h4" | "h5" | "h6"

DrawerClose

PropTypeDefault
drawerDrawer— required

DrawerHeader, DrawerFooter, and DrawerDescription take no props beyond class/children.

On this page