This guide explains how to properly integrate Bravura SDK themes with premium components, covering theme architecture, automatic widget styling, premium button integration, and command preservation across theme changes.
What You'll Learn:
---
---
| Theme | Description | Best For |
|---|---|---|
| default | Standard Light | Daytime use, professional look |
| dark | Standard Dark | Reduced eye strain |
| black | Standard Black | OLED displays, night mode |
| white | Standard White | Maximum clarity |
| blue_deep | Standard Blue | Corporate applications |
| pink_professional | Standard Pink | Modern, elegant design |
| ocean | Standard Ocean | Calming, professional |
| Theme | Tier | Features |
|---|---|---|
| wigley_site | Professional+ | Custom teal/cyan, animated background, premium buttons |
| platinum | Enterprise | Metallic silver, shimmer effects, luxury appearance |
| custom | Enterprise | Fully customizable brand colors, white-label ready |
---
ProfessionalThemeManager
├── Standard Themes (all tiers)
│ ├── default, dark, black, white
│ ├── blue_deep, pink_professional, ocean
│ └── Provide: base colors, button styles, widget styling
│
└── Premium Themes (Enterprise tier)
├── wigley_site, platinum, custom
└── Provide: everything above + ambient backgrounds + glass panels
For custom branding: See Custom Branding Theme Guide for complete documentation on customizing the custom theme with your company's brand colors.
All Bravura themes now include specialized high-contrast panel background colors for improved visual hierarchy:
| Token | Purpose | Default (Light) | Usage Example |
|---|---|---|---|
| `panel_input_bg` | Input/settings panels | `#E8E8E8` | Form sections, configuration areas |
| `panel_progress_bg` | Progress tracking panels | `#F5F5F5` | Status updates, progress bars |
| `panel_results_bg` | Results display panels | `#ECECEC` | Output data, analytics displays |
Benefits:
Example Usage:
# Get theme colors dynamically
theme_colors = self.theme_manager.get_current_theme_colors() if self.theme_manager else {}
# Apply to input panel
input_frame = tk.Frame(
parent,
bg=theme_colors.get('panel_input_bg', '#E8E8E8'),
relief="flat",
bd=1
)
# Apply to progress panel
progress_frame = tk.Frame(
parent,
bg=theme_colors.get('panel_progress_bg', '#F5F5F5'),
relief="flat",
bd=1
)
# Apply to results panel
results_frame = tk.Frame(
parent,
bg=theme_colors.get('panel_results_bg', '#ECECEC'),
relief="flat",
bd=1
)
| Component | Works with Standard Themes | Works with Premium Themes |
|---|---|---|
| `PremiumButton` | ✅ Yes (with tokens) | ✅ Yes |
| `CompactPremiumButton` | ✅ Yes (with tokens) | ✅ Yes |
| `Toast` (Standard) | ✅ Yes (theme-aware) | ✅ Yes (theme-aware) |
| `Toast` (Premium) | ⚠️ Pro/Enterprise only | ✅ Yes (glass panel) |
| `AmbientBackground` | ⚠️ Manual only | ✅ Auto-created |
| `GlassPanel` | ⚠️ Manual only | ✅ Auto-created |
| `ThemeManager` | ✅ Yes | ✅ Yes |
---
Premium buttons use a 3-tier color resolution system:
#22C55E for success buttonshover_bg, accent_color_1)
# Button color resolution order:
# 1. Check if color is already hex (e.g., "#22C55E") → use directly
# 2. Check theme tokens for key (e.g., "hover_bg") → use if found
# 3. Calculate from related colors (e.g., darken button_bg for hover)
# 4. Use neutral gray fallback (NOT teal, as of v2.0.1)
PremiumButton(parent, text="Save", style="primary")
# Requires tokens:
# - accent_color_1 → background color
# - button_fg → text color
# - hover_bg → hover background
# - active_bg → click background
PremiumButton(parent, text="Cancel", style="secondary")
# Requires tokens:
# - background_secondary → background color
# - accent_color_1 → text color
# - hover_bg → hover background
# - active_bg → click background
PremiumButton(parent, text="Confirm", style="success")
# Uses FIXED hex colors - no theme tokens needed
# Success: #22C55E, Danger: #EF4444, Warning: #F59E0B
---
Toast notifications automatically adapt to the active theme by pulling colors from theme tokens:
# Toast notification color resolution:
# 1. Background → theme's "background_secondary" token
# 2. Text → theme's "text_main" and "text_secondary" tokens
# 3. Border → theme's "border_color" token
# 4. Info icon → theme's "accent_color_1" token
# 5. Success icon → theme's "success_color" token
# 6. Warning icon → theme's "warning_color" token
# 7. Error icon → theme's "error_color" token
from bravura.components import create_toast_center
# Create toast center with theme colors
theme_colors = theme_manager.get_current_theme_colors()
toast_center = create_toast_center(root, theme_colors)
# Show notification - automatically uses theme colors
toast_center.show("Operation complete", kind="success")
# Result: Background from theme, success icon color from theme
# Create with premium style
toast_center = create_toast_center(
root,
theme_colors,
default_style="premium" # Glass panel effect
)
# Show premium toast
toast_center.show("Premium notification", kind="info", style="premium")
# Result: Frosted glass panel with theme-colored accents
Toast notifications created after a theme change automatically use the new theme colors:
def switch_theme(self, new_theme):
# Apply new theme
self.theme_manager.apply_theme(new_theme)
# Recreate toast center with new theme colors
self.toast_center = create_toast_center(
self.root,
self.theme_manager.get_current_theme_colors()
)
# New toasts now use new theme
self.toast_center.show("Theme changed!", kind="success")
Important: Existing visible toasts retain their original theme colors. Only new toasts use the updated theme.
---
from bravura.themes.theme_manager import create_theme_manager
class MyApp:
def __init__(self):
self.root = tk.Tk()
# Initialize theme manager FIRST
self.theme_manager = create_theme_manager(self)
# Create UI
self.create_ui()
# Apply theme
self.theme_manager.apply_theme("wigley_site")
def switch_theme(self, theme_name):
"""Switch to a different theme."""
self.theme_manager.apply_theme(theme_name)
# Theme preference is automatically saved
# Get current theme name
current = self.theme_manager.current_theme_name
# Get theme color palette
colors = self.theme_manager.get_current_theme_colors()
# Get available themes (license-aware)
available = self.theme_manager.available_themes
---
For most applications, use the automatic theme system:
import tkinter as tk
from bravura import ProfessionalThemeManager, PremiumButton
class MyApp:
def __init__(self):
self.root = tk.Tk()
# Step 1: Initialize theme manager FIRST
self.theme_manager = ProfessionalThemeManager(
self,
config_file="gui_config.json"
)
# Step 2: CRITICAL - Store reference on root for button token lookup
# Without this line, buttons will use fallback colors (gray/blue)
self.root.theme_manager = self.theme_manager
# Step 3: Create GUI after theme manager
self.create_gui()
def create_gui(self):
# Buttons automatically get tokens from theme_manager
save_btn = PremiumButton(
self.root,
text="Save",
style="primary"
)
save_btn.pack()
Key Points:
ProfessionalThemeManager BEFORE creating widgetstheme_manager reference on root widgettheme_manager and get fresh tokensIf you need explicit control over tokens:
# Get current theme colors
theme_colors = self.theme_manager.get_current_theme_colors()
# Pass to button manually
btn = PremiumButton(
parent,
text="Action",
style="primary",
theme_tokens=theme_colors
)
Warning: Manual tokens don't update when themes change. Use Pattern 1 for dynamic theming.
---
When you call theme_manager.apply_theme(theme_name):
- Loads new color palette from PROFESSIONAL_THEMES
- Updates TTK styles (Button.TButton, TNotebook.Tab, TCombobox, etc.)
- Configures widget colors (Entry, Text, Listbox, Treeview)
- Premium themes: Destroys old ambient background, creates new one with theme colors
- Standard themes: No ambient background (premium feature)
- Buttons do NOT automatically refresh colors on theme change
- Buttons created AFTER theme change get new colors
- Existing buttons keep their initialization colors
- tk.Frame, tk.Label, and other custom widgets need manual refresh
- Recommended pattern: store widget references and update in refresh_theme_colors() method
- High-contrast panel colors must be manually applied on theme change
# CORRECT: Create buttons after applying theme
self.theme_manager.apply_theme("blue_deep")
self.create_buttons() # Buttons get blue_deep colors
# INCORRECT: Create buttons before theme change
self.create_buttons() # Buttons get default colors
self.theme_manager.apply_theme("blue_deep") # Buttons DON'T update
For applications that allow runtime theme switching:
def switch_theme(self, new_theme):
# Apply the new theme
self.theme_manager.apply_theme(new_theme)
# Option 1: Recreate UI (simple but disruptive)
self.destroy_gui()
self.create_gui()
# Option 2: Update individual buttons (complex but smooth)
for button in self.all_buttons:
if hasattr(button, '_refresh_colors_from_current_theme'):
button._refresh_colors_from_current_theme()
# Option 3: Update stored widgets with refresh method (RECOMMENDED)
self.refresh_theme_colors()
The Admin Dashboard Pattern provides the most robust theme switching:
class MyApp:
def __init__(self):
self.root = tk.Tk()
self.theme_manager = ProfessionalThemeManager(self)
# Dictionary to store all themed widgets for updates
self.themed_widgets = {}
self.create_ui()
def create_ui(self):
"""Create UI and store widget references."""
theme_colors = self.theme_manager.get_current_theme_colors()
# Create panel with high-contrast color
input_panel = tk.Frame(
self.root,
bg=theme_colors.get('panel_input_bg', '#E8E8E8'),
relief="flat",
bd=1
)
input_panel.grid(row=0, column=0, sticky="nsew")
# Store reference for theme updates
self.themed_widgets['input_panel'] = input_panel
# Create button and store reference
action_btn = tk.Button(
input_panel,
text="Process",
bg=theme_colors.get('accent_color_1', '#0078d7'),
fg="white",
cursor="hand2"
)
action_btn.pack(pady=10)
self._add_button_hover(action_btn)
self.themed_widgets['action_btn'] = action_btn
def apply_theme(self, theme_id: str):
"""Apply theme and refresh all widgets."""
if self.theme_manager:
self.theme_manager.apply_theme(theme_id)
self.refresh_theme_colors()
def refresh_theme_colors(self):
"""Refresh all widget colors after theme change."""
if not self.theme_manager:
return
# Get new theme colors dynamically
theme_colors = self.theme_manager.get_current_theme_colors()
# Update all stored widgets
for widget_name, widget in self.themed_widgets.items():
if not widget or not widget.winfo_exists():
continue
try:
# Update frames/containers
if 'panel' in widget_name or 'frame' in widget_name:
if 'input' in widget_name:
widget.configure(bg=theme_colors.get('panel_input_bg', '#E8E8E8'))
elif 'progress' in widget_name:
widget.configure(bg=theme_colors.get('panel_progress_bg', '#F5F5F5'))
elif 'results' in widget_name:
widget.configure(bg=theme_colors.get('panel_results_bg', '#ECECEC'))
# Update buttons - recalculate hover colors
elif 'btn' in widget_name or 'button' in widget_name:
new_bg = theme_colors.get('accent_color_1', '#0078d7')
widget.configure(bg=new_bg)
# Recalculate hover colors dynamically
widget._original_bg = new_bg
widget._hover_bg = self._calculate_hover_color(new_bg)
# Update labels
elif 'label' in widget_name:
parent_bg = theme_colors.get('panel_input_bg', '#E8E8E8')
widget.configure(
bg=parent_bg,
fg=theme_colors.get('text_main', '#000000')
)
except Exception as e:
print(f"Warning: Could not update {widget_name}: {e}")
# Update root window
if self.root:
self.root.configure(bg=theme_colors.get('background_primary', '#F0F0F0'))
def _calculate_hover_color(self, base_color):
"""Calculate a lighter hover color from base color."""
try:
# Remove # if present
hex_color = base_color.lstrip('#')
# Convert to RGB
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
# Make it lighter (increase each channel by 15%)
r = min(255, int(r * 1.15))
g = min(255, int(g * 1.15))
b = min(255, int(b * 1.15))
return f'#{r:02x}{g:02x}{b:02x}'
except:
return base_color
def _add_button_hover(self, button):
"""Add theme-aware hover effect to tk.Button widgets."""
original_color = button.cget("bg")
button._original_bg = original_color
button._hover_bg = self._calculate_hover_color(original_color)
def on_enter(e):
if button["state"] != "disabled":
button["bg"] = button._hover_bg
def on_leave(e):
if button["state"] != "disabled":
button["bg"] = button._original_bg
button.bind("<Enter>", on_enter)
button.bind("<Leave>", on_leave)
Key Benefits of This Pattern:
When to Use:
---
Premium themes automatically create ambient backgrounds:
# Apply premium theme
self.theme_manager.apply_theme("wigley_site")
# → Ambient background created automatically
# → Available as self.theme_manager.wigley_ambient
self.theme_manager.apply_theme("platinum")
# → Old ambient destroyed, new one created
# → Available as self.theme_manager.platinum_ambient
To add ambient background to standard themes:
from bravura.components import AmbientBackground
# Get current theme colors
theme_colors = self.theme_manager.get_current_theme_colors()
# Create ambient background with theme colors
self.ambient_bg = AmbientBackground(
parent_widget=self.root,
width=800,
height=600,
theme_tokens=theme_colors, # Pass theme colors here!
enable_animation=True,
animation_speed="medium",
particle_count=20,
glow_effects=True
)
# Store reference for cleanup
self.ambient_backgrounds.append(self.ambient_bg)
Important: Ambient backgrounds do NOT auto-update on theme changes. You must destroy and recreate them.
# Create
ambient = AmbientBackground(
parent_widget=parent,
theme_tokens=theme_manager.get_current_theme_colors()
)
# When theme changes, destroy old and create new
old_ambient.destroy()
new_ambient = AmbientBackground(
parent_widget=parent,
theme_tokens=theme_manager.get_current_theme_colors() # New theme colors!
)
# Clean up reference
del old_ambient
def switch_theme(self, new_theme):
# Apply the new theme
self.theme_manager.apply_theme(new_theme)
# Destroy old ambient background (if exists)
if hasattr(self, 'ambient_bg') and self.ambient_bg:
try:
self.ambient_bg.destroy()
except Exception as e:
print(f"Warning: Could not destroy ambient background: {e}")
# Create new ambient background with new theme colors
self.ambient_bg = AmbientBackground(
parent_widget=self.root,
width=self.root.winfo_width(),
height=self.root.winfo_height(),
theme_tokens=self.theme_manager.get_current_theme_colors(), # New theme!
enable_animation=True,
animation_speed="medium",
particle_count=20,
glow_effects=True
)
---
# WRONG
self.create_buttons() # Buttons have no theme context
self.theme_manager = ProfessionalThemeManager(self) # Too late!
# RIGHT
self.theme_manager = ProfessionalThemeManager(self) # Theme first!
self.root.theme_manager = self.theme_manager # Store reference
self.create_buttons() # Buttons can find theme_manager
# WRONG EXPECTATION
btn = PremiumButton(parent, text="Save", style="primary")
self.theme_manager.apply_theme("dark") # Button colors DON'T change
# CORRECT APPROACH
self.theme_manager.apply_theme("dark")
btn = PremiumButton(parent, text="Save", style="primary") # New button with dark colors
# WRONG - Standard themes don't auto-create ambient backgrounds
self.theme_manager.apply_theme("dark")
# No ambient background created
# RIGHT - Manually create if needed
self.theme_manager.apply_theme("dark")
theme_colors = self.theme_manager.get_current_theme_colors()
self.ambient_bg = create_ambient_background(self.root, theme_colors)
Symptom: Buttons appear gray/blue instead of theme colors, turn gray on hover
# WRONG - Buttons can't find theme_manager
self.theme_manager = ProfessionalThemeManager(self)
btn = PremiumButton(self.root, text="Save")
# Result: Gray button (#444444) instead of theme color
# Hover: Turns gray because fallback system activates
# RIGHT - Store reference for button lookup
self.theme_manager = ProfessionalThemeManager(self)
self.root.theme_manager = self.theme_manager # CRITICAL LINE!
btn = PremiumButton(self.root, text="Save")
# Result: Proper theme colors (teal, blue, etc.)
# Hover: Proper theme hover colors
Why This Happens:
parent.theme_managerself.master, self.master.master, etc. up to 20 levels---
Prior to v2.0.1, buttons would permanently change to teal on hover in standard themes. This was caused by:
background_primary as hover colorhover_bg and active_bg tokens (same as primary)hover_bg is missing, darkens button_bg by 10%If you have custom themes or manual button styling:
# OLD (pre-v2.0.1) - could cause teal hover issue
btn = PremiumButton(parent, text="Cancel", style="secondary")
# If background_primary was missing → fell back to teal
# NEW (v2.0.1+) - works reliably
btn = PremiumButton(parent, text="Cancel", style="secondary")
# If hover_bg is missing → calculates from button_bg
# Final fallback is neutral gray, not teal
---
theme_manager reference on root widget
# Add this to debug button colors
btn = PremiumButton(parent, text="Test", style="primary")
print(f"Button bg: {btn.bg_normal}")
print(f"Hover bg: {btn.bg_hover}")
print(f"Active bg: {btn.bg_active}")
print(f"Tokens: {btn.tokens}")
Expected output with working theme:
Button bg: #0078d7 (or theme-specific color)
Hover bg: #0091ff (slightly lighter/darker than button_bg)
Active bg: #005fa3 (even more contrast)
Tokens: {'accent_color_1': '#0078d7', 'hover_bg': '#0091ff', ...}
---
# Add to PROFESSIONAL_THEMES in theme_manager.py
PROFESSIONAL_THEMES["my_custom_theme"] = {
"name": "My Custom Theme",
"description": "Custom branding for my app",
"tier": "enterprise", # or "standard"
# REQUIRED tokens for buttons:
"button_bg": "#4F46E5",
"button_fg": "#FFFFFF",
"hover_bg": "#6366F1",
"active_bg": "#3730A3",
# REQUIRED tokens for general UI:
"text_main": "#FFFFFF",
"text_secondary": "#E2E8F0",
"background_primary": "#1E293B",
"background_secondary": "#334155",
"accent_color_1": "#4F46E5",
"accent_color_2": "#06B6D4",
# Optional tokens:
"slider_trough": "#334155",
"log_bg": "#0F172A",
"log_fg": "#06B6D4",
"border_color": "#4F46E5",
"success_color": "#059669",
"warning_color": "#D97706",
"error_color": "#DC2626",
}
For enterprise-tier custom themes, add ambient background support:
# In ProfessionalThemeManager.apply_theme()
elif theme_id == "my_custom_theme":
colors = PROFESSIONAL_THEMES["my_custom_theme"]
self._apply_base_widget_styles(colors)
# Create custom ambient background
custom_tokens = {
"bg_ink": colors["background_primary"],
"teal": colors["accent_color_1"],
"glow_teal": f"rgba{self._hex_to_rgba(colors['accent_color_1'], 0.08)}",
}
self.custom_ambient = create_ambient_background(
self.app.root, custom_tokens, enable_animation=True
)
---
---
---
Last Updated: 2025-10-07
Version: 2.0
Applies to: Bravura v2.0.0+