diff --git a/.changeset/dirty-berries-mate.md b/.changeset/dirty-berries-mate.md
new file mode 100644
index 00000000..9d857060
--- /dev/null
+++ b/.changeset/dirty-berries-mate.md
@@ -0,0 +1,7 @@
+---
+'@tanstack/react-hotkeys-devtools': patch
+'@tanstack/preact-hotkeys-devtools': patch
+'@tanstack/solid-hotkeys-devtools': patch
+---
+
+Allow standalone HotkeysDevtoolsPanel components without props, supplying dark theme and open defaults while preserving props from the Devtools dock.
diff --git a/docs/devtools.md b/docs/devtools.md
index 2a3aabb3..ece7427e 100644
--- a/docs/devtools.md
+++ b/docs/devtools.md
@@ -97,6 +97,20 @@ import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools'
For React, Preact, and Solid, the Hotkeys panel appears alongside any other TanStack devtools plugins you have installed.
+### Standalone panel
+
+React, Preact, and Solid also export `HotkeysDevtoolsPanel` for rendering without the TanStack Devtools dock:
+
+```tsx
+import { HotkeysDevtoolsPanel } from '@tanstack/react-hotkeys-devtools'
+
+function DebugPanel() {
+ return
+}
+```
+
+Use the corresponding `@tanstack/preact-hotkeys-devtools` or `@tanstack/solid-hotkeys-devtools` import for those frameworks. The panel and no-op component also accept an omitted props argument during rendering. Both props are optional: `theme` defaults to `'dark'` and `devtoolsOpen` defaults to `true`. Pass `theme="light"` to select the light theme. When using `hotkeysDevtoolsPlugin()`, the dock supplies these props and its values take precedence over the standalone defaults.
+
## Production builds
In production builds, the framework devtools adapters return no-op implementations, so they don't affect your bundle's behavior.
diff --git a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx
index 39708a03..935babad 100644
--- a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx
+++ b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx
@@ -1,15 +1,27 @@
+import { h } from 'preact'
import { createPreactPanel } from '@tanstack/devtools-utils/preact'
import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact'
import type { JSX } from 'preact'
-export interface HotkeysDevtoolsPreactInit extends DevtoolsPanelProps {}
+export interface HotkeysDevtoolsPreactInit extends Partial {}
type HotkeysDevtoolsPanelComponent = (
- props: HotkeysDevtoolsPreactInit,
+ props?: HotkeysDevtoolsPreactInit,
) => JSX.Element
const panels = createPreactPanel(HotkeysDevtoolsCore)
-export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0]
-export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1]
+function withDefaults(
+ Panel: (typeof panels)[number],
+): HotkeysDevtoolsPanelComponent {
+ return (props = {}) =>
+ h(Panel, {
+ ...props,
+ theme: props.theme ?? 'dark',
+ devtoolsOpen: props.devtoolsOpen ?? true,
+ })
+}
+
+export const HotkeysDevtoolsPanel = withDefaults(panels[0])
+export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1])
diff --git a/packages/preact-hotkeys-devtools/tests/panel.test.ts b/packages/preact-hotkeys-devtools/tests/panel.test.ts
new file mode 100644
index 00000000..977e5cbd
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/tests/panel.test.ts
@@ -0,0 +1,67 @@
+import { h, render } from 'preact'
+import { act } from 'preact/test-utils'
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import {
+ HotkeysDevtoolsPanel,
+ HotkeysDevtoolsPanelNoOp,
+} from '../src/PreactHotkeysDevtools'
+import { hotkeysDevtoolsPlugin } from '../src/plugin'
+
+const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() }))
+vi.mock('@tanstack/hotkeys-devtools', () => ({
+ HotkeysDevtoolsCore: class {
+ mount = core.mount
+ unmount = core.unmount
+ },
+}))
+
+const emptyProps: NonNullable[0]> = {}
+let host: HTMLDivElement
+beforeEach(() => {
+ vi.clearAllMocks()
+ host = document.createElement('div')
+ document.body.append(host)
+})
+afterEach(() => {
+ act(() => render(null, host))
+ host.remove()
+})
+
+it('mounts a standalone panel with default props', () => {
+ act(() => render(h(HotkeysDevtoolsPanel, emptyProps), host))
+ expect(core.mount).toHaveBeenCalledOnce()
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'dark',
+ devtoolsOpen: true,
+ })
+ act(() => render(null, host))
+ expect(core.unmount).toHaveBeenCalledOnce()
+})
+
+it('preserves props supplied by the devtools dock', () => {
+ const plugin = hotkeysDevtoolsPlugin()
+ act(() =>
+ render(plugin.render(host, { theme: 'light', devtoolsOpen: false }), host),
+ )
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'light',
+ devtoolsOpen: false,
+ })
+})
+
+it('keeps the no-op panel inert with empty props', () => {
+ act(() => render(h(HotkeysDevtoolsPanelNoOp, emptyProps), host))
+ expect(core.mount).not.toHaveBeenCalled()
+})
+
+it('accepts omitted props for real and no-op panels during render', () => {
+ const Standalone = () => HotkeysDevtoolsPanel()
+ const NoOp = () => HotkeysDevtoolsPanelNoOp()
+ act(() => render(h(Standalone, {}), host))
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'dark',
+ devtoolsOpen: true,
+ })
+ act(() => render(h(NoOp, {}), host))
+ expect(core.mount).toHaveBeenCalledOnce()
+})
diff --git a/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx b/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx
index 33c7fb53..0f87279f 100644
--- a/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx
+++ b/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx
@@ -1,15 +1,27 @@
+import { createElement } from 'react'
import { createReactPanel } from '@tanstack/devtools-utils/react'
import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react'
import type { JSX } from 'react'
-export interface HotkeysDevtoolsReactInit extends DevtoolsPanelProps {}
+export interface HotkeysDevtoolsReactInit extends Partial {}
type HotkeysDevtoolsPanelComponent = (
- props: HotkeysDevtoolsReactInit,
+ props?: HotkeysDevtoolsReactInit,
) => JSX.Element
const panels = createReactPanel(HotkeysDevtoolsCore)
-export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0]
-export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1]
+function withDefaults(
+ Panel: (typeof panels)[number],
+): HotkeysDevtoolsPanelComponent {
+ return (props = {}) =>
+ createElement(Panel, {
+ ...props,
+ theme: props.theme ?? 'dark',
+ devtoolsOpen: props.devtoolsOpen ?? true,
+ })
+}
+
+export const HotkeysDevtoolsPanel = withDefaults(panels[0])
+export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1])
diff --git a/packages/react-hotkeys-devtools/tests/panel.test.ts b/packages/react-hotkeys-devtools/tests/panel.test.ts
new file mode 100644
index 00000000..ae3176a9
--- /dev/null
+++ b/packages/react-hotkeys-devtools/tests/panel.test.ts
@@ -0,0 +1,75 @@
+import { act, createElement } from 'react'
+import { createRoot } from 'react-dom/client'
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import {
+ HotkeysDevtoolsPanel,
+ HotkeysDevtoolsPanelNoOp,
+} from '../src/ReactHotkeysDevtools'
+import { hotkeysDevtoolsPlugin } from '../src/plugin'
+import type { Root } from 'react-dom/client'
+
+const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() }))
+vi.mock('@tanstack/hotkeys-devtools', () => ({
+ HotkeysDevtoolsCore: class {
+ mount = core.mount
+ unmount = core.unmount
+ },
+}))
+
+const emptyProps = {} satisfies NonNullable<
+ Parameters[0]
+>
+let host: HTMLDivElement
+let root: Root
+beforeEach(() => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
+ vi.clearAllMocks()
+ host = document.createElement('div')
+ document.body.append(host)
+ root = createRoot(host)
+})
+afterEach(async () => {
+ await act(() => root.unmount())
+ host.remove()
+ vi.unstubAllGlobals()
+})
+
+it('mounts a standalone panel with default props', async () => {
+ await act(() => root.render(createElement(HotkeysDevtoolsPanel, emptyProps)))
+ expect(core.mount).toHaveBeenCalledOnce()
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'dark',
+ devtoolsOpen: true,
+ })
+ await act(() => root.render(null))
+})
+
+it('preserves props supplied by the devtools dock', async () => {
+ const plugin = hotkeysDevtoolsPlugin()
+ await act(() =>
+ root.render(plugin.render(host, { theme: 'light', devtoolsOpen: false })),
+ )
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'light',
+ devtoolsOpen: false,
+ })
+})
+
+it('keeps the no-op panel inert with empty props', async () => {
+ await act(() =>
+ root.render(createElement(HotkeysDevtoolsPanelNoOp, emptyProps)),
+ )
+ expect(core.mount).not.toHaveBeenCalled()
+})
+
+it('accepts omitted props for real and no-op panels during render', async () => {
+ const Standalone = () => HotkeysDevtoolsPanel()
+ const NoOp = () => HotkeysDevtoolsPanelNoOp()
+ await act(() => root.render(createElement(Standalone)))
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'dark',
+ devtoolsOpen: true,
+ })
+ await act(() => root.render(createElement(NoOp)))
+ expect(core.mount).toHaveBeenCalledOnce()
+})
diff --git a/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx b/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx
index 60d152e1..4c6972fb 100644
--- a/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx
+++ b/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx
@@ -1,15 +1,26 @@
+import { createComponent, mergeProps } from 'solid-js'
import { createSolidPanel } from '@tanstack/devtools-utils/solid'
import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid'
import type { JSX } from 'solid-js'
-export interface HotkeysDevtoolsSolidInit extends DevtoolsPanelProps {}
+export interface HotkeysDevtoolsSolidInit extends Partial {}
type HotkeysDevtoolsPanelComponent = (
- props: HotkeysDevtoolsSolidInit,
+ props?: HotkeysDevtoolsSolidInit,
) => JSX.Element
const panels = createSolidPanel(HotkeysDevtoolsCore)
-export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0]
-export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1]
+function withDefaults(
+ Panel: (typeof panels)[number],
+): HotkeysDevtoolsPanelComponent {
+ return (props = {}) =>
+ createComponent(
+ Panel,
+ mergeProps({ theme: 'dark' as const, devtoolsOpen: true }, props),
+ )
+}
+
+export const HotkeysDevtoolsPanel = withDefaults(panels[0])
+export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1])
diff --git a/packages/solid-hotkeys-devtools/tests/panel.test.ts b/packages/solid-hotkeys-devtools/tests/panel.test.ts
new file mode 100644
index 00000000..94713487
--- /dev/null
+++ b/packages/solid-hotkeys-devtools/tests/panel.test.ts
@@ -0,0 +1,82 @@
+import { createComponent, createSignal } from 'solid-js'
+import { render } from 'solid-js/web'
+import { afterEach, beforeEach, expect, it, vi } from 'vitest'
+import {
+ HotkeysDevtoolsPanel,
+ HotkeysDevtoolsPanelNoOp,
+} from '../src/SolidHotkeysDevtools'
+import { hotkeysDevtoolsPlugin } from '../src/plugin'
+
+const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() }))
+vi.mock('@tanstack/hotkeys-devtools', () => ({
+ HotkeysDevtoolsCore: class {
+ mount = core.mount
+ unmount = core.unmount
+ },
+}))
+
+const emptyProps = {} satisfies NonNullable<
+ Parameters[0]
+>
+let host: HTMLDivElement
+let dispose: () => void
+beforeEach(() => {
+ vi.clearAllMocks()
+ host = document.createElement('div')
+ document.body.append(host)
+ dispose = () => {}
+})
+afterEach(() => {
+ dispose()
+ host.remove()
+})
+
+it('mounts a standalone panel with default props', () => {
+ dispose = render(
+ () => createComponent(HotkeysDevtoolsPanel, emptyProps),
+ host,
+ )
+ expect(core.mount).toHaveBeenCalledOnce()
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'dark',
+ devtoolsOpen: true,
+ })
+})
+
+it('preserves reactive props supplied by the devtools dock', () => {
+ const [theme, setTheme] = createSignal<'light' | 'dark'>('light')
+ const plugin = hotkeysDevtoolsPlugin()
+ dispose = render(
+ () =>
+ plugin.render(host, {
+ get theme() {
+ return theme()
+ },
+ devtoolsOpen: false,
+ }),
+ host,
+ )
+ const props = core.mount.mock.calls[0]![1]
+ expect(props).toMatchObject({ theme: 'light', devtoolsOpen: false })
+ setTheme('dark')
+ expect(props.theme).toBe('dark')
+})
+
+it('keeps the no-op panel inert with empty props', () => {
+ dispose = render(
+ () => createComponent(HotkeysDevtoolsPanelNoOp, emptyProps),
+ host,
+ )
+ expect(core.mount).not.toHaveBeenCalled()
+})
+
+it('accepts omitted props for real and no-op panels during render', () => {
+ dispose = render(() => HotkeysDevtoolsPanel(), host)
+ expect(core.mount.mock.calls[0]![1]).toMatchObject({
+ theme: 'dark',
+ devtoolsOpen: true,
+ })
+ dispose()
+ dispose = render(() => HotkeysDevtoolsPanelNoOp(), host)
+ expect(core.mount).toHaveBeenCalledOnce()
+})