Skip to content

Board Sizing

dbt charts uses a three-layer sizing model. Understanding which layer owns what prevents most sizing surprises.


The Three Layers

Layer Where What it controls
Layout tile rows: / cols: / grid: entry The slot the chart renders into
Chart root charts.<name>.width / height Preferred width and fixed height requests from the chart itself
Chart style charts.<name>.style.aspect_ratio Aspect-ratio-derived height for this chart
Theme default style.charts.aspect_ratio in the board/theme cascade Global fallback

Priority

  1. An explicit height: on a rows: tile is a ceiling. The row never reserves space its content does not use — it shrinks to fit. (If the content needs more — a bar chart with too many categories to stay readable, say — the content wins and a warning names the chart.) This is a rows: rule only — cols:, grid: and tabs: tiles are unchanged, and an oversized height: on one of those still reserves the space it was given.
  2. If the tile has no explicit height, chart-root height: wins.
  3. Chart-style aspect_ratio: is used next (computes height = width / aspect_ratio).
  4. The theme default style.charts.aspect_ratio is the final fallback.

The layout owns each chart's final width. Chart-root width: contributes to the dashboard's intrinsic width, but does not force the final slot width.


Default Behavior

Width Rule

dbt charts measures the layout's preferred width recursively:

  • Rows and tabs use their widest child.
  • Columns add their child widths and gaps.
  • Grids derive track widths from each item's span.
  • Nested dashboards repeat the same calculation.

The rendered dashboard shrinks to that intrinsic width and never exceeds style.frame.width, which acts as the maximum. A dashboard with no charts keeps the maximum width.

That intrinsic width sets the board's proportions, not its final on-screen size. Viewed as an HTML page — dct serve, dct render --format html — the board scales to fill the window width and keeps its aspect ratio, so these measurements govern relative density (how much room each chart gets, how large type reads against its plot) rather than pixels in the browser.

After that outer width is chosen, existing layout allocation rules still apply. For example, items in a columns layout split the available width evenly: - In a cols layout with 2 items, each gets 50% - In a cols layout with 3 items, each gets 33.3%

Height Rule

Heights are derived from width ÷ aspect ratio for plot-style charts (bar, line, area, etc.). The global default aspect ratio is 1.5 (3:2), set in the base theme under style.charts.aspect_ratio. Override it in any board via style.charts.aspect_ratio or at the individual chart level with aspect_ratio:. Some chart types override this — for example, pie and arc default to 1.0 (square), and map types default to 1.2.

Exceptions — chart-root height is ignored for these types:

  • KPI — fixed compact height (renderer owns sizing)
  • Table — data-driven height based on actual row count
  • Callout — fixed compact height
  • Spark bar — fixed height

The aspect-ratio-derived height is clamped between min_height and max_height. The board/theme defaults are style.charts.min_height (150px) and style.charts.max_height (400px). Individual charts can override these clamps with per-chart style: {min_height: ..., max_height: ...} — see Chart height clamps below.

