| name | embed-themes |
| description | This skill enables agents to assist users in programmatically creating, updating, and managing Looker themes using the Looker API. Use this when you need to automate visual styling, implement brand-specific themes, or manage instance-wide default themes. |
Looker Themes API
This skill enables agents to assist users in programmatically managing the visual appearance of Looker dashboards and reports using the Theme resource in the Looker API.
Overview
Looker themes allow you to control the styling of embedded and internal Looker content, including colors, fonts, and layout options. Using the API, you can:
- Create Themes: Define new visual styles for specific dashboards or the entire instance.
- Update Themes: Modify existing themes to reflect brand changes.
- Set Default Themes: Programmatically change the global default theme.
- Manage Color Collections: Associate custom color palettes with themes.
1. Prerequisites
- API Version: Use Looker API 4.0 for the most up-to-date theme features.
- Permissions: Requires
admin or manage_themes permissions.
- Feature Flag: Custom themes must be enabled on the Looker instance.
2. Implementation Pattern (Python SDK)
Initializing the SDK
import looker_sdk
from looker_sdk import models40 as models
sdk = looker_sdk.init40()
Creating a New Theme
When creating a theme, you define a WriteTheme object containing ThemeSettings.
new_theme = sdk.create_theme(
body=models.WriteTheme(
name="corporate_brand_v1",
settings=models.ThemeSettings(
background_color="#F4F7F9",
base_font_size="14px",
color_collection_id="my_custom_palette_id",
font_family="Roboto, Helvetica, Arial, sans-serif",
primary_button_color="#0052CC",
show_filters_bar=True,
show_title=True,
tile_background_color="#FFFFFF",
title_color="#172B4D",
show_header=True
)
)
)
print(f"Created theme ID: {new_theme.id}")
Updating an Existing Theme
You can perform partial updates by passing only the fields you wish to change.
sdk.update_theme(
theme_id="123",
body=models.WriteTheme(
settings=models.ThemeSettings(
background_color="#121212",
tile_background_color="#1E1E1E",
title_color="#FFFFFF"
)
)
)
Setting the Global Default Theme
The set_default_theme method takes the theme name, not the ID.
sdk.set_default_theme(theme_name="corporate_brand_v1")
3. Theme Settings Reference
| Property | Type | Description |
|---|
background_color | string | Main background color of the dashboard. |
tile_background_color | string | Background color of individual tiles. |
title_color | string | Color of the dashboard title text. |
font_family | string | Primary font for text elements. |
color_collection_id | string | ID of the color palette for visualizations. |
show_header | boolean | Toggle the visibility of the dashboard header. |
show_title | boolean | Toggle the visibility of the dashboard title. |
primary_button_color | string | Color of primary buttons and accents. |
warn_button_color | string | Color of warning/danger buttons. |
4. Applying Themes to Content
Via Embed URL
Append the theme parameter to the Looker embed URL.
https://your_company.looker.com/embed/dashboards/1?theme=corporate_brand_v1
Via Embed SDK
LookerEmbedSDK.createDashboardWithId(1)
.withParams({ theme: 'corporate_brand_v1' })
.build()
.connect()
5. Full Theme Payload Example
Here is a comprehensive JSON payload for creating a theme, including extended settings:
{
"name": "full_featured_theme",
"settings": {
"background_color": "#f8fafc",
"base_font_size": "16px",
"color_collection_id": "",
"font_color": "#334155",
"font_family": "'Inter', system-ui, sans-serif",
"font_source": "",
"info_button_color": "#6366f1",
"primary_button_color": "#6366f1",
"show_filters_bar": true,
"show_title": true,
"text_tile_text_color": "#334155",
"tile_background_color": "#ffffff",
"text_tile_background_color": "#ffffff",
"tile_text_color": "#334155",
"title_color": "#0f172a",
"warn_button_color": "#ef4444",
"tile_title_alignment": "left",
"tile_shadow": true,
"show_last_updated_indicator": true,
"show_reload_data_icon": true,
"show_dashboard_menu": true,
"show_filters_toggle": true,
"show_dashboard_header": true,
"center_dashboard_title": false,
"dashboard_title_font_size": "2.5rem",
"box_shadow": "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
"page_margin_top": "1.5rem",
"page_margin_bottom": "1.5rem",
"page_margin_sides": "1.5rem",
"show_explore_header": true,
"show_explore_title": true,
"show_explore_last_run": true,
"show_explore_timezone": true,
"show_explore_run_stop_button": true,
"show_explore_actions_button": true,
"show_look_header": true,
"show_look_title": true,
"show_look_last_run": true,
"show_look_timezone": true,
"show_look_run_stop_button": true,
"show_look_actions_button": true,
"tile_title_font_size": "1.25rem",
"column_gap_size": "1rem",
"row_gap_size": "1rem",
"border_radius": "0.5rem"
}
}
6. Troubleshooting & Best Practices
- Active Status: Only "active" themes can be set as defaults or used in embedding.
- Unique Names: Theme names must be unique across the instance.
- Color Collections: Ensure the
color_collection_id exists before associating it with a theme, otherwise it will fallback to the system default.
- LookML Color Override Removal: Static colors hardcoded inside LookML dashboards (
series_colors, text_color, embed_style colors) always take priority over runtime embed themes. Always strip these hardcoded color mappings from LookML files so embedded content dynamically inherits the active theme's palette.
- Expiration: Themes with an
end_at value will automatically deactivate. For permanent themes, ensure end_at is None or null.
- API Cache: When updating a theme, changes may take a few seconds to propagate to all cached dashboard views.
7. Opinionated Brand Theme Setup Pattern
This application follows an opinionated Looker embed theme naming convention tied to configured brand names in frontend/src/config/constants.ts (BRAND_OPTIONS).
Naming Convention & Brand Sanitization
Looker theme names only support alphanumeric characters and underscores. When resolving theme names from brand strings, any spaces are converted to underscores and all non-alphanumeric characters (except underscores) are removed via sanitizeBrandName(brand).
For every Brand configured in BRAND_OPTIONS (e.g. Levi's, Calvin Klein, Allegra K, Columbia), themes in Looker must be created following the sanitized <clean_brand>_Light and <clean_brand>_Dark pattern:
Levi's -> Levis_Light / Levis_Dark
Calvin Klein -> Calvin_Klein_Light / Calvin_Klein_Dark
Allegra K -> Allegra_K_Light / Allegra_K_Dark
Columbia -> Columbia_Light / Columbia_Dark
Runtime Resolution & Fallback
The frontend application dynamically sanitizes the selected brand and resolves the active theme (e.g., Levis_Light or Levis_Dark).
If a specific brand theme is not defined or fails to resolve, the application gracefully falls back to the default theme defined in VITE_THEME (e.g. Embed_Demo_Light / Embed_Demo_Dark).
LookML Dashboard Color Inheritance (Style-Agnostic Authoring)
In Looker, hardcoded colors specified inside LookML files always take priority over runtime embed themes (?theme=...). Specifically:
- Any
series_colors, color_application, color_palette, custom_colors, or totals_color defined on a visualization tile overrides the theme's color collection.
- Any
text_color on a single_value KPI tile overrides the theme font/primary button color.
- Any
background_color, title_color, or tile_text_color inside embed_style: overrides the theme container styling.
Best Practice: To ensure embedded dashboards dynamically inherit the active brand's color palette and background theme (Levis_Light, Calvin_Klein_Dark, etc.), strip all hardcoded color overrides from LookML dashboard definitions (.dashboard.lookml). When these attributes are removed, Looker automatically applies the coordinating palette from the active theme's color_collection_id.
Automated Provisioning of Color Palettes & Themes via looker-cli
When running setup or onboarding scripts, use the looker-cli to automatically provision brand-specific color palettes (Color Collections) and attach them to the corresponding Looker embed themes.
Step 1: Create or Update Color Collections
Check existing custom color collections using color_collections_custom:
looker-cli api colorcollection color_collections_custom --token-file --fields "id,label"
Pass the palette definitions via stdin (-) to create or update them:
cat palette.json | looker-cli api colorcollection create_color_collection - --token-file
cat palette.json | looker-cli api colorcollection update_color_collection levis - --token-file
Step 2: Create or Update Brand Themes
Check existing themes:
looker-cli api theme all_themes --token-file --fields "id,name"
Pass the JSON definition via stdin (-), setting color_collection_id to the ID returned from Step 1:
cat theme.json | looker-cli api theme create_theme - --token-file
cat theme.json | looker-cli api theme update_theme 12 - --token-file
Python Automation Snippet
You can automate the idempotency loop for both color collections and themes across all BRAND_OPTIONS:
import json, re, subprocess
brands = ["Levi's", "Calvin Klein", "Allegra K", "Columbia"]
def sanitize(brand):
return re.sub(r'[^a-zA-Z0-9_]', '', re.sub(r'\s+', '_', brand))
stdout_cols = subprocess.check_output(["looker-cli", "api", "colorcollection", "color_collections_custom", "--token-file"])
col_map = {c["label"]: c["id"] for c in json.loads(stdout_cols)}
brand_col_ids = {}
for brand in brands:
clean = sanitize(brand)
colors = ["#9E9E9E", "#607D8B", "#B0BEC5", "#78909C", "#D6D6D6", "#455A64"] if "Calvin" in brand else ["#0052CC", "#2A9D8F", "#E76F51", "#9D4EDD", "#3A86FF"]
palette_payload = json.dumps({
"label": clean,
"categoricalPalettes": [{"label": f"{clean} Categorical", "type": "Categorical", "colors": colors}]
})
if clean in col_map:
col_id = col_map[clean]
subprocess.run(["looker-cli", "api", "colorcollection", "update_color_collection", str(col_id), "-", "--token-file"], input=palette_payload, text=True)
brand_col_ids[clean] = col_id
else:
res = subprocess.run(["looker-cli", "api", "colorcollection", "create_color_collection", "-", "--token-file"], input=palette_payload, text=True, capture_output=True)
brand_col_ids[clean] = json.loads(res.stdout).get("id")
stdout_themes = subprocess.check_output(["looker-cli", "api", "theme", "all_themes", "--token-file"])
theme_map = {t["name"]: t["id"] for t in json.loads(stdout_themes)}
for brand in brands:
clean = sanitize(brand)
col_id = brand_col_ids.get(clean, "embed-demo")
for mode in ["Light", "Dark"]:
name = f"{clean}_{mode}"
payload = json.dumps({
"name": name,
"settings": {
"background_color": "#f0f4f9" if mode == "Light" else "#131314",
"tile_background_color": "#ffffff" if mode == "Light" else "#1e1f20",
"color_collection_id": col_id,
"font_family": "'Inter', system-ui, sans-serif",
"border_radius": "12px",
"tile_shadow": True
}
})
if name in theme_map:
subprocess.run(["looker-cli", "api", "theme", "update_theme", str(theme_map[name]), "-", "--token-file"], input=payload, text=True)
else:
subprocess.run(["looker-cli", "api", "theme", "create_theme", "-", "--token-file"], input=payload, text=True)