Styleframe Logo
Overlays

Drawer

An edge-anchored, slide-out panel composed of overlay, container, header, body, and footer sections. Anchors to any screen edge and supports multiple colors, visual styles, and sizes through the recipe system.

Overview

The Drawer is an edge-anchored, slide-out panel — often called a sheet — used for navigation, filters, detail views, and secondary forms that slide in from the side of the screen. It is composed of five recipe parts: useDrawerRecipe() for the container, useDrawerHeaderRecipe() for the top section with a bottom separator, useDrawerBodyRecipe() for the main content area, useDrawerFooterRecipe() for the bottom section with right-aligned actions, and useDrawerOverlayRecipe() for the full-screen backdrop. Each composable creates a fully configured recipe with color, variant, size, and side options — plus compound variants that handle the color-variant combinations and per-side dimensions automatically.

A Drawer is essentially a Modal anchored to a screen edge: it shares the same neutral surface colors, visual styles, and section structure, but instead of centering, the panel pins itself to the top, right, bottom, or left of the viewport. The Drawer recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.

Why use the Drawer recipe?

The Drawer recipe helps you:

  • Ship faster with sensible defaults: Get 3 colors, 3 visual styles, 3 sizes, and 4 anchor sides out of the box with a single set of composable calls.
  • Anchor to any edge: The side axis pins the panel to the top, right, bottom, or left of the screen, with the correct border edge and dimensions for each.
  • Compose structured layouts: Five coordinated recipes (overlay, container, header, body, footer) share the same variant axes, so your drawers stay internally consistent.
  • Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including separator colors and dark mode overrides.
  • Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
  • Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, size, or side values at compile time.

Usage

Register the recipes

Add the Drawer recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:

src/components/drawer.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import {
    useDrawerRecipe,
    useDrawerHeaderRecipe,
    useDrawerBodyRecipe,
    useDrawerFooterRecipe,
    useDrawerOverlayRecipe,
} from '@styleframe/theme';

const s = styleframe();

const drawer = useDrawerRecipe(s);
const drawerHeader = useDrawerHeaderRecipe(s);
const drawerBody = useDrawerBodyRecipe(s);
const drawerFooter = useDrawerFooterRecipe(s);
const drawerOverlay = useDrawerOverlayRecipe(s);

export default s;

Build the component

Import the drawer, drawerHeader, drawerBody, drawerFooter, and drawerOverlay runtime functions from the virtual module and pass variant props to compute class names. The side prop controls which edge the panel anchors to:

src/components/Drawer.tsx
import { drawer, drawerHeader, drawerBody, drawerFooter, drawerOverlay, type DrawerProps } from "virtual:styleframe";

export function Drawer({
    color = "neutral",
    variant = "solid",
    size = "md",
    side = "right",
    open = false,
    title,
    description,
    footer,
    children,
    onClose,
}: DrawerProps & { open?: boolean; title?: string; description?: string; footer?: React.ReactNode; children?: React.ReactNode; onClose?: () => void }) {
    if (!open) return null;

    return (
        <div className={drawerOverlay()} onClick={(e) => {
            if (e.target === e.currentTarget) onClose?.();
        }}>
            <div
                className={drawer({ color, variant, size, side })}
                role="dialog"
                aria-modal="true"
                aria-labelledby="drawer-title"
            >
                {title && (
                    <div className={drawerHeader({ color, variant, size })}>
                        <h2 id="drawer-title">{title}</h2>
                    </div>
                )}
                <div className={drawerBody({ size })}>
                    {description && <p>{description}</p>}
                    {children}
                </div>
                {footer && (
                    <div className={drawerFooter({ color, variant, size })}>
                        {footer}
                    </div>
                )}
            </div>
        </div>
    );
}

See it in action

Colors

The Drawer recipe includes 3 color variants: light, dark, and neutral. Like the Modal and Card recipes, the Drawer uses neutral-spectrum colors designed for content surfaces rather than status communication. Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including dark mode overrides.

The neutral color adapts automatically: it uses a light appearance in light mode and a dark appearance in dark mode, making it the safest default for general-purpose drawers.

Color Reference

ColorTokenUse Case
light@color.white / @color.gray-*Light surfaces, stays light in dark mode
dark@color.gray-900Dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use neutral as your default drawer color. It adapts automatically to the user's color scheme, so you don't need to manage light and dark variants separately.