In a cols layout, all items share the same height (the tallest item's height).


Unequal Widths with Nesting

Use nested lists as a shorthand for nested boards. A nested list automatically inherits the layout type (rows or cols) of its parent.

50% / 25% / 25% Split

To make the first chart take up half the width (50%) and the next two share the remaining half (25% each), nest them in a list:

source: examples_db
title: "Asymmetric Layout"

queries:
  sales_by_category:
    sql: |
      SELECT category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC
  sales_summary:
    sql: |
      SELECT SUM(revenue) AS revenue, SUM(units_sold) AS units_sold, COUNT(DISTINCT category) AS category_count FROM ecommerce_orders

charts:
  main_chart:
    query: sales_by_category
    type: bar
    title: "Main Chart (50%)"
    x: category
    y: revenue
  kpi1:
    query: sales_summary
    type: kpi
    label: "Side Chart 1 (25%)"
    value: revenue
  kpi2:
    query: sales_summary
    type: kpi
    label: "Side Chart 2 (25%)"
    value: units_sold

# 50% / 25% / 25% split using nested cols
cols:
  - main_chart
  - cols:
      - kpi1
      - kpi2
Asymmetric Layout 050k100k150k200kElectronicsAccessoriesToolsMain Chart (50%) 440k Side Chart 1 (25%) 15k Side Chart 2 (25%) Data as of 14:52 UTC on 21 Aug 2026 made with dbt charts

Complex Nesting

source: examples_db
title: "Complex Dashboard"

queries:
  sales_summary:
    sql: |
      SELECT SUM(revenue) AS revenue, SUM(units_sold) AS units_sold, COUNT(DISTINCT category) AS category_count FROM ecommerce_orders
  sales_by_category:
    sql: |
      SELECT category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY category ORDER BY revenue DESC
  sales_by_product:
    sql: |
      SELECT product, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY product, category ORDER BY revenue DESC
  sales_by_date_category:
    sql: |
      SELECT date, category, SUM(revenue) AS revenue, SUM(units_sold) AS units_sold FROM ecommerce_orders GROUP BY date, category ORDER BY date, category

charts:
  kpi1:
    query: sales_summary
    type: kpi
    label: "Revenue"
    value: revenue
  kpi2:
    query: sales_summary
    type: kpi
    label: "Units Sold"
    value: units_sold
  mini1:
    query: sales_by_category
    type: bar
    title: "Mini Chart 1"
    x: category
    y: revenue
  mini2:
    query: sales_by_product
    type: bar
    title: "Mini Chart 2"
    x: product
    y: units_sold
  main:
    query: sales_by_date_category
    type: line
    title: "Main Analysis"
    x: date
    y: revenue
    color: category

rows:
  - cols:
      - kpi1
      - kpi2
      - cols:
          - mini1
          - mini2
  - main
Complex Dashboard 440k Revenue 15k Units Sold 050k100k150k200kElectronicsAccessoriesToolsMini Chart 101,0002,000Widget BWidget CGadget YWidget AGadget XTool ZTool AGadget ZMini Chart 2 1Jan'242345678910111213141516171819202122232425262728293005,00010,00015,00020,000Main AnalysisElectronicsAccessoriesTools Data as of 14:52 UTC on 21 Aug 2026 made with dbt charts

Authoring Chart Size

Chart root holds the two fixed-geometry fields:

  • height: <px> — exact pixel height. Wins over aspect_ratio and theme defaults. Bypasses min_height / max_height clamps.
  • width: <px> — preferred width used to measure the dashboard. Does not override the final layout slot width.

style: holds aspect-ratio sizing fields (and all paint — colors, fonts, marks):

  • style.aspect_ratio: <float> — shape without a fixed pixel size. height = width / aspect_ratio.
  • style.min_height: <px> — floor for the computed height.
  • style.max_height: <px> — ceiling for the computed height.

height and width belong at chart root, not in style: — putting them there raises a validation error.

Choosing between chart-root height and a layout tile height

# Chart-root height — the chart requests 400px;
# the layout honours it unless the tile has its own height.
charts:
  inventory_overview:
    query: inventory
    type: bar
    x: category
    y: count
    height: 400

# Layout tile height — the slot is at most 600px. If the chart inside needs less,
# the row shrinks to it rather than leaving an empty band.
rows:
  - height: 600
    rows:
      - inventory_overview

Use chart-root height when the chart itself has a natural size independent of its context (e.g. a detail chart that always needs 400px to be readable).

Use a layout tile height to cap how tall a row may get — everything in that row is bounded by it (e.g. a KPI row that must not grow taller than the charts below it).

A rows: height is an upper bound, not a reservation

Authoring height: 120 on a KPI row does not guarantee a 120px row. If the KPIs only need 81px, the row is 81px and the board tightens up. This is deliberate: reserving space the content does not use leaves a dead band inside the row, which reads as a much larger gap to whatever comes next.

If you want breathing room between rows, set it as spacing rather than as an oversized row height.

This applies to rows: only. cols:, grid: and tabs: tiles are unchanged — a height: there still reserves exactly what it asks for, so an oversized one leaves the empty band described above.

Rejected shape

# REJECTED — height/width under style: is not supported
charts:
  bad:
    type: bar
    style:
      height: 400      # ← raises a validation error; move height to chart root

Explicit Sizing

Chart height

Set height on a chart to fix its pixel height explicitly. This bypasses aspect-ratio sizing and ignores min_height / max_height clamps.

charts:
  revenue:
    type: bar
    query: revenue_by_month
    x: month
    y: revenue
    height: 500   # fixed at 500px

Set aspect_ratio to control the shape without fixing an exact pixel size:

charts:
  revenue:
    type: bar
    query: revenue_by_month
    x: month
    y: revenue
    style:
      aspect_ratio: 2.5   # wider-than-default; height = width / 2.5

Chart height clamps

min_height and max_height cap the aspect-ratio-derived height. Board/theme defaults cascade from style.charts.min_height and style.charts.max_height. Override them per chart under style::

charts:
  stock_levels:
    type: bar
    query: stock_levels
    x: product_name
    y: stock_quantity
    style:
      aspect_ratio: 1.5
      min_height: 80    # floor for this chart only; overrides board/theme default
      max_height: 200   # ceiling for this chart only; overrides board/theme default

Explicit chart.height bypasses min_height / max_height — when you set an exact pixel height, the clamps do not apply.

Height on Rows

Set explicit height on row items:

rows:
  - height: "120px"
    cols:
      - kpi1
      - kpi2
      - kpi3

  - main_chart  # Takes remaining height

Grid Sizing

In grid layouts, use width and height on items:

grid:
  columns: 24
  items:
    - item: kpi1
      width: 8      # 8 of 24 columns
    - item: kpi2
      width: 8
    - item: main_chart
      width: 16
      height: 2     # 2 rows tall
    - item: sidebar
      width: 8
      height: 2

Sizing Patterns

KPI Row + Main Chart

rows:
  - height: "100px"
    cols: [kpi1, kpi2, kpi3]
  - main_chart
cols:
  - cols:           # 66% main content
      - chart1
      - chart2
  - sidebar_chart   # 33% sidebar
rows:
  - height: "80px"
    text: "# Dashboard Title"
  - main_content    # Flexible middle
  - height: "60px"
    text: "Footer text"

Configuration

Theme defaults (board/theme cascade)

These keys set the theme default layer — they apply to every chart that does not provide a chart-root override.

Key Default Description
style.charts.aspect_ratio 1.5 Global width:height ratio for plot-style charts
style.charts.min_height 150 Minimum chart height (px) when aspect-ratio drives sizing
style.charts.max_height 400 Maximum chart height (px) when aspect-ratio drives sizing
style.charts.<type>.aspect_ratio Per-type override (e.g. pie: 1.0, map: 1.2). Not supported on kpi, table, callout, or spark_bar — those chart types own their own sizing contracts.

height and width cannot be set in the theme — they are per-chart explicit overrides only (see below).

Chart-root fields (per chart, under charts.<name>:)

Field Description
height Exact pixel height. Wins over aspect_ratio and theme. Bypasses min_height/max_height.
width Preferred width in pixels. Contributes to the dashboard's intrinsic width; the chart still fills its final layout slot.

Per-chart style fields (under charts.<name>.style: or in the theme under style.charts.*)

Field Description
aspect_ratio Shape without a fixed pixel size. height = width / aspect_ratio.
min_height Floor for this chart only; overrides theme style.charts.min_height.
max_height Ceiling for this chart only; overrides theme style.charts.max_height.

aspect_ratio, min_height, and max_height are valid both at chart level (style: {aspect_ratio: 2.0}) and in the theme (style.charts.aspect_ratio). height and width are chart-root-only — they are not valid under style:.

These three fields are not supported on kpi, table, callout, or spark_bar charts — those families use renderer-owned or layout-owned sizing contracts rather than the aspect-ratio cascade. Authoring style.aspect_ratio on a kpi or table chart, or style.charts.kpi.aspect_ratio in the theme, is rejected with a validation error.


Card Gap

By default, cards (charts and content blocks) render edge-to-edge — no gap between them. Set card_gap: true at the board root to add spacing between every card on the board:

title: "Spaced-Out Dashboard"
card_gap: true

rows:
  - revenue
  - orders

The actual pixel value comes from the theme (style.frame.card_gap), not from the board — card_gap is a per-board on/off switch, the theme owns how big the gap is. card_gap is root-only: setting it on a nested board raises a validation error, because spacing is a whole-board layout decision, not a per-section one.