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

react-floating-dock

v0.4.0

Published

A floating bottom navigation dock for React, with an animated active-tab pill and an overflow sheet for extra destinations. Framework-agnostic — bring your own router.

Readme

react-floating-dock

A floating, fully-rounded bottom navigation bar for React, in the style of WhatsApp's mobile tab bar: an animated pill slides behind the active tab, and an optional "More" button opens the rest of your destinations in a bottom sheet.

Framework-agnostic — it doesn't assume Next.js, React Router, or Tailwind. Bring your own router's Link component and your own colors.

Example

Install

npm install react-floating-dock framer-motion

react, react-dom and framer-motion are peer dependencies — install them if your project doesn't already have them.

Quick start

import { FloatingDock, type DockItem } from "react-floating-dock";
import "react-floating-dock/style.css";
import { Home, Wallet, ArrowLeftRight, User, Settings } from "lucide-react";
import Link from "next/link"; // or your router's Link
import { usePathname } from "next/navigation";

const items: DockItem[] = [
  { key: "home", href: "/", label: "Home", icon: Home },
  { key: "wallet", href: "/wallet", label: "Wallet", icon: Wallet },
  { key: "activity", href: "/activity", label: "Activity", icon: ArrowLeftRight },
];

const moreItems: DockItem[] = [
  { key: "profile", href: "/profile", label: "Profile", icon: User },
  { key: "settings", href: "/settings", label: "Settings", icon: Settings },
];

export function AppNav() {
  const pathname = usePathname();

  return (
    <FloatingDock
      items={items}
      moreItems={moreItems}
      isActive={(href) => pathname === href}
      LinkComponent={Link}
    />
  );
}

That's it — four or five primary tabs, an overflow sheet for the rest, and a theme you can override with CSS custom properties.

See example/ for a complete, runnable demo (pnpm example).

Why a LinkComponent prop instead of just using <a>?

So client-side navigation works with whatever router you're on. Most router Link components accept href, className and children — the same shape this package expects — so passing next/link's Link, React Router's Link, or TanStack Router's Link directly just works. If you don't pass one, it falls back to a plain <a href>.

API

<FloatingDock />

| Prop | Type | Default | Description | | ----------------- | -------------------------------------- | -------------- | ----------------------------------------------------------------------------- | | items | DockItem[] | — | Destinations shown directly in the bar. Four or five is the sweet spot. | | moreItems | DockItem[] | [] | Extra destinations shown in the overflow sheet. Omit to hide the "More" button entirely. | | isActive | (href: string) => boolean | — | Tells the dock which item (if any) is the current page. | | LinkComponent | ComponentType<DockLinkProps> | plain <a> | Your router's Link component. | | moreLabel | string | "More" | Label under the overflow button. | | moreSheetTitle | string | "Menu" | Heading inside the overflow sheet. | | closeLabel | string | "Close" | aria-label for the sheet's close button. | | extraContent | ReactNode | — | Rendered below the overflow grid — a natural slot for a theme toggle, sign-out button, language switcher, etc. | | ariaLabel | string | "Main" | aria-label on the <nav> element. | | children | ReactNode \| ({ close }) => ReactNode | — | Your own design for the overflow sheet (see below). With none, the sheet shows moreItems as the default grid. | | moreActive | boolean | derived | Light up the "More" button. Derived from moreItems unless you set it — do set it when using custom children. | | showMoreSheetTitle | boolean | true | Show the sheet's title row and close button. The title still labels the dialog when hidden. | | className | string | — | Extra class on the <nav>, alongside the built-in one. | | theme | "dark" \| "light" \| "frosted" \| "liquid" | "dark" | Colour preset for the bar and the sheet. Override --rfd-* variables for anything custom. | | hideOnScroll | boolean | true | Slide the dock away when scrolling down, bring it back on scroll up (or keyboard focus). Pass false to keep it fixed. |

type DockItem = DockLinkItem | DockActionItem;

interface DockLinkItem {
  key: string;       // React key, usually the same as href
  href: string;
  label: string;
  icon: ComponentType<{ className?: string }>; // any icon component — lucide-react, heroicons, your own SVGs
  badge?: number | string | boolean; // 5, "99+", or true for a dot. 0/false hide it.
}

// If any moreItem carries a badge, the "More" button itself gets a plain attention dot — so
// something needing attention is never hidden from view just because it's tucked in the overflow.

// No href: renders a <button> instead of a link — e.g. a centre "Create" button.
interface DockActionItem {
  key: string;
  label: string;
  icon: ComponentType<{ className?: string }>;
  onClick: () => void;
  prominent?: boolean; // raised, accent-filled button
  badge?: number | string | boolean;
}

Custom overflow content

The sheet opens directly on top of the dock. By default it shows moreItems as a grid of round icons. To design it yourself, pass children — a node, or a function that receives close so your own rows can dismiss the sheet. The "More" button appears whenever there are moreItems or children.

<FloatingDock items={PRIMARY} isActive={(href) => href === pathname} moreActive={isInMenuSection}>
  {({ close }) => (
    <nav aria-label="More">
      <AccountSwitcher />
      <MenuRow href="/members" onClick={close}>Members</MenuRow>
      <MenuRow href="/settings" onClick={close}>Settings</MenuRow>
    </nav>
  )}
