---
title: DevTools
description: Load the c15t DevTools panel only in Next.js development builds to
  inspect consent state, scripts, policy and events inside ConsentRoot.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
`DevTools` is a floating panel for the c15t v3 kernel. Use it during development to inspect consent values, scripts, location, the resolved policy, IAB state, events, and consent actions. The framework adapter reads the kernel from the nearest v3 provider. It never discovers a store through a window global.

> ⚠️ **Warning:**
> Load DevTools only in development. A conditional JSX expression stops it from mounting in production, but a static import can still add DevTools to the production bundle. Use a development-only dynamic import as shown below. The component renders nothing into the React tree and mounts its panel in document.body.

## Installation

No extra package is required. The React adapter and its engine are included
with the `c15t` package. Install `@c15t/dev-tools` directly only when you
need the imperative `createDevTools({ kernel })` API.

## Add DevTools to `ConsentRoot` in development

Keep the `ConsentRoot` from your router setup. Add this client component as
one of its children, alongside the banner and dialog. It uses the existing
runtime, including its prefetch result and configured transport.

```tsx title="components/consent-dev-tools.tsx"
'use client';

import dynamic from 'next/dynamic';

const DevTools =
	process.env.NODE_ENV === 'development'
		? dynamic(
				() => import('c15t/next/devtools').then(({ DevTools }) => DevTools),
				{ ssr: false }
			)
		: () => null;

export function ConsentDevTools() {
	return <DevTools />;
}
```

Import `ConsentDevTools` into your root layout or `pages/_app.tsx` and render
`<ConsentDevTools />` inside `ConsentRoot`. Keep its `state` and other props
unchanged. A setup that uses `ConsentProvider`
directly can place the same component inside that provider.

The umbrella import is `c15t/next/devtools`. When installing the dedicated
Next.js package, use `@c15t/nextjs/devtools`.

## Configuration

```tsx
<DevTools
  position="bottom-right"
  defaultOpen={false}
  defaultTab="consents"
  maxEvents={100}
  disabled={false}
/>
```

`position` accepts any corner: `top-left`, `top-right`, `bottom-left`, or
`bottom-right`. Set `defaultOpen` to open the initial panel and `defaultTab`
to choose that panel. `maxEvents` limits captured kernel and script events.
Set `disabled` to prevent mounting without removing the component. The
panel renders inside a shadow root by default, so page CSS does not reach
it. `shadow={false}` mounts it in the light DOM with its stylesheet in
`<head>`.

## Open DevTools from the consent trigger

While `ConsentDialogTriggerToolbar` or the ready-made `ConsentDialogTrigger`
is visible, the trigger shows a DevTools button and DevTools hides its own
floating launcher, so one control occupies the corner. The button sits at the
end of the toolbar farthest from its corner. The panel opens just past the
toolbar, aligned with its outer edge, and follows it when a visitor drags the
toolbar to another corner. `position` applies again once no trigger is
visible.

`ConsentDialogTrigger` becomes a two-button toolbar while DevTools is
mounted. A trigger composed from `ConsentDialogTrigger.Root` keeps its own
markup, and DevTools keeps its floating launcher beside it.

DevTools brings its launcher back whenever no trigger is visible, for example
while `showWhen="after-prompt"` waits for a choice. A production build that
loads DevTools only in development shows no DevTools button in the trigger.

With the imperative engine, call `devTools.dock({ position, inline, block })`
to hide the launcher and anchor the panel `inline` and `block` pixels from the
viewport edges of `position`'s corner. Call `devTools.dock(null)` to restore
the launcher.

## Panels

Accept and reject apply only to categories displayed by the provider, leaving
hidden consent values unchanged. `necessary` always remains enabled. The
React adapter uses the current policy categories by default.

For an imperative integration with a narrower UI, pass
`getConsentCategories: () => ['necessary', 'measurement']` to
`createDevTools`. The getter is read again when an action runs. Headless UI
actions can use `kernel.commands.save('all', { categories: displayed })` or
`kernel.commands.save('none', { categories: displayed })`. These scoped bulk
saves use a `custom` transport action when they cover only part of the policy.
Actions covering the whole policy retain `all` or `necessary`. Without an
explicit scope or policy categories, bulk saves cover all known categories.

|Panel|What it shows|
|--|--|
|**Consents**|Inspect and save categories, accept all, or reject optional categories|
|**Scripts**|Configured scripts, loading status, search, and external page resources|
|**Location**|Resolved country and region, and location overrides|
|**Policy**|Resolved policy and UI configuration|
|**IAB**|Edit vendors, purposes, legitimate interests, and special features; save and copy TC strings|
|**Events**|Timeline of consent changes, kernel events, and script lifecycle events|
|**Actions**|Show the banner, open preferences, hide consent UI, or refresh consent data|

### Script inspection

Set `defaultTab="scripts"` to open script inspection first. It reads all script
loaders attached to the current provider's kernel, including loaders created
before DevTools mounts. Search by script ID, category, URL, or status, then
expand a script to inspect its configuration and latest lifecycle event.

