@unleash/toolbar
v1.2.0
Published
Client-side override toolbar for Unleash JavaScript SDKs
Readme
Unleash Toolbar
A client-side debugging toolbar for Unleash feature flags. Override flag values and context in real-time without server changes.

Features
- Flag Overrides: Force boolean flags ON/OFF or override variant values
- Context Overrides: Modify userId, sessionId, environment, and custom properties
- Persistence: Save overrides in memory, sessionStorage, or localStorage
- React Integration: Seamlessly wraps the official
@unleash/proxy-client-reactSDK - Next.js SSR Support: Server-side rendering with cookie-based state sync using
@unleash/nextjs - Customizable UI: Theming support and positioning options
- Draggable & Dismissible: Drag the floating icon to any window edge, minimize to the icon, or hide it entirely until the next page refresh
- Keyboard First:
Cmd/Ctrl+Shift+Fto summon,Escto minimize, focus tethered back to wherever you opened it from, and full screen-reader semantics - Custom Banner: Optional message in the toolbar to clarify its scope for your team
- SDK Compatible: Works with Unleash JavaScript SDK
Bundle Size
The toolbar is optimized for minimal impact on your application:
- Core: ~12 KB gzipped (~2.4 KB entry + ~9.6 KB panel UI, loaded asynchronously)
- React: ~0.6 KB gzipped (thin wrapper)
- Next.js: ~0.7 KB gzipped (server utilities)
- CSS: ~3 KB gzipped
Installation
npm install @unleash/toolbar
# For React integration
npm install @unleash/toolbar @unleash/proxy-client-react unleash-proxy-client
# For Next.js SSR integration
npm install @unleash/toolbar @unleash/nextjsQuick Start
Vanilla JavaScript
import { initUnleashToolbar } from '@unleash/toolbar';
import { UnleashClient } from 'unleash-proxy-client';
import '@unleash/toolbar/toolbar.css';
// Initialize toolbar with new Unleash client - returns wrapped client
const client = initUnleashToolbar(new UnleashClient({
url: 'https://your-unleash-instance.com/api/frontend',
clientKey: 'your-client-key',
appName: 'my-app'
}), {
storageMode: 'local',
position: 'bottom-right',
initiallyVisible: true
});
// Start the client
await client.start();
// Use the client for all flag evaluations
const isEnabled = client.isEnabled('my-feature');
const variant = client.getVariant('my-experiment');
// Listen for changes (from toolbar or SDK updates)
client.on('update', () => {
// Re-evaluate flags when overrides change
const newValue = client.isEnabled('my-feature');
updateUI(newValue);
});React
The toolbar integrates seamlessly with the official @unleash/proxy-client-react SDK. Just pass your configuration - the toolbar handles everything automatically!
npm install @unleash/toolbar @unleash/proxy-client-react unleash-proxy-clientimport { useFlag, useVariant } from '@unleash/proxy-client-react';
import { UnleashToolbarProvider } from '@unleash/toolbar/react';
import '@unleash/toolbar/toolbar.css';
// Same config format as the official React SDK
const config = {
url: 'https://your-unleash-instance.com/api/frontend',
clientKey: 'your-client-key',
appName: 'my-app',
refreshInterval: 15
};
// That's it! No need to import FlagProvider or manage the SDK client
function App() {
return (
<UnleashToolbarProvider
config={config}
toolbarOptions={{
storageMode: 'local',
position: 'bottom-right'
}}
>
<MyComponent />
</UnleashToolbarProvider>
);
}
// Use hooks from the official React SDK - they work with toolbar overrides!
function MyComponent() {
const isEnabled = useFlag('my-feature');
const variant = useVariant('my-experiment');
return (
<div>
{isEnabled && <NewFeature />}
{variant.name === 'variant-a' && <VariantA />}
</div>
);
}Key Points:
- Pass
configdirectly toUnleashToolbarProvider- no other imports needed! - The toolbar automatically uses
FlagProviderfrom the React SDK - Import hooks from
@unleash/proxy-client-react(the official SDK) - All official React SDK hooks work seamlessly with toolbar overrides
Advanced: Custom FlagProvider or pre-instantiated client
import { FlagProvider } from '@unleash/proxy-client-react';
import { UnleashClient } from 'unleash-proxy-client';
// Option 1: Custom FlagProvider
<UnleashToolbarProvider
FlagProvider={FlagProvider} // Optional - use if you need customization
config={config}
>
<MyApp />
</UnleashToolbarProvider>
// Option 2: Pre-instantiated client
const client = new UnleashClient({ /* config */ });
<UnleashToolbarProvider
client={client} // Pass client instead of config
toolbarOptions={{ /* ... */ }}
>
<MyApp />
</UnleashToolbarProvider>Next.js App Router (Client & Server Components)
The toolbar provides full Next.js App Router support with server-side rendering and cookie-based state synchronization.
Client Components
// 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',
}}
>
{children}
</UnleashToolbarProvider>
</body>
</html>
);
}// 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 with SSR
// 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 (uses env config)
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, {
sessionId: 'session-id',
userId: 'user-id',
});
// Create offline client
const flags = flagsClient(toggles);
// Check flags server-side
const isEnabled = flags.isEnabled('new-feature');
return <div>{isEnabled ? 'Feature ON' : 'Feature OFF'}</div>;
}Environment Variables for Next.js:
# Used by @unleash/nextjs SDK
UNLEASH_SERVER_API_URL=https://your-unleash-instance.com/api
UNLEASH_SERVER_API_TOKEN=your-server-token
UNLEASH_APP_NAME=my-app
# Used by client-side toolbar
NEXT_PUBLIC_UNLEASH_URL=https://your-unleash-instance.com/api/frontend
NEXT_PUBLIC_UNLEASH_CLIENT_KEY=your-client-key
NEXT_PUBLIC_UNLEASH_APP_NAME=my-appHow it works:
- Client-side toolbar automatically syncs state to cookies
- Server components read toolbar state from cookies
applyToolbarOverrides()modifies flag definitions before evaluation- Flags evaluate server-side with toolbar overrides applied
- Changes in toolbar immediately affect both client and server rendering
Vue 3 (Composition API)
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useUnleash } from './composables/useUnleash'
// Initialize Unleash with the composable
const { unleashClient, isReady, updateTrigger } = useUnleash()
// Reactive flag states
const isEnabled = ref(false)
// Evaluate flags
const evaluateFlags = () => {
if (!unleashClient.value) return
isEnabled.value = unleashClient.value.isEnabled('my-feature')
}
// Evaluate when ready
watch(isReady, (ready) => {
if (ready) evaluateFlags()
})
</script>Configuration Options
initUnleashToolbar(client, options)
interface InitToolbarOptions {
// Persistence mode (default: 'local')
// - 'local': Persists across tabs and browser restarts (RECOMMENDED for development)
// - 'session': Persists only in current tab, cleared when tab closes
// - 'memory': No persistence, cleared on page reload
storageMode?: 'memory' | 'session' | 'local';
// Storage key for persistence (default: 'unleash-toolbar-state')
storageKey?: string;
// UI position (default: 'bottom-right')
// Corner positions: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
// Side positions (vertically centered): 'left' | 'right'
// Used as the starting position; overridden once the user drags the toolbar.
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'left' | 'right';
// Allow dragging the floating icon to reposition the toolbar along any
// window edge. The chosen position is persisted. (default: true)
draggable?: boolean;
// Initial visibility (default: false, but respects persisted state if available)
initiallyVisible?: boolean;
// Optional informational message shown as a banner below the header.
// Useful for clarifying the toolbar's scope to your team, e.g.
// "Only client-side flags are overridable here". Empty by default.
banner?: string;
// Optional URL shown as a link next to the banner message (opens in a new
// tab). Only rendered when `banner` is also set.
bannerLink?: string;
// Text for the banner link (default: "Read more"). Only used with bannerLink.
bannerLinkText?: string;
// Sort flags alphabetically instead of by evaluation order (default: false)
sortAlphabetically?: boolean;
// Theme preset (default: 'light')
themePreset?: 'light' | 'dark';
// Theme customization (overrides preset)
theme?: {
primaryColor?: string;
backgroundColor?: string;
textColor?: string;
borderColor?: string;
fontFamily?: string;
};
// Custom container element (default: document.body)
container?: HTMLElement | null;
// Enable cookie sync for SSR (default: false)
// Set to true for Next.js or other SSR frameworks
enableCookieSync?: boolean;
// Render the floating toggle icon when collapsed (default: true)
// Set to false for a shortcut-only setup where the toolbar renders nothing
// until it is opened.
showToggleButton?: boolean;
// Keyboard shortcut that opens the panel, focuses it when already open, and
// minimizes it when pressed from inside (default: 'mod+shift+f')
// 'mod' is Cmd on macOS and Ctrl elsewhere. Set to false to register no
// global key listener.
shortcut?: string | false;
// Minimize the panel when a pointer press lands outside it (default: false)
closeOnOutsideClick?: boolean;
// Where to place focus when the panel opens (default: 'panel')
focusOnOpen?: 'panel' | 'search' | 'context';
}Next.js Server Utilities
import { applyToolbarOverrides, applyToolbarOverridesToToggles, getToolbarStateFromCookies } from '@unleash/toolbar/next/server';
import type { ClientFeaturesResponse, IToggle } from '@unleash/nextjs';
// Apply overrides before evaluation (recommended)
const modifiedDefinitions = applyToolbarOverrides(
definitions: ClientFeaturesResponse,
cookieStore: CookieStore
): ClientFeaturesResponse;
// Apply overrides after evaluation (alternative)
const modifiedToggles = applyToolbarOverridesToToggles(
toggles: IToggle[],
cookieStore: CookieStore
): IToggle[];
// Read toolbar state from cookies
const state = getToolbarStateFromCookies(
cookieStore: CookieStore
): ToolbarState | null;Storage Modes Explained
local (default): Best for development workflows
- ✅ Persists across all browser tabs
- ✅ Survives page reloads and browser restarts
- ✅ Set overrides once, test everywhere
- Use case: Daily feature development and debugging
session: Useful for isolated testing
- ✅ Persists within current tab only
- ✅ Survives page reloads in the same tab
- ❌ Lost when tab is closed
- Use case: Testing different configurations in multiple tabs simultaneously
memory: Temporary testing only
- ❌ Lost on every page reload
- ❌ No persistence whatsoever
- Use case: Quick one-off tests or strict security requirements
API Reference
Toolbar Instance
const toolbar = window.unleashToolbar;
// Show/hide the toolbar
toolbar.show();
toolbar.hide();
// Opens, or focuses an already-open panel, or minimizes it if focus is inside
toolbar.toggle();
// Open with focus on a specific control
toolbar.show({ focus: 'search' }); // flag search box
toolbar.show({ focus: 'context' }); // first Context field (switches tab)
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 overrides
toolbar.resetOverrides();
toolbar.resetContextOverrides();
// Cleanup
toolbar.destroy();Listening to Changes
The toolbar automatically triggers the Unleash SDK's 'update' event when overrides change. Use the standard SDK pattern to listen for changes:
// Listen to flag/context changes from the toolbar
client.on('update', () => {
console.log('Flags updated - re-evaluate your flags');
// Re-render your UI, re-check flags, etc.
});This works for both toolbar changes (flag overrides, context overrides) and SDK changes (new config from server).
React Hooks
Use hooks from the official @unleash/proxy-client-react SDK - they automatically work with toolbar overrides!
import {
useFlag,
useVariant,
useUnleashClient,
useUnleashContext,
useFlagsStatus
} from '@unleash/proxy-client-react';
// Check flag status - updates automatically when toolbar overrides change
const isEnabled = useFlag('my-feature');
// Get variant - updates automatically when toolbar overrides change
const variant = useVariant('my-experiment');
// Access client (the wrapped client from the toolbar)
const client = useUnleashClient();
// Update context dynamically
const updateContext = useUnleashContext();
await updateContext({ userId: 'new-user-id' });
// Check loading/ready state
const { flagsReady, flagsError } = useFlagsStatus();Next.js Hooks
For Next.js, import hooks from @unleash/toolbar/next, they're re-exported for convenience:
import { useFlag, useVariant, useUnleashClient } from '@unleash/toolbar/next';
const isEnabled = useFlag('my-feature');
const variant = useVariant('my-experiment');UI Features
Flag List Tab
- Override Controls: Dropdown to set boolean or variant overrides
- Flag Info: See default vs. effective values for each flag
- Quick Actions: Reset individual flag overrides
Context Tab
- Standard Fields: userId, sessionId, remoteAddress, environment, appName
- Custom Properties: Edit or reset property values
- Live Updates: Changes apply immediately to all evaluations
Header Actions
- Reset Flags: Clear all flag overrides
- Reset Context: Clear all context overrides
- Close: Hide the toolbar
Keyboard & Accessibility
The toolbar is fully operable from the keyboard.
Shortcuts
| Key | Action |
| --- | --- |
| Cmd/Ctrl + Shift + F | Open the panel, focus it when it is already open, or minimize it when pressed from inside |
| Esc | Minimize, handing focus back to wherever 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 (while a tab has focus) |
| ← → | Change a flag's override between OFF / — / ON |
Rebind or disable the shortcut with the shortcut option:
initUnleashToolbar(client, {
shortcut: 'mod+shift+u', // 'mod' is Cmd on macOS, Ctrl elsewhere
});
initUnleashToolbar(client, {
shortcut: false, // register no global key listener
});Shortcut-driven setup
To render nothing until the toolbar is summoned, and land directly in the flag search box:
initUnleashToolbar(client, {
showToggleButton: false,
focusOnOpen: 'search',
});The toolbar stays mounted and listening while "closed" — it is hidden with
display: none, so there is no re-initialization cost on reopen, and it is out
of both the tab order and the accessibility tree while hidden.
With
showToggleButton: falseandshortcut: falsethere is no way for a user to reopen the toolbar; only the programmatic API can. Keep at least one of them enabled.
Focus behaviour
The panel is not modal. The page underneath stays interactive, is never marked
inert, and focus is never trapped — watching your app react to a flag change is
the whole point of the tool, and a keyboard user has to be able to get back out
there.
Opened from the keyboard, focus lands on the panel itself rather than on a control, so screen readers announce the region and its name — and the panel draws a focus ring so 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 the panel was summoned from:
Taboff the last control (orShift+Taboff the first) returns focus to the element you opened the panel from, leaving the panel open. The nextTabcarries on through your page from that point.- The shortcut is the way back in. Pressed from the page with the panel open it pulls focus into the panel rather than closing it; pressed from inside the panel it minimizes.
Escinside the panel minimizes and hands focus back to the same origin — or to the floating icon when the icon is what opened it.
Without an origin to return to (the panel was opened by clicking the icon, or the
origin has since been unmounted), Tab is left to the browser's own ordering.
Semantics
- The panel is a labelled
regionlandmark, so assistive tech can jump straight to it regardless of where the toolbar sits in the tab order - Tabs use the WAI-ARIA tabs pattern, with arrow-key navigation and a roving tab stop
- Each flag's OFF / — / ON control is a
radiogrouplabelled with the flag name, so assistive tech reports which state is in effect. Its roving tab stop also means a flag costs oneTabpress to pass, not three - Icon-only controls carry visually hidden text rather than relying on
title - Filtering the flag list announces its result count through a
statusregion, debounced so a burst of typing produces one announcement rather than one per keystroke - Animations are suppressed under
prefers-reduced-motion: reduce
Theme Customization
Override CSS variables or use the theme option:
const toolbar = initUnleashSessionToolbar({
theme: {
primaryColor: '#ff6b6b',
backgroundColor: '#ffffff',
textColor: '#2d3436',
borderColor: '#dfe6e9',
fontFamily: 'Inter, sans-serif',
focusColor: '#ff6b6b' // keyboard focus ring; optional
}
});Focus ring colour
The keyboard focus ring follows themePreset automatically — a mid purple on
light, a lighter purple on dark, both above the 3:1 contrast ratio WCAG
requires of non-text indicators. Controls on the coloured header use a white ring
instead, since primaryColor already has to be dark enough to carry the header's
white text.
Set theme.focusColor only when a custom backgroundColor would leave the
default ring hard to see — e.g. a dark backgroundColor while staying on the
light preset:
initUnleashToolbar(client, {
theme: { backgroundColor: '#101010', textColor: '#f5f5f5', focusColor: '#A9A6F5' },
});Or override CSS variables globally:
:root {
--unleash-toolbar-primary: #your-color;
--unleash-toolbar-bg: #your-bg;
--unleash-toolbar-text: #your-text;
--unleash-toolbar-border: #your-border;
--unleash-toolbar-font: your-font;
}Development
# Install dependencies
npm install
# Build the library
npm run build
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run type checking
npm run type-check
# Run linter
npm run lint
# Auto-fix linting issues
npm run lint:fixExample Applications
The repository includes example applications demonstrating integration with different frameworks:
Vanilla JS - Basic HTML/JavaScript integration
npm run serve:example:vanillaReact - Hooks and provider pattern with official React SDK
npm run serve:example:reactNext.js App Router - Server-side rendering with client and server components
npm run serve:example:nextjsAngular - Service-based integration
npm run serve:example:angularVue 3 - Composition API with composables
npm run serve:example:vue
All examples include:
- Environment configuration setup
- Multiple feature flags for testing
- Variant flag demonstrations
- Toolbar integration best practices
- Server-side rendering examples (Next.js)
Requirements
- Browser: Modern browsers with ES2020 support (Chrome 90+, Firefox 88+, Safari 14+)
- Unleash SDKs:
unleash-proxy-client^3.0.0 (required)@unleash/proxy-client-react^5.0.0 (optional, for React)@unleash/nextjs^1.0.0 (optional, for Next.js SSR)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
Apache-2.0
