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

@live-tabs/react

v0.1.0

Published

Browser-style workspace tabs with kept-alive pages for TanStack Router. Switch pages without losing scroll, form input, or query state.

Readme

@live-tabs/react

Browser-style workspace tabs with kept-alive pages, for TanStack Router.

Open pages in tabs and switch between them without losing a thing — scroll position, form input, filters, sub-section pickers, and in-flight queries all survive because each page's React subtree stays mounted off-screen. Built for admin consoles and dashboards where users hop between records all day.

npm i @live-tabs/react

Peer deps: react, react-dom, @tanstack/react-router (>=1.150), zustand. Zero runtime dependencies of its own.


Why

A normal router unmounts the old page on every navigation. Go from a customer you were editing to another tab and back, and your form is blank, your scroll is at the top, your filters are reset. @live-tabs/react keeps each page alive — the way native app tabs work — and gives you the tab strip to drive it.

  • Kept-alive pagesuseState, refs, scroll, and TanStack Query subscriptions resume exactly where you left them.
  • Auto-managed tabs — every distinct URL opens a tab; clicking it restores the exact URL you last saw (including ?search state).
  • Headless or batteries-included — drop in <WorkspaceTabBar>, or compose the unstyled <TabStrip>/<Tab>/<TabClose> primitives yourself.
  • Live titlesuseSetTabTitle("Acme Inc") upgrades a tab from its registry fallback once data lands.

Quick start (5 steps)

1. Wrap your app

import { WorkspaceProvider, createTabRegistry } from "@live-tabs/react"

const registry = createTabRegistry({
  staticRoutes: {
    "/dashboard": { title: "Dashboard", iconKey: "home" },
    "/dashboard/customers": { title: "Customers", iconKey: "customer" },
  },
  dynamicPatterns: [
    { match: /^\/dashboard\/customers\/[^/]+$/, iconKey: "customer", noun: "Customer" },
  ],
})

;<WorkspaceProvider
  options={{ pinnedPath: "/dashboard", pinnedTab: { title: "Dashboard", iconKey: "home" }, maxTabs: 12 }}
  registry={registry}
>
  <RouterProvider router={router} />
</WorkspaceProvider>

WorkspaceProvider also mounts KeepAliveProvider for you (pass keepAlive={false} to mount it yourself).

2. Swap <Outlet/> for <KeepAliveOutlet/> in your layout segment

// routes/dashboard/route.tsx
import { KeepAliveOutlet } from "@live-tabs/react"

function DashboardLayout() {
  return (
    <div className="layout">
      <WorkspaceTabBar /* see step 4 */ />
      <KeepAliveOutlet />
    </div>
  )
}

3. Opt each leaf route into keep-alive

export const Route = createFileRoute("/dashboard/customers/$id")({
  staticData: { keepAlive: true }, // ← the only per-route change
  component: CustomerDetail,
})

@live-tabs/react augments TanStack's StaticDataRouteOption type, so keepAlive is fully typed. (Set it on leaf pages, not on layout route.tsx segments.)

4. Render the tab bar

import { WorkspaceTabBar } from "@live-tabs/react"
import "@live-tabs/react/styles.css" // optional default theme
import { IconHome, IconUser } from "@tabler/icons-react"

const ICONS: Record<string, React.ReactNode> = {
  home: <IconHome size={14} />,
  customer: <IconUser size={14} />,
}

;<WorkspaceTabBar
  renderIcon={(key) => (key ? ICONS[key] : null)}
  leftSlot={<SidebarToggle />}
  rightSlot={<><GlobalSearch /><Notifications /></>}
/>

5. Give detail pages a real title

import { useSetTabTitle } from "@live-tabs/react"

function CustomerDetail() {
  const { data } = useQuery(customerQuery)
  useSetTabTitle(data?.name) // tab reads "Acme Inc" instead of "Customer #42"
  // ...
}

That's it. Pages now persist across tab switches, and the bar manages itself.


Headless usage

Want your own bar, or just the tab UI without the router glue? Use @live-tabs/core directly (router-agnostic — no TanStack/keep-alive code):

import { createWorkspaceTabsStore, TabStrip, Tab, TabClose } from "@live-tabs/core"

const useTabs = createWorkspaceTabsStore({ pinnedPath: "/", maxTabs: 10 })

function MyBar() {
  const tabs = useTabs((s) => s.tabs)
  const closeTab = useTabs((s) => s.closeTab)
  const active = useActivePathSomehow()

  return (
    <TabStrip activeKey={active}>
      {tabs.map((t) => (
        <Tab
          key={t.pathname}
          active={t.pathname === active}
          pinned={t.pinned}
          render={<a href={t.href} />}
          onMiddleClick={() => closeTab(t.pathname)}
        >
          <span>{t.title}</span>
          {!t.pinned && <TabClose onClose={() => closeTab(t.pathname)}>✕</TabClose>}
        </Tab>
      ))}
    </TabStrip>
  )
}

<Tab> emits role="tab", aria-selected, and data-active/data-pinned — style off those. render lets it become any element (a router <Link>, an <a>, etc.) with the tab semantics merged in.


API

Provider & store

| Export | What | | --- | --- | | WorkspaceProvider | Creates the store, exposes the registry, mounts keep-alive. | | useWorkspaceTabs() | { tabs, openTab, renameTab, closeTab, closeAllTabs }. | | useWorkspaceTabsStore() | The raw Zustand store (selector or .getState()). | | createWorkspaceTabsStore(options) | Standalone store factory (no provider). | | createTabRegistry(options) | Path → { title, iconKey } resolver. |

Components

| Export | What | | --- | --- | | WorkspaceTabBar | Batteries-included bar (auto-open, restore, close, scroll). | | TabStrip | Headless scroll rail (edge-fade, wheel-to-horizontal, auto-scroll). | | Tab, TabClose | Headless, unstyled tab + close primitives. |

Keep-alive

| Export | What | | --- | --- | | KeepAliveProvider, KeepAliveOutlet | The cache + the <Outlet/> replacement. | | useKeepAlive() | { aliveRoutes, destroy, destroyAll } — free cached subtrees. | | useActiveEffect(fn, deps) | Like useEffect, but (re)runs on show / cleans up on hide. | | useSetTabTitle(title) | Live-rename the current tab. | | useKeptPathname() | The frozen pathname inside a kept subtree. |


How keep-alive works (and its one trade-off)

Each kept route is rendered once and toggled with display (contents when visible, none when hidden) — the only approach that reliably preserves state in stable React 19. Hidden subtrees stay mounted but don't display.

Trade-off: a hidden subtree's router hooks (useParams, useLocation) see the live URL when it re-renders. @live-tabs/react wraps each kept subtree in a frozen location so its own state isn't disturbed; use useKeptPathname() (not useLocation) when a kept page needs "which page am I?". useQuery dedupes, so query-driven UI is unaffected.

State is intentionally not persisted across reloads — the kept React subtrees can't survive a reload, so persisting tab metadata alone would over-promise. A reload starts fresh with the pinned tab + the opened URL.


Credits

The keep-alive engine is adapted from tanstack-router-keepalive (MIT). The off-screen rendering strategy and the direct-component render path differ from upstream to work with @tanstack/react-router >= ~1.150 and stable React 19.

License

MIT