npm install variations
# or
yarn add variations
# or
pnpm add variations
Wrap your application with the VariationsProvider
at the root level. The VariationControls
component provides a UI for switching between different variations in your app. You can style it or position it as needed.:
"use client";
import { VariationsProvider, VariationsControls } from "variations";
function RootLayout({ children }: { children: React.ReactNode }) {
return (
<VariationsProvider>
{children}
<VariationsControls position="bottom-center" />
</VariationsProvider>
);
}
By default, variations are saved to and loaded from the URL query string so that you can share variations with others. You can disable this behavior by setting disableQueryString
to true
:
<VariationsProvider disableQueryString={true}>
{children}
<VariationsControls position="bottom-center" />
</VariationsProvider>
The URL query string uses a clean, readable format:
http://localhost:3000/?var=group.id_group.id_group.id&s=base64_encoded_state
For example:
http://localhost:3000/?var=root.login-page_login-form.option-2_email-form.show&s=eyJ0aGVtZSI6eyJwcmltYXJ5Q29sb3IiOiIjMDAwIn19
Where:
.
separates a group from its ID (e.g.,login-form.option-2
)_
separates different variations (e.g.,login-form.option-2_email-form.show
)s
contains the base64-encoded JSON of your global state
This format makes it easy to share not just variations but also the entire application state with others. The state is automatically synchronized with the URL, so you can:
- Share URLs that include both variations and state
- Use browser back/forward navigation to move through state history
- Bookmark specific states of your application
You can disable this behavior by setting disableQueryString
to true
:
<VariationsProvider disableQueryString={true}>{children}</VariationsProvider>
Note for Next.js users: Variations requires the "use client"
directive to be present in the root component.
"use client";
import { Variations, Variation } from "variations";
export default function MyComponent() {
return (
<Variations label="Button Styles">
<Variation label="Primary">
<button className="bg-blue-500 text-white">Click me</button>
</Variation>
<Variation label="Secondary">
<button className="bg-gray-500 text-white">Click me</button>
</Variation>
</Variations>
);
}
You can create nested variations to explore combinations of different components:
<Variations label="Layout">
<Variation label="Sidebar">
<div className="flex">
<Variations label="Theme">
<Variation label="Light">
<div className="bg-white">{/* Content */}</div>
</Variation>
<Variation label="Dark">
<div className="bg-gray-900">{/* Content */}</div>
</Variation>
</Variations>
</div>
</Variation>
<Variation label="Top Nav">
<div className="flex flex-col">{/* Similar nested variations */}</div>
</Variation>
</Variations>
The root component that manages a set of variations.
Props:
children
: React nodes containing Variation componentsdisableQueryString
(boolean, optional): When true, disables the URL query string functionality. This prevents variations from being saved to or loaded from the URL. Defaults tofalse
The root component that manages a set of variations.
Props:
label
(string): The label for this set of variationsisRoot
(boolean, optional): When true, indicates this is the root variations component. Only one root component should exist in the tree. Defaults tofalse
children
: React nodes containing Variation components
The component that wraps each variation.
Props:
label
(string): The display name for this variationchildren
: React nodes to render when this variation is active
A pre-built UI component that provides a control panel for managing variations.
Props:
position
(string, optional): The position of the control panel. Possible values arebottom-center
,bottom-left
,bottom-right
,middle-left
,middle-right
,top-center
,top-left
,top-right
. Defaults tobottom-center
The Controls component automatically displays all registered variations and allows users to switch between them. It updates in real-time as variations are added or removed from your app. You can show and hide the controls by clicking on the Variations icon or by using the keyboard shortcut Option + v
. The controls support keyboard navigation or clicking on the controls to switch between variations.
A hook for accessing and updating global state that lives alongside your variations. This state is independent of any specific variation and can be used to store any application-wide data. The hook follows React's useState pattern, returning a tuple of the state and a setter function.
interface GlobalState {
theme: {
primaryColor: string;
fontSize: number;
};
preferences: {
showAdvancedOptions: boolean;
};
}
// First, provide initial state through the provider
function App() {
const initialState: GlobalState = {
theme: {
primaryColor: "#000000",
fontSize: 16,
},
preferences: {
showAdvancedOptions: false,
},
};
return (
<VariationsProvider initialState={initialState}>
<YourApp />
</VariationsProvider>
);
}
// Then use the state anywhere in your app
function ThemeControls() {
const [state, setState] = useVariationsState<GlobalState>();
return (
<div>
{/* Direct value update */}
<button
onClick={() =>
setState({
...state,
theme: { ...state.theme, primaryColor: "#000000" },
})
}
>
Set Dark Theme
</button>
{/* Updater function */}
<button
onClick={() =>
setState((prev) => ({
...prev,
theme: { ...prev.theme, primaryColor: "#ffffff" },
}))
}
>
Set Light Theme
</button>
{/* Toggle with updater function */}
<label>
<input
type="checkbox"
checked={state.preferences.showAdvancedOptions}
onChange={(e) => {
setState((prev) => ({
...prev,
preferences: {
...prev.preferences,
showAdvancedOptions: e.target.checked,
},
}));
}}
/>
Show Advanced Options
</label>
</div>
);
}
The global state feature provides:
- Familiar React useState-like API with support for both direct values and updater functions
- Type-safe state management with TypeScript
- Immutable state updates
- Access to state from anywhere in your app
- Independent of variations system
- Persists across variation changes
- Compact URL encoding for sharing
A hook for managing a single variation group. This is the recommended way to programmatically interact with variations.
const { active, setActive, variations } = useVariation("theme");
// active: { id: string; label: string } | null
// The currently active variation or null if none is selected
console.log(active?.label); // e.g. "Light Theme"
// variations: Array<{ id: string; label: string }>
// All available variations for this group
console.log(variations); // e.g. [{ id: 'light', label: 'Light Theme' }, ...]
// setActive: (id: string) => void
// Function to change the active variation
setActive("dark");
Example usage with a select element:
function ThemeSelector() {
const { active, setActive, variations } = useVariation("theme");
return (
<select
value={active?.id || ""}
onChange={(e) => setActive(e.target.value)}
>
{variations.map(({ id, label }) => (
<option
key={id}
value={id}
>
{label}
</option>
))}
</select>
);
}
A lower-level hook that provides full access to the variations context. Use this if you need more control over variations or need to work with multiple groups at once.
const {
// Map of group IDs to their active variation IDs
activeIds,
// Function to set the active variation for a group
setActiveId,
// Map of all registered variations
variations,
// Tree representation of active variations
activeTree,
} = useVariations();
// Example: Get active variation for a group
const activeThemeId = activeIds.get("theme");
// Example: Set active variation
setActiveId("theme", "dark");
// Example: Get all variations for a group
const themeVariations = Array.from(variations.entries()).filter(
([, variation]) => variation.group === "theme"
);
For TypeScript users, you can make the hook type-safe by providing group and ID types:
type Groups = "theme" | "layout" | "typography";
type Ids = "light" | "dark" | "sidebar" | "topnav";
const { activeIds, setActiveId } = useVariations<Groups, Ids>();
// Now TypeScript will ensure you only use valid group and variation IDs
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © Dan Perrera
The Variations library includes a built-in state management system that lives alongside your variations. This state is:
- Independent of your variations
- Globally accessible
- Type-safe with TypeScript
- Automatically synced to the URL (optional)
// First, define your state type (optional but recommended)
interface AppState {
theme: {
primaryColor: string;
fontSize: number;
};
settings: {
showAdvancedFeatures: boolean;
};
}
// Initialize the state in your root component
function App() {
const initialState: AppState = {
theme: {
primaryColor: "#000000",
fontSize: 16,
},
settings: {
showAdvancedFeatures: false,
},
};
return (
<VariationsProvider initialState={initialState}>
<YourApp />
</VariationsProvider>
);
}
// Then use the state anywhere in your app
function ThemeControls() {
const { state, setState } = useVariationsState<AppState>();
// Read from state
const { primaryColor, fontSize } = state.theme;
// Update a single value
const updateColor = (color: string) => {
setState((prev) => ({
...prev,
theme: {
...prev.theme,
primaryColor: color,
},
}));
};
// Update multiple values at once
const updateTheme = (color: string, size: number) => {
setState((prev) => ({
...prev,
theme: {
...prev.theme,
primaryColor: color,
fontSize: size,
},
}));
};
return (
<div>
<input
type="color"
value={primaryColor}
onChange={(e) => updateColor(e.target.value)}
/>
<input
type="number"
value={fontSize}
onChange={(e) => updateTheme(primaryColor, Number(e.target.value))}
/>
</div>
);
}
The state is automatically synchronized with the URL, making it easy to share specific states:
// This URL includes both variations and state
//localhost:3000/?var=theme.dark&state=%7B%22theme%22%3A%7B%22primaryColor%22%3A%22%23000%22%7D%7D
// You can disable URL synchronization if needed
http: <VariationsProvider disableQueryString={true}>
<YourApp />
</VariationsProvider>;
The state management works great alongside React's Context API:
// Create a custom hook that combines variations state with context
function useTheme() {
const { state, setState } = useVariationsState<AppState>();
const updateTheme = useCallback(
(color: string) => {
setState((prev) => ({
...prev,
theme: {
...prev.theme,
primaryColor: color,
},
}));
},
[setState]
);
return {
primaryColor: state.theme.primaryColor,
updateTheme,
};
}
// Use it in your components
function ThemeButton() {
const { primaryColor, updateTheme } = useTheme();
return <button style={{ backgroundColor: primaryColor }}>Click Me</button>;
}
- Define your state type with TypeScript for better type safety
- Use the updater function pattern to modify state
- Create custom hooks for common state operations
- Keep state updates immutable
- Consider disabling URL sync for sensitive data