`loading` means an external script was inserted; `loaded` means its browser
load event fired, or an inline script or callback-only integration mounted.
`error` reports a loader error. `blocked` means consent requirements were not
met, and `pending` means an eligible script has not mounted. `present` means
the loader reused an element without a confirmed load result. `retained`
means consent was revoked but `persistAfterConsentRevoked` kept the element.
Retained scripts still receive `onConsentChange` with `hasConsent: false`, so
their integrations can send the vendor's consent-revocation command.
`alwaysLoad` bypasses the loading gate, not consent: callbacks receive the
actual consent state and updates across categories for integrations such as
Google Consent Mode.

Script details distinguish `allowedToLoad` from `consentGranted`. An
`alwaysLoad` integration can be allowed to load while its consent is denied.
Diagnostics expose these as `eligible` and `hasConsent`, respectively.

Lifecycle diagnostics work even when legacy debug forwarding is disabled.
The page scan lists external scripts and iframes present in the
DOM; it does not prove that they loaded successfully or were consent-gated.

For custom inspection tools, import `getScriptDiagnostics(kernel)` and
`subscribeScriptDiagnostics(kernel, listener)` from
`c15t/modules/script-loader`. The subscription reports loader
registration, updates, disposal, and lifecycle events. Read a fresh snapshot
after changes and call the returned unsubscribe function during cleanup.

### IAB editing

Under an IAB policy, the Consents tab is read-only. Edit and save vendors
and purposes in the IAB tab so the derived categories and TC string stay
consistent.

The IAB panel connects to the existing `@c15t/iab` module attached to the
provider's kernel. It does not create a second CMP or replace `__tcfapi`.
Controls become available after initialization, when the current policy uses
IAB and the vendor list is loaded.

Choose Vendors, Purposes, or Special features, then search by name or ID.
Vendors include the provider's custom vendors. Long lists show 20 entries per
page. Legitimate-interest controls appear for declared legitimate interests.
Search only filters the view; Accept all IAB and Reject all IAB apply to the
configured choices, including custom vendors.

Toggles update live IAB state and script gating immediately. Use Save IAB
consent to generate a fresh TC string and run the configured save transport.
The panel displays the last generated string; unsaved edits are not represented
in it. Copy TC string confirms success or reports clipboard failures. Raw IAB
data remains available in an expandable section.

IAB saves normally write the TC string to its standard cookie and localStorage.
For an in-memory playground, set `persistence: false` in the IAB module options
as well as disabling the provider's core persistence. The IAB option prevents
TC-string storage writes; it does not disable the configured save transport
or remove previously stored values.

Save and refresh actions show pending, success, and failure feedback. Controls
are disabled while a request is pending. A failed save does not roll back the
live choices; retry to record them.

For custom inspection tools, `getIABControls(kernel)` and
`subscribeIABControls(kernel, listener)` are exported from `c15t`.
The getter returns undefined before module initialization and after disposal.
A snapshot without an attached IAB module is read-only in DevTools.

## TanStack Devtools

Each embedded panel owns its event history and subscriptions. Unmounting it
destroys the instance; remounting starts a new history. If you need to capture
events while switching plugins, keep the c15t panel mounted. This differs from
the old globally shared DevTools store.

The React v3 DevTools adapter exports a panel component and plugin factory that match TanStack Devtools' plugin API:

```tsx
import * as React from 'react';
import { useRouter } from '@tanstack/react-router';
import { TanStackDevtools } from '@tanstack/react-devtools';
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
import { c15tDevtools } from 'c15t/react/devtools';

export function AppDevtools() {
  const router = useRouter();

  return (
    <TanStackDevtools
      plugins={[
        {
          name: 'TanStack Query',
          render: <ReactQueryDevtoolsPanel />,
        },
        {
          name: 'TanStack Router',
          render: <TanStackRouterDevtoolsPanel router={router} />,
        },
        c15tDevtools(),
      ]}
    />
  );
}
```

In a Next.js app, import `c15tDevtools` from `c15t/next/devtools` instead. Scoped installs use `@c15t/react/devtools` and `@c15t/nextjs/devtools`.

The c15t panel follows the TanStack Devtools light or dark theme, not your app's consent theme. If you write the plugin entry yourself, use a render function and pass the theme to `C15tTanStackDevtoolsPanel`:

```tsx
import { C15tTanStackDevtoolsPanel } from 'c15t/react/devtools';

const c15tPlugin = {
  name: 'Consent',
  render: (_element: HTMLElement, { theme }: { theme: 'light' | 'dark' }) => (
    <C15tTanStackDevtoolsPanel theme={theme} />
  ),
};
```

## Props

|Property|Value|
|:--|:--|
|Type Name|\`ConsentDevToolsProps\`|
|Source Path|\`./packages/react/src/devtools.tsx\`|

\*ExtractedTypeTable: Could not extract "ConsentDevToolsProps" from "./packages/react/src/devtools.tsx" using base path "/vercel/path0/apps/c15t-docs/.leadtype/c15t". Verify the path/name and that the file is included by your tsconfig.\*
