npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@carissa-io/components-drawer

v0.1.3

Published

Carissa UI: side panel built on the native dialog element, as a React component with SCSS variables

Readme

@carissa-io/components-drawer

A panel that slides in from an edge of the screen: Drawer, built on the native <dialog> element. It uses showModal(), so the browser provides the top layer, the focus trap, the inert background, Escape and the focus return; there is no portal or focus-trap package. A React component, SCSS-themed, that builds on @carissa-io/components-typography for the title and the description. It has the same contract as Modal; on small screens it is where an app renders its sidebar or navigation.

npm i @carissa-io/components-drawer

Use

import { useState } from 'react'
import { Drawer } from '@carissa-io/components-drawer'

function Filters() {
    const [open, setOpen] = useState(false)
    return (
        <>
            <button type="button" onClick={() => setOpen(true)}>{t('filters')}</button>
            <Drawer
                open={open}
                on_close={() => setOpen(false)}
                side="end"
                title={t('filters')}
                description={t('filters_description')}
                close_label={t('close')}
                footer={<button type="button" onClick={apply}>{t('apply')}</button>}
            >
                {t('filters_body')}
            </Drawer>
        </>
    )
}

The drawer never closes itself: it calls on_close(reason) and the caller sets open to false.

| Prop | | |---|---| | open | shown while true (the dialog stays in the DOM; the browser hides it when closed) | | on_close | on_close(reason), where reason is escape, backdrop (a click on the dimmed area) or close_button | | side | end (default), start, top or bottom; start and end follow the writing direction; anything else falls back to end | | dismissible | default true; when false, Escape and the backdrop click do nothing and the close button is not rendered | | title | optional; rendered with the typography Heading (visual size 4) and used as the dialog name (aria-labelledby) | | title_level | the document level of the title, default 2 | | description | optional; rendered with the typography Text and used as the dialog description (aria-describedby) | | close_label | the accessible name of the close button; default the code close, the app passes the translated text | | size | sm, md (default) or lg: the width of a start/end drawer, the height of a top/bottom one; anything else falls back to md | | footer | a node for the actions, placed after the body | | className, id, ... | className is appended; unknown props go to the dialog element (for example aria-label when there is no title) |

Children are the body. The ids of the title and the description are generated with useId. While any drawer is open the class c-drawer-lock is on <html> (reference counted, removed on close and on unmount) and the package styles it with overflow: hidden. The drawer carries its own small scroll lock instead of depending on the modal package. Browsers without showModal (and jsdom) get the open attribute instead.

Style

@use '@carissa-io/components-drawer/scss' with (
    $c-drawer-radius: 0.5rem,
    $c-drawer-backdrop-color: rgb(0 0 0 / 0.7)
);

Classes: c-drawer with --start, --end, --top, --bottom and --sm, --md, --lg; c-drawer__header, c-drawer__heading, c-drawer__title, c-drawer__description, c-drawer__close, c-drawer__body and c-drawer__footer; c-drawer--overflowing on the root while the body scrolls; c-drawer-lock on <html>. The native ::backdrop is styled through $c-drawer-backdrop-color. Every variable is !default (see scss/_variables.scss); the defaults are neutral, so the component works without a theme. The slide transition is disabled with prefers-reduced-motion.

| Variable | Default | |---|---| | $c-drawer-background, $c-drawer-color | #fff, inherit | | $c-drawer-border-width, $c-drawer-border-color | 1px, rgb(0 0 0 / 0.15) | | $c-drawer-radius, $c-drawer-shadow | 0 (the corners facing the page), 0 0 3rem rgb(0 0 0 / 0.25) | | $c-drawer-padding-x, $c-drawer-padding-y, $c-drawer-gap | 1.5rem, 1.25rem, 1rem | | $c-drawer-header-padding-bottom, $c-drawer-footer-padding-top | 0.5rem, 0.5rem | | $c-drawer-header-divider-width, $c-drawer-header-divider-color | 0, currentColor | | $c-drawer-footer-divider-width, $c-drawer-footer-divider-color | 0, currentColor | | $c-drawer-size-sm, $c-drawer-size-md, $c-drawer-size-lg | 20rem, 28rem, 40rem | | $c-drawer-close-size, $c-drawer-close-color, $c-drawer-close-color-hover | 1.25rem, inherit, inherit | | $c-drawer-footer-justify | flex-end | | $c-drawer-backdrop-color | rgb(0 0 0 / 0.5) | | $c-drawer-transition-duration, $c-drawer-transition-timing | 0.2s, ease-out |

The title and the description take their look from the typography package, whose variables are forwarded by this package's scss.

Scroll edges: the gap and the dividers

The body is the scroll area, and padding inside a scroller scrolls away: with all the vertical padding on the body, the content would sit flush against the header and the footer as soon as it moves, with nothing to show where the scroll area ends. So the vertical spacing is split between the fixed parts and the body:

  • $c-drawer-header-padding-bottom (default 0.5rem) is the gap between the header content and the header's bottom edge, and $c-drawer-footer-padding-top (default 0.5rem) the gap between the footer's top edge and the footer content. They never move while the body scrolls.
  • The body keeps the rest of $c-drawer-padding-y (max(0px, padding-y - gap)), so unscrolled the layout is the same as a single padding-y between the header and the body and between the body and the footer. A gap larger than padding-y leaves the body with no vertical padding.
  • When there is no header (no title, no description and dismissible={false}) or no footer, the body is the first or the last child of the dialog and keeps the whole padding-y on that side (.c-drawer__body:first-child and :last-child).

A divider is a border under the header and over the footer. The widths default to 0, so the component draws no line; a theme turns them on. The footer line exists only when there is a footer, because it is the footer's own border.

The dividers show only while the body overflows. The component measures the scrolling body (scrollHeight against clientHeight, ignoring a 1px difference from sub-pixel rounding) and puts the modifier c-drawer--overflowing on the root (<dialog>) while the content is taller than the body's box. The base rules keep a 0-width border, and the widths come into play only under the modifier:

.c-drawer__header { border-bottom: 0 solid $c-drawer-header-divider-color; }
.c-drawer--overflowing .c-drawer__header { border-bottom-width: $c-drawer-header-divider-width; }
// and the same for the footer (border-top, $c-drawer-footer-divider-width)

So a short drawer has no line (there is nothing to separate), and a scrolling one shows both. The modifier is re-measured when the drawer opens (a closed <dialog> is display: none and cannot be measured), when the body is resized (ResizeObserver, which also covers a window resize) and when its content changes (MutationObserver on the body); it is removed when the drawer closes. The observers are disconnected on close and on unmount. Without ResizeObserver or MutationObserver (or on the server) the component does not throw: it measures once when it opens and the modifier follows that. The padding model and the divider variables are the same as before; only when the line is drawn changed. A consumer className is kept next to the modifier.

@use '@carissa-io/components-drawer/scss' with (
    $c-drawer-header-divider-width: 1px,
    $c-drawer-header-divider-color: rgb(0 0 0 / 0.15),
    $c-drawer-footer-divider-width: 1px,
    $c-drawer-footer-divider-color: rgb(0 0 0 / 0.15)
);

The lines are the clip edge of the scroll area: while the body scrolls, its content is cut right at each line, with the header or footer gap on the outer side of the line. Unscrolled, the body content starts padding-y minus the gap below the header line (and ends the same distance above the footer line). Change the two gap variables to move the line closer to or farther from the header and footer content.

Development

npm test        # vitest + Testing Library (jsdom: showModal and close are mocked)
npm run lint
npm run build   # esbuild -> dist/

Published only by GitHub Actions on push to main; see AGENTS.md.