> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.getunleash.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.getunleash.io/_mcp/server.

# Developer Toolbar

> Set up the Unleash Developer Toolbar to override feature flags and context at runtime for local testing and debugging.

The [Unleash Developer Toolbar](https://github.com/Unleash/toolbar) lets you override feature flag values and context properties at runtime without making server changes. It's designed for local development and testing workflows where you need to quickly test different flag configurations or context values.

![The Unleash Developer Toolbar showing flag overrides and context settings](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/unleash.docs.buildwithfern.com/7977f7cd50b83d674a3392d5bd88fdd8cc8e3875744a313248cf89a0c7c3b4d4/assets/toolbar-screenshot.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260808%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260808T100848Z&X-Amz-Expires=604800&X-Amz-Signature=fdf86a2d0742a9654007f56fee346d84cffc0ded46336bcc9492557d84c6a8df&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

## Key features

* **Flag overrides**: Force boolean flags on or off, or select specific variant values
* **Context overrides**: Modify `userId`, `sessionId`, and custom properties to test targeting rules
* **Persistence**: Choose between memory, session, or local storage for your overrides
* **Framework support**: Works with React, Next.js, Vue, Angular, and JavaScript
* **SSR support**: Cookie-based state sync for server-side rendering in Next.js
* **Movable and dismissible**: Drag the floating icon to any window edge, minimize it to the icon, or hide it entirely until the next page refresh
* **Keyboard support**: Open and close the toolbar with a configurable shortcut, operate every control from the keyboard, and dismiss it with `Esc`
* **Screen reader support**: Landmark, tab, and radio group semantics, labeled controls, and visible focus indicators
* **Custom banner**: Show an optional message (with an optional link) to clarify the toolbar's scope for your team

The toolbar is intended for development and testing environments only. Don't use it in production unless you're building a public demo where users interact with feature flags without Unleash access.

## Installation

Choose the installation command for your framework:

#### JavaScript

```bash
npm install @unleash/toolbar unleash-proxy-client
```

#### React

```bash
npm install @unleash/toolbar @unleash/proxy-client-react unleash-proxy-client
```

#### Next.js

```bash
npm install @unleash/toolbar @unleash/nextjs
```

Import the CSS file to load the toolbar styles:

```javascript
import '@unleash/toolbar/toolbar.css';
```

## Quickstart

#### JavaScript

The `initUnleashToolbar` function wraps your `UnleashClient` and returns a client with the same API. Use the wrapped client for all flag evaluations.

```javascript {5-12}
import { initUnleashToolbar } from '@unleash/toolbar';
import { UnleashClient } from 'unleash-proxy-client';
import '@unleash/toolbar/toolbar.css';

const client = initUnleashToolbar(new UnleashClient({
  url: 'https://your-unleash.com/api/frontend',
  clientKey: 'your-frontend-token',
  appName: 'my-app'
}), {
  storageMode: 'local',
  position: 'bottom-right'
});

await client.start();

// Use the wrapped client for flag checks
const isEnabled = client.isEnabled('my-feature');
const variant = client.getVariant('my-experiment');

// Listen for changes from toolbar or SDK updates
client.on('update', () => {
  const newValue = client.isEnabled('my-feature');
  updateUI(newValue);
});
```

[View the complete JavaScript example on GitHub](https://github.com/Unleash/toolbar/tree/main/examples/vanilla)

#### React

Use `UnleashToolbarProvider` as a drop-in replacement for `FlagProvider`. Pass your configuration directly and use hooks from the official React SDK.

```tsx {14-17}
import { useFlag, useVariant } from '@unleash/proxy-client-react';
import { UnleashToolbarProvider } from '@unleash/toolbar/react';
import '@unleash/toolbar/toolbar.css';

const config = {
  url: 'https://your-unleash.com/api/frontend',
  clientKey: 'your-frontend-token',
  appName: 'my-app',
  refreshInterval: 15
};

function App() {
  return (
    <UnleashToolbarProvider 
      config={config}
      toolbarOptions={{ storageMode: 'local', position: 'bottom-right' }}
    >
      <MyComponent />
    </UnleashToolbarProvider>
  );
}

function MyComponent() {
  const isEnabled = useFlag('my-feature');
  const variant = useVariant('my-experiment');
  
  return (
    <div>
      {isEnabled && <NewFeature />}
      {variant.name === 'variant-a' && <VariantA />}
    </div>
  );
}
```

[View the complete React example on GitHub](https://github.com/Unleash/toolbar/tree/main/examples/react)

#### Next.js

Wrap your app in `UnleashToolbarProvider` and conditionally enable the toolbar based on environment.

### Client components

```tsx {9-20}
// app/layout.tsx
import { UnleashToolbarProvider } from '@unleash/toolbar/next';
import '@unleash/toolbar/toolbar.css';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <UnleashToolbarProvider
          config={{
            url: process.env.NEXT_PUBLIC_UNLEASH_URL!,
            clientKey: process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY!,
            appName: 'my-next-app',
          }}
          toolbarOptions={
            process.env.NODE_ENV !== 'production' 
              ? { themePreset: 'dark', initiallyVisible: false }
              : undefined
          }
        >
          {children}
        </UnleashToolbarProvider>
      </body>
    </html>
  );
}
```

```tsx {4}
// app/page.tsx
'use client';

import { useFlag, useVariant } from '@unleash/toolbar/next';

export default function HomePage() {
  const isEnabled = useFlag('new-checkout');
  const variant = useVariant('payment-provider');

  return (
    <div>
      {isEnabled && <NewCheckout />}
      <PaymentForm provider={variant.name} />
    </div>
  );
}
```

### Server components (SSR)

The toolbar supports server-side rendering through cookie-based state sync. Client-side toolbar changes automatically sync to cookies, which server components can read.

```tsx {14}
// app/server-page/page.tsx
import { cookies } from 'next/headers';
import { getDefinitions, evaluateFlags, flagsClient } from '@unleash/nextjs';
import { applyToolbarOverrides } from '@unleash/toolbar/next/server';

export default async function ServerPage() {
  // Fetch definitions from Unleash API
  const definitions = await getDefinitions({
    fetchOptions: { next: { revalidate: 15 } },
  });

  // Apply toolbar overrides from cookies
  const cookieStore = await cookies();
  const modifiedDefinitions = applyToolbarOverrides(definitions, cookieStore);

  // Evaluate flags with context
  const { toggles } = evaluateFlags(modifiedDefinitions, {
    userId: 'user-123',
  });

  const flags = flagsClient(toggles);
  const isEnabled = flags.isEnabled('new-feature');

  return <div>{isEnabled ? 'Feature ON' : 'Feature OFF'}</div>;
}
```

### Environment variables

```shell
# Server-side (used by @unleash/nextjs SDK)
UNLEASH_SERVER_API_URL=https://your-unleash.com/api
UNLEASH_SERVER_API_TOKEN=your-server-token
UNLEASH_APP_NAME=my-app

# Client-side (used by toolbar)
NEXT_PUBLIC_UNLEASH_URL=https://your-unleash.com/api/frontend
NEXT_PUBLIC_UNLEASH_CLIENT_KEY=your-frontend-token
```

[View the complete Next.js example on GitHub](https://github.com/Unleash/toolbar/tree/main/examples/nextjs)

#### Vue

Create a composable that wraps the Unleash client with the toolbar in development mode.

```typescript {20-27}
// composables/useUnleash.ts
import { ref, onMounted } from 'vue'
import { UnleashClient } from 'unleash-proxy-client'
import { initUnleashToolbar } from '@unleash/toolbar'
import '@unleash/toolbar/toolbar.css'

export function useUnleash() {
  const isReady = ref(false)
  const unleashClient = ref(null)
  const updateTrigger = ref(0)

  onMounted(async () => {
    const client = new UnleashClient({
      url: import.meta.env.VITE_UNLEASH_URL,
      clientKey: import.meta.env.VITE_UNLEASH_CLIENT_KEY,
      appName: import.meta.env.VITE_UNLEASH_APP_NAME,
    })

    // Wrap with toolbar in development mode
    if (import.meta.env.DEV) {
      unleashClient.value = initUnleashToolbar(client, {
        themePreset: 'dark',
        initiallyVisible: false,
      })
    } else {
      unleashClient.value = client
    }

    await unleashClient.value.start()
    isReady.value = true

    // Trigger re-evaluation on updates
    unleashClient.value.on('update', () => {
      updateTrigger.value++
    })
  })

  return { unleashClient, isReady, updateTrigger }
}
```

[View the complete Vue example on GitHub](https://github.com/Unleash/toolbar/tree/main/examples/vue)

#### Angular

Create a service that wraps the Unleash client with the toolbar.

```typescript {19-26}
// unleash.service.ts
import { Injectable, isDevMode } from '@angular/core';
import { UnleashClient } from 'unleash-proxy-client';
import { initUnleashToolbar } from '@unleash/toolbar';
import '@unleash/toolbar/toolbar.css';
import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class UnleashService {
  private client: any;

  constructor() {
    const unleashClient = new UnleashClient({
      url: environment.unleash.url,
      clientKey: environment.unleash.clientKey,
      appName: environment.unleash.appName,
    });

    if (isDevMode()) {
      this.client = initUnleashToolbar(unleashClient, {
        themePreset: 'dark',
        initiallyVisible: false,
      });
    } else {
      this.client = unleashClient;
    }
  }

  async start() {
    await this.client.start();
  }

  isEnabled(flagName: string): boolean {
    return this.client.isEnabled(flagName);
  }

  getVariant(flagName: string): any {
    return this.client.getVariant(flagName);
  }

  onUpdate(callback: () => void): void {
    this.client.on('update', callback);
  }
}
```

[View the complete Angular example on GitHub](https://github.com/Unleash/toolbar/tree/main/examples/angular)

## Configuration options

| Option                | Type                                                                                          | Default                   | Description                                                                                                                                                                                                        |
| --------------------- | --------------------------------------------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `storageMode`         | `'memory'` \| `'session'` \| `'local'`                                                        | `'local'`                 | Where to persist overrides. `local` survives browser restarts, `session` clears when tab closes, `memory` clears on reload.                                                                                        |
| `storageKey`          | `string`                                                                                      | `'unleash-toolbar-state'` | Storage key for persistence.                                                                                                                                                                                       |
| `position`            | `'top-left'` \| `'top-right'` \| `'bottom-left'` \| `'bottom-right'` \| `'left'` \| `'right'` | `'bottom-right'`          | Starting toolbar position. Once the user drags the toolbar, the dragged position takes over.                                                                                                                       |
| `draggable`           | `boolean`                                                                                     | `true`                    | Allow dragging the floating icon to any window edge. The chosen position is persisted.                                                                                                                             |
| `initiallyVisible`    | `boolean`                                                                                     | `false`                   | Whether the toolbar panel is open on load.                                                                                                                                                                         |
| `showToggleButton`    | `boolean`                                                                                     | `true`                    | Render the floating icon when the panel is collapsed. Set to `false` for a shortcut-driven setup where the toolbar renders nothing until it's opened.                                                              |
| `shortcut`            | `string` \| `false`                                                                           | `'mod+shift+f'`           | Keyboard shortcut that opens the panel, focuses it when it's already open, and minimizes it when pressed from inside. `mod` is Cmd on macOS and Ctrl elsewhere. Set to `false` to register no global key listener. |
| `focusOnOpen`         | `'panel'` \| `'search'` \| `'context'`                                                        | `'panel'`                 | Where to place focus when the panel opens.                                                                                                                                                                         |
| `closeOnOutsideClick` | `boolean`                                                                                     | `false`                   | Minimize the panel when a click lands outside it. Off by default so that clicking around your app doesn't dismiss the panel.                                                                                       |
| `banner`              | `string`                                                                                      | -                         | Optional message shown as a banner below the header. Useful for clarifying the toolbar's scope to your team. Empty by default.                                                                                     |
| `bannerLink`          | `string`                                                                                      | -                         | Optional URL shown as a link next to the banner message. Only rendered when `banner` is also set. Opens in a new tab.                                                                                              |
| `bannerLinkText`      | `string`                                                                                      | `'Read more'`             | Text for the banner link. Only used when `bannerLink` is set.                                                                                                                                                      |
| `sortAlphabetically`  | `boolean`                                                                                     | `false`                   | Sort flags alphabetically instead of by evaluation order.                                                                                                                                                          |
| `themePreset`         | `'light'` \| `'dark'`                                                                         | `'light'`                 | Color theme preset.                                                                                                                                                                                                |
| `theme`               | `object`                                                                                      | -                         | Custom theme with `primaryColor`, `backgroundColor`, `textColor`, `borderColor`, `fontFamily`, `focusColor`.                                                                                                       |
| `container`           | `HTMLElement`                                                                                 | `document.body`           | DOM element to render the toolbar into.                                                                                                                                                                            |
| `enableCookieSync`    | `boolean`                                                                                     | `false`                   | Enable cookie sync for SSR frameworks like Next.js.                                                                                                                                                                |

### Storage modes explained

* **`local`** (recommended for development): Persists across all tabs and browser restarts. Set overrides once, test everywhere.
* **`session`**: Persists within the current tab only. Useful for testing different configurations in multiple tabs simultaneously.
* **`memory`**: No persistence. Clears on every page reload. Use for quick one-off tests.

### Moving and hiding the toolbar

* **Move it**: Drag the floating icon to any window edge. It snaps to the nearest edge and remembers its position across reloads. Set `draggable: false` to disable dragging and keep the configured `position`.
* **Minimize it**: Click the minimize (`_`) button in the panel header, or press `Esc` while focus is inside the panel, to collapse the panel back to the floating icon. `Esc` also returns focus to wherever you opened the panel from.
* **Hide it completely**: Click the close (`×`) button to hide both the panel and the icon. This is temporary; the toolbar reappears (minimized) after a page refresh.

### Adding a banner

Use the `banner` option to display a message in the toolbar, for example to clarify which flags the toolbar can override. Optionally add a link with `bannerLink` (and customize its label with `bannerLinkText`).

```javascript
initUnleashToolbar(client, {
  banner: 'Only client-side flags are overridable here. Backend-only flags are unaffected.',
  bannerLink: 'https://docs.getunleash.io/integrate/toolbar',
  bannerLinkText: 'Learn more', // defaults to "Read more"
});
```

## Keyboard and accessibility

The toolbar is fully operable from the keyboard.

### Keyboard shortcuts

To open the toolbar panel, or move focus into it when it's already open:

* **Windows/Linux**: `Ctrl + Shift + F`
* **macOS**: `Cmd + Shift + F`

Pressing the same shortcut again from inside the panel minimizes it.

Once the panel is open:

| Key                   | Action                                                                             |
| --------------------- | ---------------------------------------------------------------------------------- |
| `Esc`                 | Minimize, handing focus back to where you opened the panel from                    |
| `Tab` / `Shift + Tab` | Move between controls; off either end, focus returns to the page where you left it |
| `←` `→`               | Switch tabs, or change a flag between off, default, and on                         |

Use the `shortcut` option to rebind the shortcut if it conflicts with one in your app, or set it to `false` to register no global key listener:

```javascript
initUnleashToolbar(client, {
  shortcut: 'mod+shift+u', // mod is Cmd on macOS, Ctrl elsewhere
});
```

### Shortcut-driven setup

To render nothing until the toolbar is summoned, and open it with focus already in the flag search box:

```javascript
initUnleashToolbar(client, {
  showToggleButton: false,
  focusOnOpen: 'search',
});
```

The toolbar stays mounted while closed. It's hidden with `display: none`, so there's no re-initialization cost when it reopens, and it stays out of both the tab order and the accessibility tree while hidden.

With both `showToggleButton: false` and `shortcut: false`, users have no way to open the toolbar; only the programmatic API can. Keep at least one of them enabled.

### Focus behavior

The panel isn't modal. The page underneath stays interactive, is never marked `inert`, and focus is never trapped, since watching your app react to a flag change is the point of the tool and keyboard users need to be able to get back out to it.

When you open the panel from the keyboard, focus lands on the panel itself rather than on a control, so screen readers announce the region and its name. The panel also draws a focus ring so that sighted keyboard users can see that focus has left the page. The ring stays off when the panel is opened by clicking the floating icon.

From there, focus is tethered to wherever you summoned the panel from:

* `Tab` off the last control, or `Shift + Tab` off the first, returns focus to the element you opened the panel from and leaves the panel open. The next `Tab` continues through your page from that point.
* The shortcut is the way back in. Pressed from the page while the panel is open, it pulls focus into the panel instead of closing it. Pressed from inside the panel, it minimizes.
* `Esc` minimizes and hands focus back to the same element, or to the floating icon if the icon is what opened the panel.

If there's no element to return to, because the panel was opened by clicking the icon or the original element has been removed from the page, `Tab` follows the browser's normal order.

Calling `hide()` from your own code only moves focus if the toolbar currently holds it, so hiding the toolbar programmatically never interrupts a user who is working in your app.

### Screen reader support

* The panel is a labeled `region` landmark, so assistive technology can jump straight to it regardless of where the toolbar sits in the tab order.
* Tabs follow the WAI-ARIA tabs pattern, with arrow-key navigation and a single tab stop.
* Each flag's off/default/on control is a `radiogroup` labeled with the flag name, so assistive technology reports which state is in effect. The group is a single tab stop, so you can move past a flag with one `Tab` press instead of three.
* Icon-only controls have visually hidden text labels rather than relying on tooltips.
* Animations are suppressed when the user prefers reduced motion.

### Focus ring color

The keyboard focus ring follows `themePreset` automatically, and both presets meet the 3:1 contrast ratio that WCAG requires of non-text indicators. Controls on the colored header use a white ring instead, since `primaryColor` is already required to be dark enough for the header's white text.

Set `theme.focusColor` only if a custom `backgroundColor` leaves the default ring hard to see, for example a dark background while staying on the `light` preset:

```javascript
initUnleashToolbar(client, {
  theme: { backgroundColor: '#101010', textColor: '#f5f5f5', focusColor: '#A9A6F5' },
});
```

## API reference

Access the toolbar instance via `window.unleashToolbar`:

```typescript
const toolbar = window.unleashToolbar;

// Show/hide the toolbar panel
toolbar.show();
toolbar.hide();
toolbar.toggle();

// Open with focus on a specific control
toolbar.show({ focus: 'search' });   // flag search box
toolbar.show({ focus: 'context' });  // first Context field, switching tabs
toolbar.show({ focus: 'panel' });    // the panel itself (default)

// Get current state
const state = toolbar.getState();

// Set flag overrides
toolbar.setFlagOverride('my-feature', { type: 'flag', value: true });
toolbar.setFlagOverride('my-variant', { type: 'variant', variantKey: 'variant-b' });
toolbar.setFlagOverride('my-feature', null); // Clear override

// Set context overrides
toolbar.setContextOverride({
  userId: 'test-user-123',
  properties: { tier: 'premium' }
});

// Reset all overrides
toolbar.resetOverrides();
toolbar.resetContextOverrides();

// Clean up
toolbar.destroy();
```

### Next.js server utilities

```typescript
import { 
  applyToolbarOverrides, 
  applyToolbarOverridesToToggles, 
  getToolbarStateFromCookies 
} from '@unleash/toolbar/next/server';

// Apply overrides to definitions before evaluation (recommended)
const modifiedDefinitions = applyToolbarOverrides(definitions, cookieStore);

// Apply overrides to toggles after evaluation (alternative)
const modifiedToggles = applyToolbarOverridesToToggles(toggles, cookieStore);

// Read toolbar state directly from cookies
const state = getToolbarStateFromCookies(cookieStore);
```

## Bundle size

The toolbar is optimized for minimal impact:

| Package           | Size (gzipped) |
| ----------------- | -------------- |
| Core              | \~12 KB        |
| React wrapper     | \~0.6 KB       |
| Next.js utilities | \~0.7 KB       |
| CSS               | \~3 KB         |

Of the core bundle, only \~2.4 KB is the entry point. The panel UI (\~9.6 KB) is a separate chunk loaded asynchronously, so it doesn't block your initial bundle.

## Requirements

* **Browser**: ES2020 support (Chrome 90+, Firefox 88+, Safari 14+)
* **SDK versions**:
  * `unleash-proxy-client` ^3.0.0
  * `@unleash/proxy-client-react` ^5.0.0 (optional)
  * `@unleash/nextjs` ^1.0.0 (optional)

## Troubleshooting

#### Toolbar doesn't appear

Make sure you've imported the CSS file:

```javascript
import '@unleash/toolbar/toolbar.css';
```

Check that you're using the wrapped client returned by `initUnleashToolbar()`, not the original client.

#### Overrides don't persist

Check your `storageMode` setting. If set to `'memory'`, overrides clear on page reload. Use `'local'` for persistent overrides across sessions.

#### Server components don't reflect overrides

For Next.js SSR, ensure you're using `applyToolbarOverrides()` in your server components and that `enableCookieSync` is enabled in your toolbar options.

#### Hooks not re-rendering on override changes

The wrapped client emits `'update'` events when overrides change. If you're using a custom setup, make sure you're listening to these events and triggering re-renders.

#### Keyboard shortcut doesn't work or conflicts with my app

The toolbar only reacts when the modifiers match exactly, so a shortcut like `Cmd + Alt + Shift + F` still reaches your app. If the default conflicts anyway, rebind it with `shortcut: 'mod+shift+u'`, or set `shortcut: false` to register no global key listener and open the toolbar through `toolbar.show()` instead.

If the shortcut does nothing at all, check that another handler on the page isn't calling `preventDefault()` on the keydown event before it reaches the toolbar.