Variants

Three visual style variants control how the drawer is rendered. Each variant is combined with the selected color through compound variants, so you always get the correct background, text, border, and separator colors for your chosen color.

Solid

Filled background with a subtle border on the anchored edge. The most prominent style, ideal for primary navigation and focused panels.

Soft

Light tinted background with no visible border. A gentle, borderless style that works well for informational and less critical panels.

Subtle

Light tinted background with a matching border on the anchored edge. Combines the softness of the soft variant with added visual definition.

Sides

The side axis is the defining feature of the Drawer. It pins the panel to one edge of the screen, sets the single border edge that faces the content, and determines how the panel is sized.

SideAnchors toBorder edgeSizing
leftLeft edge, full heightRight borderWidth from size
rightRight edge, full heightLeft borderWidth from size
topTop edge, full widthBottom borderHeight fits content
bottomBottom edge, full widthTop borderHeight fits content

The container is position: fixed, so it anchors to the viewport regardless of where it sits in the DOM. Left and right drawers span the full viewport height and take their width from size (capped at 100vw); top and bottom drawers span the full width and take their height from their content (capped at 100vh).

In every orientation the body fills the space between the header and footer and scrolls when its content overflows, while the header and footer keep their natural height.

Good to know:side defaults to right, the most common placement for sheets. The panel renders flush against its edge with no border radius and an elevation shadow (@box-shadow.lg).

Sizes

Three size variants from sm to lg control the width of left/right drawers through side × size compound variants. (top/bottom drawers are sized by their content, so size does not change their height.) The same size prop also controls the padding and gap of the header, body, and footer sections.

Size Reference

SizeWidth (left/right)Header/Footer Padding (V / H)Body Padding (V / H)Gap
sm16rem@0.5 / @0.75@0.5 / @0.75@0.375@0.5
md20rem@0.75 / @1@0.75 / @1@0.5@0.75
lg28rem@1 / @1.25@1 / @1.25@0.75@1
Good to know: The width column applies only to left/right drawers; top/bottom drawers ignore it and size their height to their content. Pass the size prop to each sub-recipe individually — the container sets the width, while the header, body, and footer manage their own padding and gap.

Anatomy

The Drawer recipe is composed of five independent recipes that work together to form a slide-out panel:

PartRecipeRole
OverlayuseDrawerOverlayRecipe()Full-screen backdrop behind the panel
ContaineruseDrawerRecipe()Edge-anchored, fixed-position panel with shadow and a single border edge
HeaderuseDrawerHeaderRecipe()Top section with a bottom separator border
BodyuseDrawerBodyRecipe()Main content area with vertical flex layout
FooteruseDrawerFooterRecipe()Bottom section with a top separator border and right-aligned actions

Each part is a standalone recipe with its own set of variants. The color and variant props should be passed consistently to the container, header, and footer so that separator border colors match the drawer's visual style. The body recipe only requires a size prop for padding. The overlay recipe has no variants — it provides a fixed-position backdrop with a dark semi-transparent background.

<!-- All five parts working together -->
<div class="drawerOverlay()">
    <div class="drawer({ side: 'right' })" role="dialog" aria-modal="true">
        <div class="drawerHeader(...)">Header content</div>
        <div class="drawerBody(...)">Body content</div>
        <div class="drawerFooter(...)">Footer content</div>
    </div>
</div>
Pro tip: The Drawer shares its header, body, footer, and overlay recipes with the Modal — they are built from the same internal builders. You don't have to use all five parts. A drawer with only a body is valid for simple content.

Accessibility

  • Use role="dialog" and aria-modal="true". The drawer container should have role="dialog" and aria-modal="true" to announce it as a modal dialog to assistive technologies.
<!-- Correct: dialog role with modal semantics -->
<div class="drawerOverlay()">
    <div class="drawer({ side: 'right' })" role="dialog" aria-modal="true" aria-labelledby="drawer-title">
        <div class="drawerHeader(...)">
            <h2 id="drawer-title">Filters</h2>
        </div>
        <div class="drawerBody(...)">
            <p id="drawer-desc">Refine your results.</p>
        </div>
        <div class="drawerFooter(...)">
            <button>Reset</button>
            <button>Apply</button>
        </div>
    </div>
