Keyboard shortcuts in Next.js, SSR-safe

Most keyboard-shortcut libraries assume window exists the moment their module loads. Next.js renders that same module on the server first, where it doesn't — here's how bind-keyboard avoids the crash, in both routers.

The actual failure mode

A library that reads window/document at module scope, or inside a constructor called eagerly at render time, throws a ReferenceError: window is not defined the moment Next.js renders that component on the server — which it does for every page, App Router or Pages Router, before any client-side JavaScript runs at all.

App Router

useKeybind only ever touches the DOM inside a useEffect, which the App Router's server components can't run — so the component using it needs the "use client" directive, same as any other hook:

app/editor/Shortcuts.tsx
"use client";

import { useKeybind } from "bind-keyboard/react";

export function EditorShortcuts({ onSave }: { onSave: () => void }) {
  useKeybind("cmdOrCtrl+s", (event) => {
    event.preventDefault();
    onSave();
  });

  return null; // a pure "wire up shortcuts" component, renders nothing
}

Render it from a server component like any other client component — EditorShortcuts itself is the only part that needs the directive.

Pages Router

Pages Router components already run their useEffects client-side only, no directive needed — the same hook call works directly inside a page or a layout-level component:

pages/_app.tsx
import type { AppProps } from "next/app";
import { useKeybind } from "bind-keyboard/react";
import { useRouter } from "next/router";

export default function App({ Component, pageProps }: AppProps) {
  const router = useRouter();
  useKeybind("cmdOrCtrl+k", () => router.push("/search"));

  return <Component {...pageProps} />;
}

If you're not using the React hook

Constructing a plain new BindKeyboard() instance is also safe during SSR on its own terms — with the default autostart: true, the constructor checks whether its target can actually listen (addEventListener exists) before attaching anything, and warns instead of throwing when it can't. Still, prefer constructing it inside a useEffect (or, better, just use useKeybind — see the React hook recipe) rather than at module scope, so it's never even attempted server-side in the first place.

Related

React keyboard shortcuts: the useKeybind hook · Building a Cmd/Ctrl+K command palette