</FloatingDock>

Pair it with showMoreSheetTitle={false} for a header-less sheet (it closes by dragging, tapping outside, Escape, or More), and use --rfd-sheet-gap to leave space between the dock and the sheet. extraContent, when given, renders below your children.

<FloatingDockSpacer />

A fixed dock covers the bottom of your page. Render <FloatingDockSpacer /> as the last child of your scrolling content to reserve exactly the dock's height (including the iOS safe area), or use padding-bottom: var(--rfd-height) yourself.

<DockDrawer />

The bottom sheet FloatingDock uses internally for its overflow menu. Exported separately because it's useful on its own for any mobile-style sheet (filters, a confirmation prompt, etc.).

| Prop | Type | Default | Description | | ------------ | ------------ | --------- | ----------------------------------- | | open | boolean | — | Whether the sheet is visible. | | onClose | () => void | — | Called on backdrop click, Escape, or the close button. | | title | string | — | Heading shown in the sheet's header. | | children | ReactNode | — | Sheet body. | | closeLabel | string | "Close" | aria-label for the close button. | | theme | "dark" \| "light" \| "frosted" \| "liquid" | "dark" | Colour preset. | | showHeader | boolean | true | Show the title row and close button. The title still labels the dialog when hidden. | | bodyClassName | string | — | Extra class on the scrolling body. |

On touch screens the sheet can be dragged down by its handle or header to dismiss it.

Styling

Import react-floating-dock/style.css once, anywhere in your app. It's plain CSS driven entirely by custom properties, so no Tailwind (or any other build-time CSS tooling) is required.

To theme it, redefine the variables wherever makes sense for your app — :root for a global theme, or a wrapper element for something scoped:

:root {
  --rfd-bg: rgba(255, 255, 255, 0.9);        /* bar + sheet background */
  --rfd-bg-solid: #ffffff;                    /* sheet background (no backdrop-filter) */
  --rfd-border: #e2e8f0;
  --rfd-text: #0f172a;
  --rfd-text-muted: #64748b;
  --rfd-accent: #3b82f6;                      /* fills: the raised centre button, the active circle in the sheet */
  --rfd-accent-text: #1d4ed8;                 /* active tab text and icon — keep it at 4.5:1 against --rfd-bg */
  --rfd-accent-bg: rgba(59, 130, 246, 0.12);   /* active tab's pill background */
  --rfd-accent-contrast: #ffffff;              /* text/icon color on top of --rfd-accent */
  --rfd-hover: rgba(0, 0, 0, 0.05);
  --rfd-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
  --rfd-radius: 9999px;                        /* fully rounded by default — set a smaller value for softer corners */
  --rfd-max-width: 32rem;                      /* the bar is centred and never wider than this */
  --rfd-bar-height: 52px;                      /* total height of the bar (tabs are this minus its padding and border) */
  --rfd-offset: 0.75rem;                       /* gap to the screen edges (the safe area is added on top) */
  --rfd-badge-bg: #ef4444;
  --rfd-badge-text: #ffffff;
}

Liquid Glass

theme="liquid" gives the dock and its sheet an iOS-style glass look: the page shows through a clear, blurred bar with a bright rim, an inner glow, and a glass pill on the active tab. The ink switches with the system light/dark setting. It looks best over content with some colour behind it — on a flat background there is nothing to show through. For a clearer or smokier bar, change --rfd-glass (the backdrop-filter) and the tint in --rfd-bg.

All of the dock's classes are prefixed rfd- to avoid collisions with the rest of your app's CSS.

Accessibility

  • The active tab gets aria-current="page".
  • The overflow sheet is a role="dialog" with aria-modal="true", locks body scroll while open, and closes on Escape.
  • Focus moves into the sheet when it opens, Tab/Shift+Tab stay inside it, and focus returns to the button that opened it when it closes.
  • Every theme keeps ink, muted ink and the active tab at 4.5:1 contrast or better. The glass themes (frosted, liquid) stay see-through and get there by squeezing the contrast and shifting the brightness of whatever is behind the bar (via --rfd-glass), with the active pill as a denser chip on top. Calculated for black, white, grey, saturated colours, orange, navy and sky blue behind the bar — not for every possible photo, so check it over your own content. They fall back to an opaque system surface under prefers-reduced-transparency or without backdrop-filter.
  • Keyboard focus shows a 2px ring on tabs, the close button and sheet items in every theme (--rfd-focus to recolour it). Motion — the pill's travel and stretch, the sheet's slide — is turned off under prefers-reduced-motion.
  • Count and text badges are part of the item's accessible name; dots are decorative.
  • Every interactive element is a real <button> or your LinkComponent, so it's reachable and operable by keyboard out of the box.

Development

pnpm install
pnpm test        # vitest
pnpm typecheck
pnpm lint
pnpm build        # outputs dist/ (ESM + CJS + types + style.css)
pnpm example      # runs the example app against the live source, at localhost:5173

License

MIT