</div>
  • Label the drawer. Use aria-labelledby pointing to the drawer title element so screen readers announce the panel's purpose when it opens.
  • Describe the drawer (optional). Use aria-describedby pointing to the drawer description for additional context.
  • Trap focus inside the drawer. When the drawer is open, focus should cycle between focusable elements inside the panel. The recipe handles visual styling only — implement focus trapping in your component logic.
  • Close on Escape. Add a keydown listener for the Escape key to close the drawer (WCAG 2.1.1).
  • Return focus on close. When the drawer closes, return focus to the element that triggered it.
  • Animate the open/close yourself. The recipe is static — it describes the panel's anchored appearance only. Add your own transform/transition (for example, translating the panel off its edge when closed) to slide it in and out, and respect prefers-reduced-motion.

Customization

Overriding Defaults

Each drawer composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:

src/components/drawer.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import {
    useDrawerRecipe,
    useDrawerFooterRecipe,
    useDrawerOverlayRecipe,
} from '@styleframe/theme';

const s = styleframe();

const drawer = useDrawerRecipe(s, {
    base: {
        boxShadow: '@box-shadow.xl',
    },
    defaultVariants: {
        color: 'neutral',
        variant: 'subtle',
        size: 'lg',
        side: 'left',
    },
});

const drawerFooter = useDrawerFooterRecipe(s, {
    defaultVariants: {
        color: 'neutral',
        variant: 'subtle',
        size: 'lg',
    },
});

const drawerOverlay = useDrawerOverlayRecipe(s, {
    base: {
        background: 'rgba(0, 0, 0, 0.5)',
    },
});

export default s;

Filtering Variants

If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:

src/components/drawer.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useDrawerRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate neutral drawers that anchor left or right
const drawer = useDrawerRecipe(s, {
    filter: {
        color: ['neutral'],
        side: ['left', 'right'],
    },
});

export default s;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent. Filtering side to ['left', 'right'] prunes the top/bottom dimension compounds automatically.

API Reference

useDrawerRecipe(s, options?)

Creates the drawer container recipe: a fixed-position, edge-anchored panel with an elevation shadow and a single border edge.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the drawer container
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, soft, subtlesolid
sizesm, md, lgmd
sidetop, right, bottom, leftright

useDrawerHeaderRecipe(s, options?)

Creates the drawer header recipe with a bottom separator border. Accepts the same parameters and the color/variant/size axes as useDrawerRecipe. Includes a setup function that hides the top border when the header is the first child and the bottom border when it is the last child.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, soft, subtlesolid
sizesm, md, lgmd

useDrawerBodyRecipe(s, options?)

Creates the drawer body recipe for the main content area. Accepts the same parameters as useDrawerRecipe. The body recipe has no compound variants — it controls only padding and gap through the size axis.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, soft, subtlesolid
sizesm, md, lgmd

useDrawerFooterRecipe(s, options?)

Creates the drawer footer recipe with a top separator border and right-aligned content (justifyContent: "flex-end"). Accepts the same parameters and the color/variant/size axes as useDrawerRecipe. Includes a setup function that hides the top border when the footer is the first child and the bottom border when it is the last child.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, soft, subtlesolid
sizesm, md, lgmd

useDrawerOverlayRecipe(s, options?)

Creates the drawer overlay recipe for the full-screen backdrop. The overlay has no variants — it provides a fixed-position container with a dark semi-transparent background (rgba(0, 0, 0, 0.75)) and a z-index of 1000.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the overlay

Learn more about recipes →

Best Practices

  • Pass color and variant consistently: The container, header, and footer all need the same color and variant values so that separator borders match the drawer's visual style.
  • Pass size to each sub-recipe: The container sets the panel thickness, but each section (header, body, footer) manages its own padding and gap based on the size prop.
  • Choose side to match the content: Use left/right for navigation and filters, bottom for mobile action sheets, and top for notifications or command palettes.
  • Use neutral for general-purpose drawers: The neutral color adapts to light and dark mode automatically, making it the safest default.
  • Implement focus trapping and the slide transition in your component: The recipe handles the anchored appearance only. Add focus trapping and a transform-based transition to slide the panel in and out.
  • Close on backdrop click and Escape: Use @click.self on the overlay to close when clicking the backdrop, and listen for the Escape key.
  • Filter what you don't need: If your component only anchors to one side, pass a filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination (including side) as defaultVariants so component consumers write less code.

FAQ