+1 (726) 227-2971

Custom Visualizations in Looker: Palettes, Liquid HTML, and the Custom Viz API

Updated August 2026. This is a full rewrite of the 2023 article, which contained invalid LookML and copy-paste artifacts. Everything below is current Looker behavior.

Looker's built-in visualizations cover most reporting needs, but three customization layers are worth knowing well: color collections and themes, Liquid-driven HTML formatting in LookML, and the custom visualization API for when you need a chart Looker does not ship.

Choosing and configuring built-in visualizations

Visualization type is chosen in the Explore or dashboard tile editor, not in LookML. What LookML can do is make fields behave well in any visualization: value_format_name for consistent number formatting, drill_fields for click-through, and group_label so related measures sit together in the field picker.

view: orders {
  measure: total_revenue {
    type: sum
    sql: ${TABLE}.amount ;;
    value_format_name: usd_0
    drill_fields: [order_detail*]
  }

  set: order_detail {
    fields: [order_id, created_date, users.name, total_revenue]
  }
}

The set: named in drill_fields must exist; referencing detail* without defining a detail set is one of the most common validator errors we see in inherited projects. If you want a saved visualization to travel with the model, define it in a LookML dashboard file (.dashboard.lookml), where each element carries its type and visualization settings.

Color palettes: collections and themes

There is no color_palette parameter in a model file. Colors are managed in three places:

  1. Per visualization: the Series tab in the visualization editor lets you pick a palette from the instance's color collections or set individual series colors. This is stored with the Look or dashboard tile.
  2. Color collections (instance-wide): in Admin > Platform > Color Collections an admin can create custom collections with categorical, sequential and diverging palettes that match your brand. Mark one as the default and every new visualization picks it up.
  3. Themes (embedded dashboards): Admin > Platform > Themes defines fonts, background and tile colors for embedded dashboards, selected with a theme parameter in the embed URL. Themes change chrome, not series colors; combine them with a color collection for a fully branded embed.

In a LookML dashboard file you can pin a palette per element so the model and the branding deploy together:

- name: revenue_by_region
  type: looker_column
  model: ecommerce
  explore: orders
  fields: [users.region, orders.total_revenue]
  series_colors:
    orders.total_revenue: "#1A73E8"
  color_application:
    collection_id: vistelio-brand
    palette_id: vistelio-categorical

Liquid and HTML for conditional formatting

The html: parameter on a dimension or measure renders a Liquid template in place of the plain value, in table and single-value visualizations and in drill tooltips. The Liquid variables available are value (the raw value), rendered_value (with value_format applied), filterable_value, link and linked_value, plus other fields' values via ${view_name.field_name._value} style references. There is no _dimension_value variable.

Here is a measure that colors itself by threshold:

view: orders {
  measure: total_revenue {
    type: sum
    sql: ${TABLE}.amount ;;
    value_format_name: usd_0
    html: {% if value < 1000 %}
            <span style="color: #C5221F;">{{ rendered_value }}</span>
          {% elsif value < 10000 %}
            <span style="color: #E37400;">{{ rendered_value }}</span>
          {% else %}
            <span style="color: #188038;">{{ rendered_value }}</span>
          {% endif %} ;;
  }
}

Two rules keep this sane. First, put the html: on the measure itself; do not create a string dimension that wraps a measure, because a dimension's sql: cannot reference a measure (that was the bug in the 2023 example). Second, compare on value, not rendered_value; the rendered string has currency symbols and separators in it. For per-row formatting that depends on another field, reference it explicitly:

dimension: status_badge {
  type: string
  sql: ${status} ;;
  html: {% if orders.is_late._value == "Yes" %}
          <span style="background:#FCE8E6;padding:2px 6px;">{{ value }} (late)</span>
        {% else %}
          {{ value }}
        {% endif %} ;;
}

Looker sanitizes the HTML to a safe subset (inline styles, spans, images, links), which is plenty for badges, progress bars made of <div>s, and logos looked up from a URL in another field. For table-wide rules that analysts can edit without LookML, the table visualization's conditional formatting settings do the same job without code.

Custom visualizations with the Visualization API

When you need a chart Looker does not have (a sankey, a network graph, a specific gauge), you write a JavaScript visualization against the Custom Visualization API and install it on the instance. The contract is unchanged from the original API: register an object with id, label, options, create and updateAsync.

looker.plugins.visualizations.add({
  id: "revenue_gauge",
  label: "Revenue Gauge",
  options: {
    target: { type: "number", label: "Target", default: 100000 },
    color: { type: "string", display: "color", label: "Bar color", default: "#1A73E8" },
  },

  create: function (element, config) {
    element.innerHTML = "<div class='gauge'><div class='bar'></div><div class='label'></div></div>";
    this.bar = element.querySelector(".bar");
    this.label = element.querySelector(".label");
  },

  updateAsync: function (data, element, config, queryResponse, details, done) {
    this.clearErrors();
    if (queryResponse.fields.measures.length < 1) {
      this.addError({ title: "No measure", message: "Add one measure to the query." });
      return;
    }
    const measure = queryResponse.fields.measures[0].name;
    const value = data[0][measure].value;
    const pct = Math.max(0, Math.min(100, (value / config.target) * 100));
    this.bar.style.width = pct + "%";
    this.bar.style.background = config.color;
    this.label.textContent = LookerCharts.Utils.textForCell(data[0][measure]);
    done();
  },
});

The options object defines controls that appear in the visualization editor; config carries their current values. queryResponse.fields tells you which dimensions and measures the user selected, and LookerCharts.Utils.textForCell gives you the rendered value with value_format applied so your custom chart matches the rest of the dashboard.

Installing it. Custom visualizations are not added from a dropdown in the IDE. You either:

  • install one of the ready-made visualizations from the Looker Marketplace (Admin > Platform > Marketplace), or
  • host your built JavaScript file on HTTPS (a bucket, a CDN, or a Looker extension) and register it under Admin > Platform > Visualizations with an id, a label and the URL.

Once registered, the visualization appears in the chart picker for every user. Use the looker-open-source/custom_visualizations_v2 repository on GitHub as a starting point; it includes the development harness and build configuration.

Which layer to use

  • Brand colors everywhere: a default color collection, plus a theme for embeds.
  • Thresholds and badges in tables: html: with Liquid on the measure, or table conditional formatting.
  • A chart type Looker lacks: a custom visualization, installed via Admin > Platform > Visualizations.

Need a bespoke visualization or a branded embedded dashboard? Our Looker developers build them; contact us.