- Companion Module Development: https://github.com/bitfocus/companion-module-base
- Companion Wiki: https://github.com/bitfocus/companion/wiki
- Module SDK API: https://github.com/bitfocus/companion-module-base/wiki
- Preset not appearing: Check category path format (use forward slashes:
'Category/Subcategory') - Action not triggering: Verify
actionIdmatches a valid action inactions.js - Feedback not working: Ensure
feedbackIdmatches a valid feedback infeedbacks.js - Colors incorrect: Always use
combineRgb(r, g, b)function from@companion-module/base - Button text not displaying: Check for proper escape sequences (
\\nfor line breaks in JavaScript)
- This file (
PRESETS_GUIDE.md) - Comprehensive preset creation patterns actions.js- Available action definitionsfeedbacks.js- Available feedback definitionsmain.js- Module integration example
Presets in Bitfocus Companion are pre-configured button controls that users can drag onto their control surface. They combine visual styling, actions, and optional dynamic feedback to create reusable controls.
All presets are defined in a dedicated src/presets.ts file using TypeScript:
export function UpdatePresetDefinitions(self: ModuleInstance): void {
const presets: CompanionPresetDefinitions = {
// preset definitions here
}
self.setPresetDefinitions(presets)
}// In main.ts (or main.js for JavaScript projects)
class ModuleInstance extends InstanceBase<ModuleConfig> {
async init(config: ModuleConfig): Promise<void> {
// ... initialization code ...
this.updateActions()
this.updateFeedbacks()
this.updateVariableDefinitions()
this.updatePresetDefinitions() // Called during init
}
updatePresetDefinitions(): void {
UpdatePresetDefinitions(this) // Delegates to presets.ts/presets.js
}
}Bitfocus Companion presets use the type property to define what kind of preset it is. Understanding the different type values is fundamental to creating effective presets.
| Type | Purpose | Interactive | Use Case |
|---|---|---|---|
'button' |
Clickable preset that triggers actions | Yes | Controls, switches, selections |
'text' |
Non-interactive label/header | No | Organization, section breaks |
Button presets are clickable controls that trigger actions when pressed and can show dynamic visual feedback based on system state.
presetKey: {
type: 'button', // Required: marks as interactive button
category: 'Category Name', // Groups presets in UI
name: 'Preset Display Name', // User-facing name
style: { // Visual appearance
text: 'Button Text',
size: '14', // Font size or 'auto'
color: combineRgb(255, 255, 255), // Text color
bgcolor: combineRgb(0, 0, 0), // Background color
alignment: 'center:top', // Text alignment
pngalignment: 'center:bottom', // Icon alignment
png64: 'data:image/png;base64,...' // Base64 icon
},
steps: [ // Actions to execute (REQUIRED for buttons)
{
down: [ // On button press
{
actionId: 'action_name',
options: {
option1: 'value',
// action-specific options
},
},
],
up: [], // On button release
},
],
feedbacks: [ // Optional visual feedback based on state
{
feedbackId: 'feedback_name',
options: {},
style: {
bgcolor: combineRgb(208, 179, 75),
color: combineRgb(0, 0, 0),
},
isInverted: false,
},
],
}Characteristics of Button Presets:
- Required properties:
type,category,name,style,steps - Can be pressed: Users click/tap them to execute actions
- Has visual styling: Text, colors, icons, alignment
- Can execute multiple actions: Each button can have many actions in the
downarray - Can have feedback: Visual appearance changes based on system state (optional)
- Stateful: Can reflect current status through feedbacks (optional)
Real-World Examples:
// Simple camera selection button
presets['pgm_source_cam1'] = {
type: 'button',
category: 'Switcher/Program/Sources/Cameras',
name: '1. Camera 1',
style: {
text: 'PGM\\nCAM 1',
size: 'auto',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(255, 0, 0),
},
steps: [
{
down: [
{
actionId: 'sample_action',
options: { num: 1 },
},
],
up: [],
},
],
feedbacks: [],
}
// Status-aware button with feedback
presets['toggle_rehearsal'] = {
type: 'button',
category: 'Control/Playback',
name: 'Rehearsal Mode',
style: {
text: 'REHEARSAL',
size: '14',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(0, 0, 0),
},
steps: [
{
down: [
{
actionId: 'set_rehearsal_mode',
options: { enable: true },
},
],
up: [],
},
],
feedbacks: [
{
feedbackId: 'rehearsal_active',
options: {},
style: {
bgcolor: combineRgb(255, 255, 0), // Turns yellow when active
color: combineRgb(0, 0, 0),
},
},
],
}Text presets are non-interactive labels used to organize and visually break up sections in the preset browser. They do NOT execute actions when clicked.
presetKey: {
type: 'text', // Required: marks as non-interactive text
category: 'Category Name', // Which category in the preset list
name: 'Header Display Name', // User-facing name for sorting
text: 'Header Display Text', // The actual text shown to user
}Characteristics of Text Presets:
- Required properties:
type,category,name,text - Non-clickable: Cannot be pressed or triggered
- No styling: Cannot change colors, fonts, or add icons
- No actions: No
stepsproperty (would be ignored) - No feedbacks: Cannot have dynamic visual feedback
- Simple structure: Minimal required properties
- Organization tool: Groups related buttons visually
Purpose of Text/Header Presets:
- Organize related buttons into visual groups
- Create section breaks in the preset browser
- Help users find buttons quickly by category
- Reduce clutter by grouping similar presets
- Improve UX of the preset list
Real-World Examples:
// Simple section header
presets['header_cameras'] = {
type: 'text',
category: 'Switcher/Program/Sources/Cameras',
name: 'Program Cameras', // Numbered for sorting
text: 'Program Camera Sources', // What user sees
}
// Another section header
presets['header_media'] = {
type: 'text',
category: 'Switcher/Program/Sources/Media',
name: 'Media Players',
text: 'Media Playback Sources',
}
// Continuation header (breaks up long lists)
presets['header_cameras_cont'] = {
type: 'text',
category: 'Switcher/Program/Sources/Cameras',
name: 'Additional Cameras', // Different name for sorting order
text: 'Extra Camera Sources (7-12)',
}
// Status/info header
presets['header_status'] = {
type: 'text',
category: 'System/Status',
name: 'System Status',
text: '━━━ System Information ━━━', // Can use special characters
}| Feature | Button | Text |
|---|---|---|
| Type Value | 'button' |
'text' |
| Clickable | ✅ Yes | ❌ No |
Has steps |
✅ Required | ❌ Not used |
Has style |
✅ Required | ❌ Not used |
Has feedbacks |
✅ Optional | ❌ Not supported |
Has text property |
(In style) |
✅ Required |
| Triggers actions | ✅ Yes | ❌ No |
| Shows status | ✅ Via feedbacks | ❌ Static only |
| Min. properties | 5 | 4 |
Use clear naming to distinguish types in your code:
// Headers (text type) - use 'header' prefix
presets['header_cameras'] = { type: 'text', ... }
presets['header_audio'] = { type: 'text', ... }
// Buttons - use descriptive names
presets['pgm_source_cam1'] = { type: 'button', ... }
presets['audio_mic1_mute'] = { type: 'button', ... }
presets['control_play'] = { type: 'button', ... }This makes it instantly clear what type of preset you're working with when reading the code.
| Property | Type | Purpose | Example |
|---|---|---|---|
text |
string | Button label (supports \n for line breaks) |
'F12' or 'KAM 1\nHARD' |
size |
string | Font size (number or 'auto') | '14' or '16' |
color |
number | Text color from combineRgb(r, g, b) |
combineRgb(255, 255, 255) |
bgcolor |
number | Background color from combineRgb(r, g, b) |
combineRgb(0, 0, 0) |
alignment |
string | Text position | 'center:top', 'center:center', 'left:center' |
pngalignment |
string | Icon position | 'center:bottom', 'center:center' |
png64 |
string | Base64-encoded PNG image | 'data:image/png;base64,iVBORw0...' |
Steps define what happens when the button is pressed/released:
steps: [
{
down: [ // Actions on button press (required)
{
actionId: 'action_name',
options: {
// Action-specific configuration
},
},
],
up: [], // Actions on button release (use [] for none)
},
]actionId: Must reference a valid action defined inactions.tsoptions: Configuration passed to the action (specific to each action)
Feedbacks make buttons respond visually to system state:
feedbacks: [
{
feedbackId: 'feedback_name', // Must reference valid feedback
options: {}, // Feedback-specific config
style: { // Visual changes when feedback is active
bgcolor: combineRgb(208, 179, 75),
color: combineRgb(0, 0, 0),
},
isInverted: false, // Invert feedback condition (optional)
},
]Single, unique preset definitions for specific functions.
Best for: One-off buttons that don't need repetition
start_continue: {
type: 'button',
category: 'Rundown',
name: 'Start/Continue the rundown',
style: {
text: 'F12',
size: '14',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(0, 0, 0),
},
steps: [
{
down: [
{
actionId: 'start_continue',
options: {
option: 'Default',
},
},
],
up: [],
},
],
feedbacks: [],
}Buttons that change appearance based on system state.
Best for: Controls that need to show current status
toggle_rehearsal_mode: {
type: 'button',
category: 'Rundown',
name: 'Toggle Rehearsal Mode',
style: {
text: 'Rehearsal',
size: '14',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(0, 0, 0),
},
steps: [
{
down: [
{
actionId: 'rehearsal_mode',
options: {},
},
],
up: [],
},
],
feedbacks: [
{
feedbackId: 'RehearsalStatus',
options: {},
style: {
bgcolor: combineRgb(208, 179, 75), // Gold when active
color: combineRgb(0, 0, 0),
},
},
],
}Multiple similar presets created programmatically.
Best for: Repetitive controls (cameras, sources, channels, etc.)
// Add a header for organization
presets['header_hard_cameras'] = {
category: 'Camera',
name: 'Hard Cameras',
type: 'text',
text: 'Hard Camera Presets',
}
// Generate 10 similar presets in a loop
for (let i = 1; i <= 10; i++) {
presets[`cam_hard_${i}`] = {
type: 'button',
category: 'Camera',
name: `Camera ${i}`,
style: {
text: `KAM ${i}\nHARD`,
alignment: 'center:top',
size: 16,
color: combineRgb(0, 0, 0),
bgcolor: combineRgb(128, 255, 128), // Green
pngalignment: 'center:bottom',
png64: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJv...',
},
steps: [
{
down: [
{
actionId: 'template',
options: {
type: 'Camera',
variant: `${i}HARD`,
bus: 'Program',
},
},
],
up: [],
},
],
feedbacks: [],
}
}A common pattern in broadcast control modules is to create two nearly identical preset groups that demonstrate how the loop pattern scales with minor variations.
// Header
presets['header1'] = {
category: 'Camera',
name: 'Hard Cameras',
type: 'text',
text: 'Hard Camera Presets',
}
// 10 buttons
for (let i = 1; i <= 10; i++) {
presets[`cam_hard_${i}`] = {
type: 'button',
category: 'Camera',
name: `Camera ${i}`,
style: {
text: `KAM ${i}\nHARD`,
alignment: 'center:top',
size: 16,
color: combineRgb(0, 0, 0),
bgcolor: combineRgb(128, 255, 128), // Light green
pngalignment: 'center:bottom',
png64: 'data:image/png;base64,...',
},
steps: [{
down: [{
actionId: 'template',
options: {
type: 'Camera',
variant: `${i}HARD`, // KEY DIFFERENCE
bus: 'Program',
},
}],
up: [],
}],
feedbacks: [],
}
}// Header
presets['header2'] = {
category: 'Camera',
name: 'Soft Cameras',
type: 'text',
text: 'Soft Camera Presets',
}
// 10 buttons
for (let i = 1; i <= 10; i++) {
presets[`cam_soft_${i}`] = {
type: 'button',
category: 'Camera',
name: `Camera ${i}`,
style: {
text: `KAM ${i}\nSOFT`,
alignment: 'center:top',
size: 16,
color: combineRgb(0, 0, 0),
bgcolor: combineRgb(128, 255, 128), // Light green
pngalignment: 'center:bottom',
png64: 'data:image/png;base64,...',
},
steps: [{
down: [{
actionId: 'template',
options: {
type: 'Camera',
variant: `${i}SOFT`, // KEY DIFFERENCE
bus: 'Program',
},
}],
up: [],
}],
feedbacks: [],
}
}Key Differences:
- Preset IDs:
cam_hard_${i}vscam_soft_${i} - Button text:
HARDvsSOFT - Variant parameter:
${i}HARDvs${i}SOFT - Everything else is identical (styling, icon, action type, bus)
Result: 20 total buttons (10 hard + 10 soft) from minimal code
Use consistent colors for different preset types:
// Camera buttons - green
bgcolor: combineRgb(128, 255, 128)
// External sources - red
bgcolor: combineRgb(155, 0, 0)
// Default/neutral - black
bgcolor: combineRgb(0, 0, 0)
// Active/feedback state - gold
bgcolor: combineRgb(208, 179, 75)Group related presets using text headers:
presets['header_cameras'] = {
category: 'Camera',
name: 'Cameras',
type: 'text',
text: 'Camera Presets',
}
presets['header_external'] = {
category: 'External',
name: 'External Sources',
type: 'text',
text: 'External Source Presets',
}Use clear, consistent preset IDs:
// Good:
presets['cam_hard_1'] // Type_variant_number
presets['external_source_1'] // Type_description_number
presets['status_connected'] // Status_state
// Less clear:
presets['button_1']
presets['preset_a']
presets['thing_1']Use \n for line breaks to fit more info on small buttons:
text: `KAM ${i}\nHARD` // Displays as two lines
text: `Camera\n${i}` // Another formatWhen using line breaks, position text carefully:
alignment: 'center:top' // Number at top, text at bottom
alignment: 'center:center' // Balanced for single lineBe explicit about unused properties:
steps: [{ down: [...], up: [] }] // No action on release
feedbacks: [] // No dynamic feedbackUse loops instead of copying/pasting:
// Good: 10 cameras in ~15 lines
for (let i = 1; i <= 10; i++) {
presets[`cam_${i}`] = { ... }
}
// Bad: 150+ lines for 10 nearly identical presets
presets['cam_1'] = { ... }
presets['cam_2'] = { ... }
// ... repeat 8 more timesinterface ButtonPreset {
type: 'button' // Required
category: string // Required - organization
name: string // Required - display name
style: {
text: string // Optional but recommended
size: string // Optional
color: number // Optional (text color)
bgcolor: number // Optional (background)
alignment?: string // Optional
pngalignment?: string // Optional (for icons)
png64?: string // Optional (base64 icon)
}
steps: Array<{ // Required
down: Array<ActionDefinition> // Required
up: Array<ActionDefinition> // Required
}>
feedbacks: Array<{ // Required (can be empty)
feedbackId: string
options: any
style: any
isInverted?: boolean
}>
}interface TextPreset {
type: 'text' // Required
category: string // Required
name: string // Required
text: string // Required - display text
}Example categories commonly used in broadcast control modules (create your own as needed):
'Camera'- Camera selection presets'Switcher/Program'- Program bus control'Switcher/Preview'- Preview bus control'Audio/Inputs'- Audio input controls'Audio/Outputs'- Audio output controls'Control/Transport'- Playback transport controls'Status'- Status indicators'Recall'- Memory recall functions
Category Naming Best Practices:
- Use forward slashes (
/) for hierarchical organization - Keep category names clear and descriptive
- Group related functionality together
- Consider operator workflows when organizing
| Scenario | Pattern | Example |
|---|---|---|
| Single control button | Static | start_continue, stop |
| Button with status feedback | With Feedback | rehearsal_mode, automation_status |
| Multiple similar buttons | Loop Generated | Camera 1-10, Sources 1-8 |
| Section organization | Text Header | "Camera Presets", "External Sources" |
| Grouped controls | Header + Loop | Header + 10 camera buttons |
When implementing presets in your module:
- Create
src/presets.tsfile - Export
UpdatePresetDefinitionsfunction - Call
self.setPresetDefinitions(presets)at end - Reference only valid
actionIdvalues fromactions.ts - Reference only valid
feedbackIdvalues fromfeedbacks.ts - Use
combineRgb()for all color values - Add text headers for organization
- Use loops for repetitive presets
- Test that presets appear in Companion UI
- Verify actions are triggered correctly
- Confirm feedbacks update button appearance
This section documents how the camera preset system was implemented in practice, showing how to scale from a basic preset to a full organizational structure.
Start by adding a header to organize the first group of cameras:
presets['header_pgm_cameras'] = {
category: 'Switcher/Program/Sources/Cameras',
name: 'Program Cameras',
type: 'text',
text: 'Program Camera Sources',
}Key points:
type: 'text'marks this as a non-interactive headercategoryplaces it in the preset browsernameis the sortable identifier (useful for ordering)textis what users see
Create the initial preset buttons. For cameras 1 and 2, define them individually. Then, for cameras 3-6, use the same pattern:
presets['pgm_source_cam3'] = {
type: 'button',
category: 'Switcher/Program/Sources/Cameras',
name: '3. Camera 3', // Numbered for sorting
style: {
text: 'PGM\\nCAM 3', // Multi-line with \n
size: 'auto',
color: combineRgb(255, 255, 255), // White text
bgcolor: combineRgb(255, 0, 0), // Red background
},
steps: [
{
down: [
{
actionId: 'sample_action',
options: {
num: 3, // Sequential numbering
},
},
],
up: [],
},
],
feedbacks: [],
}Pattern to follow:
- Preset ID:
pgm_source_cam${number}- consistent naming - Name: Start with number for automatic sorting in preset list
- Text: Use
\\nfor line breaks to fit more info - Action options: Pass the camera number to differentiate behavior
- Sequential IDs: Easy to find and modify later
After the first 6 cameras, add a visual break with a second header:
presets['header_pgm_cameras_2'] = {
category: 'Switcher/Program/Sources/Cameras',
name: 'Program Cameras (Continued)', // Different name for sorting
type: 'text',
text: 'Additional Camera Sources',
}Why a second header?
- Breaks up large lists visually in the preset browser
- Helps users understand the preset organization
- Can group related functionality (e.g., "Hardware Cameras" vs "Virtual Cameras")
Repeat the button pattern for cameras 7-12 with the same structure:
presets['pgm_source_cam7'] = {
type: 'button',
category: 'Switcher/Program/Sources/Cameras',
name: '7. Camera 7',
style: {
text: 'PGM\\nCAM 7',
size: 'auto',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(255, 0, 0),
},
steps: [
{
down: [
{
actionId: 'sample_action',
options: {
num: 7,
},
},
],
up: [],
},
],
feedbacks: [],
}
// ... repeat for cameras 8-12 with same patternpresets['header_pgm_cameras'] // Header 1
↓
presets['pgm_source_cam1-6'] // Buttons 1-6
↓
presets['header_pgm_cameras_2'] // Header 2
↓
presets['pgm_source_cam7-12'] // Buttons 7-12
↓
this.setPresetDefinitions(presets) // Register all at end
Total code for 12 cameras + 2 headers:
- ~350 lines of JavaScript
- Highly repetitive with only 3 changes per button:
- Camera number in ID:
cam1,cam2, etc. - Camera number in text:
CAM 1,CAM 2, etc. - Camera number in action options:
num: 1,num: 2, etc.
- Camera number in ID:
How to optimize this further:
Use a loop to generate cameras programmatically:
// Header
presets['header_pgm_cameras'] = {
category: 'Switcher/Program/Sources/Cameras',
name: 'Program Cameras',
type: 'text',
text: 'Program Camera Sources',
}
// Generate cameras 1-6 dynamically
for (let i = 1; i <= 6; i++) {
presets[`pgm_source_cam${i}`] = {
type: 'button',
category: 'Switcher/Program/Sources/Cameras',
name: `${i}. Camera ${i}`,
style: {
text: `PGM\\nCAM ${i}`,
size: 'auto',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(255, 0, 0),
},
steps: [
{
down: [
{
actionId: 'sample_action',
options: {
num: i,
},
},
],
up: [],
},
],
feedbacks: [],
}
}
// Second header
presets['header_pgm_cameras_2'] = {
category: 'Switcher/Program/Sources/Cameras',
name: 'Program Cameras (Continued)',
type: 'text',
text: 'Additional Camera Sources',
}
// Generate cameras 7-12 dynamically
for (let i = 7; i <= 12; i++) {
presets[`pgm_source_cam${i}`] = {
type: 'button',
category: 'Switcher/Program/Sources/Cameras',
name: `${i}. Camera ${i}`,
style: {
text: `PGM\\nCAM ${i}`,
size: 'auto',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(255, 0, 0),
},
steps: [
{
down: [
{
actionId: 'sample_action',
options: {
num: i,
},
},
],
up: [],
},
],
feedbacks: [],
}
}Optimization Result:
- Reduces 350 lines → ~50 lines
- Easy to scale to 20, 50, or 100 cameras
- Single point of change for styling
The implementation uses a specific naming strategy:
| Element | Format | Example | Purpose |
|---|---|---|---|
| Preset ID | pgm_source_cam${i} |
pgm_source_cam1 |
Unique identifier |
| Display Name | ${i}. Camera ${i} |
1. Camera 1 |
Sortable in UI |
| Button Text | PGM\nCAM ${i} |
PGM\nCAM 1 |
User-facing label |
| Action Option | num: ${i} |
num: 1 |
Differentiates action |
Why this matters:
- Preset ID: Functional naming (pgm=program, source, camera type)
- Display Name: Number prefix ensures alphabetical sorting works correctly
- Button Text: Brief, operator-friendly label (KAM, CAM, etc.)
- Action Option: Links the button to specific behavior
When expanding from 12 to more cameras:
-
Under 10 cameras per section: Keep in one group
-
10-20 cameras: Use headers to break into sections
-
20+ cameras per section: Consider sub-categories
category: 'Switcher/Program/Sources/Cameras/Hardwired' category: 'Switcher/Program/Sources/Cameras/Robotic' category: 'Switcher/Program/Sources/Cameras/Virtual'
After implementing camera presets:
- Headers appear in correct category
- All 12 buttons visible in preset browser
- Buttons display correct numbering (1-12)
- Text renders properly with line breaks
- Colors match design (red background, white text)
- Clicking a button triggers the correct action
- Action receives correct option number
Presets follow a predictable, scalable pattern:
- Static presets for unique controls
- Feedback presets for state-aware buttons
- Loops for repetitive controls
- Headers for organization
By combining these patterns, you can create powerful, organized preset libraries that make your Companion module intuitive and efficient to use. The camera preset example demonstrates how this works in practice: starting with simple button definitions, organizing with headers, and scaling to larger numbers using loops and consistent naming conventions.