Vanilla JavaScript SDK
Guided sample
Open the JavaScript sample, follow the first-toggle exercise in its README, then read src/demo.ts (flag keys and offline fixture), src/sample-app.ts (initialization, user and Order context), and tests/published-sdk.test.ts (browser SDK checks). Wait for Toggly.init() before evaluating flags.
The sample's VITE_ settings are public browser build inputs. The App Key identifies the definitions application; do not put management API credentials or secrets in those settings.
Use Toggly's JavaScript SDK in plain JavaScript, HTML, and CSS applications.
Installation
Embed the browser bundle from the CDN:
<script src="https://cdn.jsdelivr.net/npm/@ops-ai/feature-flags-toggly/dist/feature-flags-toggly.bundle.js"></script>
Alternatively, install the package and serve its browser bundle.
$ npm install @ops-ai/feature-flags-toggly
Copy node_modules/@ops-ai/feature-flags-toggly/dist/feature-flags-toggly.bundle.js into your public assets and include it in your HTML. No build inside node_modules is required. The bundle exposes window.Toggly; use that browser global in the examples below.
<script src="./path/to/feature-flags-toggly.bundle.js"></script>
For browser applications, use only the App Key. Never expose API keys in client-side code.
Initialization
Initialize Toggly by running the Toggly.init method and by providing your App Key from your Toggly application page
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>'
})
.then(function (flags) {
// Now you can check if a feature (or more) is Enabled/Disabled
if (Toggly.isFeatureOn('SignUpButton')) {
// SignUpButton is ON
}
if (Toggly.isFeatureOff('DemoScreenshot')) {
// DemoScreenshot is OFF
}
});
Configuration Options
You can customize Toggly's behavior using configuration options:
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
baseURI: 'https://definitions.toggly.io', // Default API URL
connectTimeout: 5000, // Connection timeout in milliseconds (default: 5000)
featureFlagsRefreshInterval: 180000, // Refresh interval in milliseconds (default: 3 minutes)
isDebug: false, // Enable debug logging (default: false)
reloadOnFeatureFlagValidation: false, // Reload page when flags change (default: false)
enableLiveUpdates: true, // WebSocket live updates (default: true)
verifySignatures: false, // Verify ES256 signed envelopes (default: false)
// allowedKeyIds: ['kid-1'], // Optional JWKS kid allow-list when verifySignatures is true
// maxSignatureAgeSeconds: 300, // Optional signature freshness window
onError: function (message, error) {
// Report SDK failures to your app logger, Sentry, or Datadog
console.warn('Toggly error:', message, error);
},
persistCache: true, // Cache flags/variants in localStorage (default: true)
maxCacheKeys: 20, // Optional LRU cap for identity-scoped cache keys (default: unlimited)
flagDefaults: {
"Feature1": true,
"Feature2": false
}
});
maxCacheKeys limits how many identity-scoped flags/variants localStorage
keys are retained. When variants are enabled, flags and variants for the
same evaluation context are protected together during eviction so a
refresh cannot drop one half of the pair. See
Client-side cache limits.
Auto-Generated Identity
Identity is stored in origin-wide localStorage when available. It survives reloads
and browser restarts until cleared and is shared by SDK apps on that origin;
it is not tab-scoped or scoped by App Key. persistCache: false disables definition
caching, not context persistence. When storage is unavailable, supplied context is
retained in memory.
Pass known identity, groups, and claims to init. Omitted fields preserve
stored values; when identity is omitted and none is stored, the SDK generates one.
An explicit empty identity remains anonymous. Identity and claims are targeting
data, not proof of authentication.
Initialization Promise
The init method returns a Promise that resolves with the loaded feature flags:
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>'
})
.then(function (flags) {
console.log('Loaded flags:', flags);
// Flags are now available via Toggly.featureFlagsValue
})
.catch(function (error) {
console.error('Failed to initialize Toggly:', error);
});
Directly Checking a Flag
Check if a flag is on
if (Toggly.isFeatureOn('SignUpButton')) {
// SignUpButton is ON
}
Check if a flag is off
if (Toggly.isFeatureOff('DemoScreenshot')) {
// DemoScreenshot is OFF
}
Check if at least one of the listed flags is on
if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], 1 /* any */)) {
// AT LEAST ONE the provided feature keys is TRUE
}
Evaluate a feature gate (with requirement & negate support)
if (Toggly.evaluateFeatureGate(['ExampleFeatureKey1', 'ExampleFeatureKey2'], 0 /* all */, true)) {
// NOT(all): at least one provided feature key is OFF
}
You can also check multiple feature keys and make use of the requirement (0 for all, 1 for any) and negate (bool) options.
Accessing Feature Flags Value
You can directly access the current feature flags object:
const flags = Toggly.featureFlagsValue;
const isFeatureEnabled = flags['ExampleFeatureKey1'] || false;
This property returns:
- Cached flags from localStorage (if appKey is provided and flags are cached)
- Flag defaults (if no appKey or no cache exists)
Error reporting and fallback behavior
Use onError to observe fetch, cache, parse, and refresh failures without
relying on debug-only console output.
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
onError: function (message, error) {
monitoring.captureException(error, {
tags: { source: 'toggly' },
extra: { message }
});
}
});
Toggly.lastError contains the latest SDK error message. It is set before
onError runs, so it is safe to read synchronously inside the callback.
After a successful load, refresh failures preserve the last-known-good flags
instead of replacing them with defaults or {}. Empty feature maps do not enable
non-empty gates by accident; gated checks fail closed unless a flag is explicitly
enabled.
For the shared reliability contract, see Reliability and Error Handling.
Entity context
Target features based on page entities (orders, products) in addition to user rollouts. See Entity & page context.
Client SDKs do not register context kinds with the dashboard — use local mappers only.
Register a mapper
Toggly.registerContext('Order', function (order) {
return {
kind: 'Order',
key: String(order.id),
attributes: {
Status: order.status,
Total: order.total,
},
};
});
Per-evaluation checks
Pass the entity on each check (not on global setContext()):
// Detail page
if (Toggly.isFeatureOn('OrderBadge', order, 'Order')) {
showBadge();
}
// List page — one fetch, local gate per row
orders.forEach(function (order) {
var on = Toggly.isFeatureOn('OrderBadge', order, 'Order');
});
Or pass a pre-built context object:
Toggly.isFeatureOn('OrderBadge', {
kind: 'Order',
key: '7',
attributes: { Status: 'red' },
});
Mixed defs and fail closed
Evaluated-signed may return true, false, or an { requirement, rules } gate object. isFeatureOn resolves gates locally. Without entity context, gates fail closed (off).
Do not use Toggly.featureFlagsValue[key] === true — gate objects are not booleans.
Users and Rollouts
To facilitate rollouts, and consistently serve the same value for a flag during a session, we need a way of identifying the user, which needs to be supplied by the application
Supply known context before startup
Use the bundle's window.Toggly browser global. Pass the context together so the
first evaluated request targets the intended user without a second startup refresh.
await Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: 'Production',
identity: 'user-123', // Stable user identifier.
groups: ['beta'], // Memberships.
claims: { plan: 'pro' }, // String rule attributes.
});
The SDK copies supplied context before startup, including when storage is unavailable. Omitted fields preserve stored values; explicit empty groups and claims clear them, and an explicit empty identity stays anonymous.
Change context later
For login, logout, or membership changes after startup, use the refreshing API:
await Toggly.setContext({
identity: 'user-456',
groups: ['subscribers'],
claims: { plan: 'pro' },
});
// Evaluate the updated context after this resolves.
Assigning Toggly.identity updates identity but does not refresh definitions. Prefer setContext when changing users. await Toggly.clearContext() removes the stored identity, clears groups/claims, resets the in-memory state, and refreshes definitions. It does not generate a new stable anonymous identity; follow the sample's explicit anonymous transition if you need one.
Browser user-agent/language and country filters are evaluated by the Definitions service from the HTTP request. The browser SDK exposes no per-call user-agent or country override. The second/third arguments of isFeatureOn are entity context and kind, not HTTP overrides.
See the Feature filters reference and Evaluated-signed docs for the query contract.
Live updates and signed definitions
WebSocket live updates are on by default. Set enableLiveUpdates: false to rely on the refresh interval only. When verifySignatures: true, the SDK verifies ES256 envelopes via JWKS before applying flags (see Evaluated-signed and WebSocket sync).
setContext() resets the in-memory evaluated state and refreshes definitions for the new context; direct identity assignment alone does not.
When using user identifiers, evaluated features are cached per user for 30 minutes by default on a sliding window, so the user might not see the change right away as to not confuse the user. The session length is configurable and the session store can be cleared in App Settings
Definition Refresh
Feature definitions are automatically refreshed at the configured interval. The default is 3 minutes, but this can be customized.
Automatic Refresh
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
featureFlagsRefreshInterval: 3 * 60 * 1000 // 3 minutes (default)
})
.then(function () {
// Flags will automatically refresh every 3 minutes
});
Manual Refresh
You can manually refresh feature flags from the server:
Toggly.refresh()
.then(function (flags) {
console.log('Flags refreshed:', flags);
})
.catch(function (error) {
console.error('Failed to refresh flags:', error);
// Falls back to cached flags or defaults
});
Managing Refresh Interval
You can control the automatic refresh interval:
// Start the refresh interval (automatically called during init)
Toggly.startRefreshInterval();
// Cancel the refresh interval
Toggly.cancelRefreshInterval();
If featureFlagsRefreshInterval is set to 0 or negative, automatic refresh is disabled.
Flag Defaults
In case your application can't reach toggly, you can set feature defaults. These defaults are used when:
- The app is offline
- The API request fails
- No cached flags are available
var featureFlagsDefaults = {
"SignUpButton": true,
"DemoScreenshot": true
};
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
flagDefaults: featureFlagsDefaults
});
Using Toggly Without API (Offline Mode)
You can use Toggly with only flag defaults, without connecting to the Toggly API:
Toggly.init({
flagDefaults: {
"Feature1": true,
"Feature2": false,
}
});
This is useful for:
- Development and testing
- Offline-first applications
- Prototyping without API setup
Cache Management
Toggly caches feature flags in localStorage for offline access and performance.
Accessing Cached Flags
Flags are automatically cached after successful API calls. You can access them via:
const flags = Toggly.featureFlagsValue;
Manual Cache Management
You can manually manage the cache:
// Cache flags manually
Toggly.cacheFeatureFlags({
"Feature1": true,
"Feature2": false
});
// Clear the cache
Toggly.clearFeatureFlagsCache();
When the cache is cleared, Toggly will use the flag defaults until new flags are fetched.
HTML/CSS Integration
For static show/hide with no JavaScript, use the dedicated CSS / HTML stylesheet. Link defs.css from client.toggly.io and mark elements with feature-key. That path cannot do per-visitor targeting or percentage rollouts.
This Vanilla SDK fetches JSON from definitions.toggly.io and is the right choice when you need identity, targeting, hooks, or programmatic checks.
Debug Mode
Enable debug mode to see detailed logging of Toggly operations:
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
isDebug: true
});
When debug mode is enabled, Toggly will log:
- Feature flag fetches
- Cache operations
- Feature gate evaluations
- Refresh operations
Page Reload on Flag Changes
You can configure Toggly to automatically reload the page when feature flags change:
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
reloadOnFeatureFlagValidation: true
});
This is useful when you want to ensure users see the latest feature configuration immediately.
Device-local post-filter gates
Gate bundles of flags behind device-local master switches while keeping rollouts on the worker. See the full guide: Post-filter gates.
let apiRedesignEnabled = false;
await Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
localGates: [{
id: 'apiRedesign',
flagKeys: ['ApiV2Checkout', 'ApiV2Profile'],
isEnabled: () => apiRedesignEnabled,
}],
});
apiRedesignEnabled = false;
Toggly.notifyLocalGatesChanged();
Best Practices
- Initialize Once: Initialize Toggly once when your app loads
- Use Caching: Feature definitions are automatically cached in localStorage
- Handle Errors: The SDK gracefully falls back to cached flags or defaults on errors
- Provide User Context: Set identity for accurate targeting and rollouts
- Set Flag Defaults: Provide defaults for offline scenarios
- Enable Debug Mode: Use
isDebug: trueduring development to troubleshoot issues - Manage Refresh Interval: Adjust
featureFlagsRefreshIntervalbased on your needs (0 to disable) - Clear Cache When Needed: Clear the cache when switching users or testing different configurations
Extensibility with Hooks
Toggly provides a powerful hooks system that allows you to extend SDK functionality by hooking into feature flag lifecycle events. This is perfect for integrating with analytics platforms like Microsoft Clarity, monitoring tools, or implementing custom behaviors.
What are Hooks?
Hooks let you execute custom code at specific points in the feature flag evaluation lifecycle:
- beforeEvaluation: Called before a feature flag is evaluated
- afterEvaluation: Called after a feature flag is evaluated (with the result)
- beforeIdentify: Called before user identity is set or cleared
- afterIdentify: Called after user identity is set or cleared
- afterRefresh: Called after feature definitions are refreshed from Toggly
Creating a Hook
A hook is an object that implements the Hook interface from @ops-ai/toggly-hooks-types:
const myAnalyticsHook = {
getMetadata: () => ({
name: 'MyAnalyticsHook',
version: '1.0.0'
}),
beforeEvaluation: async (data) => {
console.log('About to evaluate:', data.featureKey);
},
afterEvaluation: async (data) => {
console.log('Evaluated:', data.featureKey, '=', data.result);
// Send to analytics
if (typeof analytics !== 'undefined') {
analytics.track('Feature Flag Evaluated', {
feature: data.featureKey,
enabled: data.result
});
}
},
afterIdentify: async (data) => {
console.log('Identity set:', data.userId);
}
};
Registering Hooks
You can register hooks in two ways:
1. During initialization:
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
hooks: [myAnalyticsHook, myMonitoringHook]
})
.then(function () {
// Hooks are now active
});
2. At runtime:
// Add a hook
Toggly.addHook(myAnalyticsHook);
// Remove a hook
Toggly.removeHook(myAnalyticsHook.getMetadata().name);
Hook Execution Order
When multiple hooks are registered:
- before hooks execute in FIFO order (first registered, first executed)
- after hooks execute in LIFO order (last registered, first executed)
This creates a "wrap" pattern where the first hook to start is the last to finish.
Error Isolation
Hooks are designed to be safe:
- If a hook throws an error, it won't affect feature flag evaluation
- Other hooks will continue to execute
- Errors are logged but don't propagate to your application code
Performance
Hooks are optimized for minimal performance impact:
- Hooks execute asynchronously without blocking evaluation
- Hook execution is extremely fast (typically < 1ms per hook)
- Multiple hooks can be registered without significant overhead
Common Use Cases
Microsoft Clarity Integration
const clarityHook = {
getMetadata: () => ({ name: 'Microsoft Clarity', version: '1.0.0' }),
afterEvaluation: async (data) => {
if (typeof clarity !== 'undefined') {
clarity('event', `FeatureFlag:${data.featureKey}`);
}
}
};
Toggly.init({
appKey: '<YOUR_APP_KEY>',
environment: '<YOUR_APP_ENVIRONMENT>',
hooks: [clarityHook]
});
Debug Logging
const debugHook = {
getMetadata: () => ({ name: 'DebugLogger', version: '1.0.0' }),
beforeEvaluation: async (data) => {
console.debug('[Toggly] Evaluating:', data);
},
afterEvaluation: async (data) => {
console.debug('[Toggly] Result:', data.featureKey, '=', data.result);
}
};
Performance Monitoring
const performanceHook = {
getMetadata: () => ({ name: 'PerformanceMonitor', version: '1.0.0' }),
beforeEvaluation: async (data) => {
return { startTime: performance.now() };
},
afterEvaluation: async (data) => {
const duration = performance.now() - data.context.startTime;
if (duration > 10) {
console.warn(`Slow evaluation: ${data.featureKey} took ${duration}ms`);
}
}
};
Google Analytics 4 Integration
const ga4Hook = {
getMetadata: () => ({ name: 'Google Analytics 4', version: '1.0.0' }),
afterEvaluation: async (data) => {
if (typeof gtag !== 'undefined') {
gtag('event', 'feature_flag_evaluated', {
feature_name: data.featureKey,
feature_enabled: data.result,
user_id: data.userId
});
}
}
};
See also
- WebSocket sync and definitions revision
- Evaluated-signed responses
- Reliability and Error Handling
- Client-side cache limits
- Post-filter gates
Next Steps
- Learn about React SDK
- Explore Angular SDK
- Read about Vue.js SDK
- Check out Integrations