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

foldkit-sidenav

v0.1.0

Published

Grouped side navigation for Foldkit: task groups that expand in place, an icon rail with an overlay, and a full mode for phone drawers.

Readme

foldkit-sidenav

Grouped side navigation for Foldkit, as a Submodel.

  • Task groups that expand and collapse in place; a group may also be a single link (a leaf group).
  • Three looks from one markup: a full sidebar, an icon rail that opens the full sidebar as an overlay, and a full mode for use inside a phone drawer.
  • Entries are real <a href> links, so navigation stays with the host's router.
  • aria-expanded on group buttons, aria-current="page" on the current entry, every target at least 44 × 44 px.
  • The layout (full or rail) comes from a matchMedia Subscription, so update stays pure.

No domain types: the host passes groups, labels, links and icons. License: MIT.

Install

pnpm add foldkit-sidenav

Peer dependencies: effect (4.x), foldkit (0.158.x). Import the default look once:

import "foldkit-sidenav/styles.css"

Usage

A complete host, compiled against the published package:

import { Option, Schema } from "effect"
import { Subscription, Update } from "foldkit"
import type { Html, HtmlBuilder } from "foldkit/html"
import { defineMessageUnion } from "foldkit/message"
import { evo } from "foldkit/struct"
import * as Sidenav from "foldkit-sidenav"

// MODEL
const Model = Schema.Struct({ sidenav: Sidenav.Model })
type Model = typeof Model.Type
export const init = (): Model => ({ sidenav: Sidenav.init({ id: "main-nav" }) })

// MESSAGE
const Message = defineMessageUnion({
  GotSidenavMessage: { message: Sidenav.Message },
  ChangedRoute: { groupId: Schema.String }
})
type Message = typeof Message.Type

// UPDATE
const foldSidenav = Update.foldChild({
  update: Sidenav.update,
  read: (model: Model) => Option.some(model.sidenav),
  write: (model: Model, sidenav: Sidenav.Model) => evo(model, { sidenav: () => sidenav }),
  toParentMessage: (message: Sidenav.Message) => Message.GotSidenavMessage({ message })
})

export const update = (model: Model, message: Message): Update.Return<Model, Message> =>
  Message.match<Update.Return<Model, Message>>(message, {
    GotSidenavMessage: ({ message }) => foldSidenav(model, message),
    // On entering a route, open the group that holds it:
    ChangedRoute: ({ groupId }) => ({ model: evo(model, { sidenav: (nav) => Sidenav.reveal(nav, groupId) }) })
  })

// SUBSCRIPTIONS
export const subscriptions = Subscription.lift(Sidenav.subscriptions)<Model, Message>({
  toChildModel: (model) => model.sidenav,
  toParentMessage: (message) => Message.GotSidenavMessage({ message })
})

// VIEW
const icon = (groupId: string, h: HtmlBuilder<Message>): Html => h.span([h.AriaHidden(true), h.DataAttribute("icon", groupId)], [])

export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
  h.submodel({
    slotId: "sidenav",
    model: model.sidenav,
    view: Sidenav.view,
    toParentMessage: (message) => Message.GotSidenavMessage({ message }),
    viewInputs: {
      ariaLabel: "Main",
      mode: "responsive", // "full" inside a drawer
      currentItemId: Option.some("suppliers"),
      groups: [
        {
          id: "stock",
          label: "Stock",
          items: [
            { id: "stock-levels", label: "Levels", href: "/stock" },
            { id: "suppliers", label: "Suppliers", href: "/suppliers", badge: "2", badgeLabel: "2 unpaid invoices" }
          ]
        },
        { id: "reports", label: "Reports", href: "/reports", items: [] } // a leaf group: one link
      ],
      groupIcon: (groupId) => icon(groupId, h),
      header: () => h.div([], ["Acme"]),
      footer: () => h.div([], ["Alice"])
    }
  })

Markup the host owns (groupIcon, header, footer) is passed as top-level slot callbacks, so its handlers dispatch through the host's boundary.

API

