This tutorial shows you how to build a dark mode toggle in a Confluence Forge app and control it with a feature flag. You'll use the client SDK to evaluate the flag directly in the frontend — no resolver needed.
By the end of this tutorial, you'll have a working Confluence macro that lets users switch between light and dark mode, with the ability to enable or disable the feature instantly without redeploying your app.
Feature flags are not available in Atlassian Government Cloud or FedRAMP environments. See Limitations.
This tutorial assumes you have experience with basic Forge development. If you're new to Forge, complete the hello world tutorial first.
You need:
Install the @forge/bridge package:
1 2npm install @forge/bridge@latest
Feature flags in the client SDK require @forge/bridge version 5.14.0 or later.
A Confluence macro that:
@forge/bridge) to check a feature flag in the browserThis demonstrates the client SDK pattern: flag evaluation happens entirely in the frontend, with no backend resolver involved.
If you already have a hello world Confluence app, you can use it. Otherwise, create a new one:
1 2forge create
When prompted:
dark-mode-switcherconfluence-macroUI KitYour app structure:
1 2dark-mode-switcher/ ├── manifest.yml ├── package.json └── src/ ├── index.js ├── frontend/ │ └── index.jsx └── resolvers/ └── index.js
Is Dark Mode EnabledEnable or disable dark mode in the Forge appaccountId (targets individual users)The Flag ID (is_dark_mode_enabled) is generated automatically from the name. Note this ID exactly — you'll use it in your code.
On the Setup page after confirming:
installContext for the Attribute key100% and Fail to 0%Replace your manifest.yml:
1 2# manifest.yml modules: macro: - key: switch-dark-mode resource: main render: native resolver: function: resolver title: switch-dark-mode adfExport: function: export-key function: - key: resolver handler: index.handler - key: export-key handler: macroExport.exportFunction resources: - key: main path: src/frontend/index.jsx app: runtime: name: nodejs22.x memoryMB: 256 architecture: arm64 id: ari:cloud:ecosystem::app/<your-app-id>
Replace your src/frontend/index.jsx:
1 2// src/frontend/index.jsx import React, { useEffect, useState } from "react"; import ForgeReconciler, { Box, Button, Text, Stack } from "@forge/react"; import { view, FeatureFlags } from "@forge/bridge"; const App = () => { const [refresh, setRefresh] = useState(false); const [isDarkModeEnabled, setIsDarkModeEnabled] = useState(null); const [darkUiActive, setDarkUiActive] = useState(false); const onRefresh = () => { setRefresh(true); }; const onToggleDarkMode = () => { setDarkUiActive((prev) => !prev); }; useEffect(() => { void view.theme.enable(); }, []); useEffect(() => { const initializeFeatureFlags = async () => { const { accountId, cloudId, environmentType } = await view.getContext(); const user = { attributes: { installContext: `ari:cloud:confluence::site/${cloudId}`, }, identifiers: { accountId: accountId, }, }; const config = { environment: environmentType.toLowerCase(), }; const featureFlags = new FeatureFlags(); await featureFlags.initialize(user, config); const result = featureFlags.checkFlag("is_dark_mode_enabled"); setIsDarkModeEnabled(result); if (!result) { setDarkUiActive(false); } }; initializeFeatureFlags(); setRefresh(false); }, [refresh]); if (isDarkModeEnabled === null) { return <Text>Loading…</Text>; } return ( <Box padding="space.200" backgroundColor={darkUiActive ? "color.background.neutral.bold" : undefined} > <Stack space="space.200" alignInline="center"> <Button onClick={onRefresh}>Refresh</Button> {isDarkModeEnabled ? ( <Text color={darkUiActive ? "color.text.inverse" : "color.text"}> Dark mode feature is enabled </Text> ) : ( <Text color={darkUiActive ? "color.text.inverse" : "color.text"}> Dark mode feature is disabled </Text> )} <Text color={darkUiActive ? "color.text.inverse" : "color.text"}> Dark mode UI is {darkUiActive ? "on" : "off"} </Text> {isDarkModeEnabled ? ( <Button onClick={onToggleDarkMode}> {darkUiActive ? "Switch to light mode" : "Switch to dark mode"} </Button> ) : null} </Stack> </Box> ); }; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode>, );
How this works:
view.theme.enable() activates Forge's theme support for the componentuseEffect with [refresh] dependency re-initializes the feature flag client each time the Refresh button is pressed, picking up any flag configuration changes without a full page reloadcheckFlag("is_dark_mode_enabled") returns false by default if the flag doesn't exist yet — safe to deploy before the flag is activedarkUiActive resets to false1 2forge deploy forge install --upgrade
Select your Confluence site when prompted.
/ to open the macro menuWith the flag enabled (Pass: 100%), you see:
is_dark_mode_enabled0%, Fail to 100% → Save100%, Fail to 0% → SaveFeatureFlags in @forge/bridgeRate this page: