Template repository for building Condenser plugins. Fork or use this as a GitHub template to create your own plugin.
Condenser customizes Steam Big Picture Mode and SteamOS using plugins, on any platform (Windows, macOS, Linux, Steam Deck). Plugins are small React + Node.js modules injected into Steam's browser UI.
- Node.js 24 LTS — required by condenser-app and this template (
nvm use 24or nodejs.org)
git clone https://github.com/condenser-team/condenser-app
git clone https://github.com/your-username/my-pluginBoth repos must be siblings in the same parent directory so the dev script can find condenser-app automatically.
cd my-plugin
npm installEdit package.json and set name to your plugin's slug (e.g. my-plugin). Then update frontend.tsx:
export const key = 'my-plugin'; // must match the package name and directory name
export const title = 'My Plugin';
export const route = '/my-plugin/home';npm run devThis finds condenser-app in the sibling directory and starts the condenser-app dev server with your plugin loaded and hot-reloaded automatically. The plugin repo must be a sibling of condenser-app so the directory name is used as the plugin ID — make sure it matches the key export in frontend.tsx.
If condenser-app is not a sibling, set the path explicitly:
CONDENSER_APP_PATH=/path/to/condenser-app npm run devmy-plugin/
├── frontend.tsx # React UI injected into Steam's Big Picture Mode
├── backend.ts # Node.js backend — exports become RPC actions
├── types/
│ └── condenser.d.ts # TypeScript global for window.condenser (from condenser-app)
└── scripts/
├── dev.mjs # Starts condenser-app with this plugin loaded
└── build.mjs # Standalone production build (used by release workflow)
Export any combination of these from frontend.tsx:
| Export | Description |
|---|---|
Page |
Full-screen page opened from the BPM home screen |
Tab |
Icon shown in Steam's quick-access tab bar |
Panel |
Content inside the quick-access panel |
Persistent |
Always-visible overlay rendered on every screen |
Condenser injects a condenser global into Steam's browser context before any plugin loads. Access the full API by destructuring from it — no imports needed.
// At module top-level (outside any component)
const { navigate, back } = condenser.nav;
const { showToast, showModal, Focusable } = condenser.ui;
const { createStyleToggle } = condenser.css;
// Inside a component (React hook)
const send = condenser.plugin.useSend(key);TypeScript types are provided automatically via types/condenser.d.ts, which imports CondenserNamespace directly from condenser-app so types stay in sync without manual maintenance.
Every exported async function in backend.ts becomes a callable action. Call them from the frontend with condenser.plugin.useSend:
// frontend.tsx
const send = condenser.plugin.useSend('my-plugin');
const result = await send('myAction', { value: 42 });
// backend.ts
export async function myAction(data: { value: number }) {
return { result: data.value * 2 };
}Export onMount and onUnmount alongside your surfaces. Condenser calls them automatically — onUnmount is guaranteed to fire before any disable or hot-reload.
// frontend.tsx
export function onMount(): void {
// plugin enabled or first load
}
export function onUnmount(): void {
// plugin disabled or hot-reloaded — clean up CSS, patches, timers
style.disable();
}// backend.ts
export async function onLoad(api: BackendAPI) {
// called when the plugin is loaded — start timers, open connections, etc.
}
export async function onUnload() {
// called when condenser shuts down
}Use condenser.css to inject styles into Steam windows. Styles are always JavaScript objects — never raw CSS strings.
Two source formats are accepted:
StyleProperties— flat camelCase property bag applied to the target element
{ borderRadius: '10px', color: 'white' }StyleSheet— map of CSS selector → property bag; selectors are automatically scoped when a section target is used
{ '.Panel': { borderRadius: '10px' }, '.Header': { color: 'white' } }
const { inject, createStyleToggle, createStyleVars, Target } = condenser.css;
// createStyleToggle — recommended for enable/disable patterns
const style = createStyleToggle(
'my-plugin',
{ outline: '3px solid #ff6b6b', outlineOffset: '-3px' },
{ window: Target.BigPicture, scope: '#header' }, // stable cross-platform selector
);
style.enable();
style.disable();
console.log(style.enabled); // boolean
export function onUnmount() { style.disable(); }
// inject — one-shot, returns a cleanup function
const remove = inject('my-plugin',
{ fontFamily: 'Inter, sans-serif' },
Target.Global, // BigPicture + MainMenu + QuickAccess
);
// later: remove();
// createStyleVars — live-updatable CSS custom properties
const vars = createStyleVars('my-plugin', { '--accent': '#4fc3f7' }, Target.BigPicture);
vars.update({ '--accent': '#ff6b6b' });
vars.remove();Targets — pass as the third argument to any CSS function:
| Target | Scope |
|---|---|
Target.BigPicture |
Main BPM window |
Target.QuickAccess |
Quick Access Menu popup |
Target.MainMenu |
Steam button overlay |
Target.Global |
BigPicture + MainMenu + QuickAccess |
Target.Library, Target.Home, Target.Settings, … |
Section-scoped (SteamOS only) |
{ window: Target.BigPicture, scope: '#Main' } |
Custom selector — works on all platforms |
Injected <style> elements are tagged with data-condenser-plugin="my-plugin" for easy DevTools inspection.
npm run build
# produces dist/frontend.js and dist/backend.mjsnpm version minor # or patch / major
git push origin --tagsThe GitHub Actions release workflow (.github/workflows/release.yml) will:
- Build the plugin
- Create a
.zipcontainingfrontend.jsandbackend.mjs - Compute a SHA256 of the zip
- Publish a GitHub Release with the zip attached
- Print the exact YAML snippet to paste into your registry submission
Open a pull request to condenser-registry adding your plugin under resources/plugins/your-plugin/. Use the YAML snippet printed in the release notes and follow the PR template checklist.
Once a maintainer merges your PR, your plugin appears in the Condenser plugin browser and can be installed by any user.
See types/condenser.d.ts for the full typed API — it re-exports CondenserNamespace directly from condenser-app so it always reflects the real implementation.
Key namespaces:
| Namespace | Description |
|---|---|
condenser.nav |
navigate, back, openQAM, openSideMenu, closeSideMenus |
condenser.plugin |
useSend(pluginId), useMessage(pluginId, event, handler) |
condenser.ui |
showToast, showModal, showContextMenu, Focusable, SidebarNavigation, Tabs, Menu, MenuItem |
condenser.css |
inject, createStyleToggle, createStyleVars, Target |
condenser.steam |
classes (resolved CSS class names), resetClasses |
condenser.events |
UIMode, getUIMode, onUIModeChanged, useQAMVisible |
MIT