| Export | | |---|---| | init({ id, expandedGroupIds?, layout?, fullLayoutMinWidthPx? }) | layout is what to show until the Subscription reports (default "full"); fullLayoutMinWidthPx defaults to 1024. | | update(model, message) | Returns { model, commands? }. The only Commands move focus when the rail overlay opens and closes (FocusNavigation); there are no OutMessages. | | reveal(model, groupId) | Expands a group, leaving the others as they are. Call it on route entry. | | collapse, isExpanded, groupPanelId, groupButtonId | Small helpers. | | FocusNavigation | The focus Command, for hosts' Story tests: resolve it with Message.CompletedFocus(). | | subscriptions | layout (the media query) and documentEscape (only while the overlay is open). | | layoutChanges(minWidthPx) | The layout Stream on its own. | | view | defineView with the ViewInputs below. |

Messages

| Message | Effect | |---|---| | PressedGroup{groupId} | A group button in responsive mode: in the rail it opens the overlay with the group expanded; otherwise it toggles the group. | | ToggledGroup{groupId} | Expands or collapses the group. Sent by full mode. | | OpenedRailOverlay{groupId} / ClosedRailOverlay | Open or close the overlay. Opening moves focus to the group's first entry; Escape and a tap on the backdrop close it and return focus to the rail button that opened it. | | ChoseItem{itemId} | A link was followed; closes the overlay. The browser/router handles the navigation. | | ChangedLayout{layout} | From the Subscription. Widening to full closes the overlay. | | CompletedFocus | A focus move ran (or found no target). |

ViewInputs

| Field | | |---|---| | ariaLabel | Name of the <nav> landmark. | | groups | { id, label, href?, items: { id, label, href, badge?, badgeLabel? }[] }[]. A group with href is a leaf link; its id is compared with currentItemId. badgeLabel is how a screen reader says the badge ("2 unpaid invoices"); it defaults to the badge. | | currentItemId | Option<string>: the entry (or leaf group) of the current page. | | mode | "responsive" or "full". | | groupIcon(groupId) | The icon markup. | | header(), footer() | Optional host markup above and below the groups. |

The host decides where the navigation shows: for example, hide it below 640 px and put a full-mode navigation into a drawer there.

Styling

styles.css gives neutral defaults. Theme it with custom properties on any ancestor:

| Variable | Default | Used for | |---|---|---| | --fk-sidenav-bg | #ffffff | panel background | | --fk-sidenav-fg | #1f2937 | group labels | | --fk-sidenav-muted | #4b5563 | entry labels | | --fk-sidenav-line | #e5e7eb | borders | | --fk-sidenav-hover | #f3f4f6 | hovered row | | --fk-sidenav-current-bg / --fk-sidenav-current-fg | #eef2ff / #1e3a8a | current entry; the rail icon of the current group | | --fk-sidenav-badge-bg / --fk-sidenav-badge-fg | #1f2937 / #ffffff | badges, and the rail's badge dot | | --fk-sidenav-focus | #2563eb | focus ring | | --fk-sidenav-backdrop | rgba(0,0,0,.25) | behind the rail overlay | | --fk-sidenav-overlay-shadow | 0 10px 30px rgba(0,0,0,.18) | the overlay panel | | --fk-sidenav-width / --fk-sidenav-rail-width | 240px / 64px | widths | | --fk-sidenav-radius | 8px | row corners |

Hooks for your own CSS:

  • On .fk-sidenav (the <nav>): data-mode (responsive | full), data-layout (full | rail), data-overlay-open (true | false). Use them to adapt the header and footer to the rail.
  • On group buttons and leaf links: data-current (the group holds the current page) and, on buttons, data-badge (an entry in the group has a badge).
  • Classes: fk-sidenav__panel, __header, __footer, __groups, __group, __group-button, __group-link, __items, __item, __icon, __label, __chevron, __badge, __sr-only, __backdrop.

Accessibility

  • In the rail, labels are visually hidden but stay in the accessibility tree, so every icon button keeps its name; a title shows the name to mouse users.
  • aria-expanded on group buttons; aria-controls names the item list while it exists.
  • The current entry (or leaf group) carries aria-current="page".
  • A badge is drawn aria-hidden and read after a comma from a visually hidden span (fk-sidenav__sr-only), so it is never glued to the label.
  • Opening the rail overlay moves focus into it; Escape or a tap outside returns focus to the rail button. Following an entry leaves focus to the host's router and its new page.