---
title: Building HTML Tables in Text Blocks
slug: building-html-tables-in-text-blocks
docTags: 
createdAt: 2026-04-14T03:43:43.959Z
---

Text blocks in ChartHop dashboards support raw HTML, which means you can build a fully formatted, styled table using a `<table class="ch-table-1">` element combined with CQLT (template expressions). This is especially useful when you need a custom multi-row table that responds to dashboard filters — but where the native chart types don't give you exactly the layout you want.
## When to Use This
- You want a table that displays computed metrics side by side (e.g., review scores across multiple categories)
- You need 20+ versions of the same dashboard with different filters — and want one flexible text block that adapts to `jobFilter`, rather than rebuilding native chart tables for each version
- The data you're displaying comes from long or complex CQL expressions (like custom scoring calculations) that don't map cleanly to a native chart formula
## How It Works
Inside a Text Block, you can write HTML directly. ChartHop will render it as formatted content. Wrap any CQL expression in `{{ }}` to have it evaluated dynamically — just like you would in any other template expression.
The class `ch-table-1` applies ChartHop's standard table styling, so your table visually matches the rest of the dashboard.
## Basic Structure
```html
<table class="ch-table-1">
  <thead>
    <tr>
      <th>Category</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Overall Performance</td>
      <td>{{ findAnswersByAssessment('managerReviewRating', 'H1 Performance Review').filter{target.department=jobFilter}.mean{value} }}</td>
    </tr>
    <tr>
      <td>Values Alignment</td>
      <td>{{ findAnswersByAssessment('valuesRatingManager', 'H1 Performance Review').filter{target.department=jobFilter}.mean{value} }}</td>
    </tr>
  </tbody>
</table>
```

## Available Variables

Text blocks have access to three key dashboard variables that make your table dynamic and filter-aware:

| Variable        | What It Represents                                                                                                           |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `jobFilter`     | The audience/filter the dashboard viewer has currently applied (e.g., filtered to Engineering, or a specific manager's team) |
| `intervalFrom`  | The start of the reporting period selected on the dashboard (inclusive)                                                      |
| `intervalUntil` | The end of the reporting period selected on the dashboard (exclusive — i.e. the day *after* the last day in the period)      |

Using all three means your table responds fully to whatever the viewer has configured — both in terms of *who* is in scope and *when*.

These are the same variables used in time-series chart formulas, so if you're already familiar with `intervalFrom`/`intervalUntil` from building charts, they work exactly the same way here.

### Example: using all three variables

```html
<td>{{ findHires(intervalFrom, intervalUntil).filter{jobFilter}.count() }}</td>
```

This counts new hires within the selected date range, scoped to the active filter.

## Using `jobFilter` Inside the Table

Because this is a Text Block, the `jobFilter` variable is available — it reflects whatever filter the dashboard viewer has currently applied (e.g., filtered to Engineering, or a specific manager's team).

You can pass `jobFilter` directly into your CQL expression:

```html
<td>{{ db.job.find(jobFilter).mean{baseComp.annualized} }}</td>
```

This means a single text block table can power 20+ filtered dashboard versions without any changes — just apply a different audience or department filter on the dashboard itself.

## Real-World Example: Multi-Score Review Summary Table

This example displays average scores across several review dimensions, filtered to whoever is in scope:

```html
<table class="ch-table-1">
  <thead>
    <tr>
      <th>Review Dimension</th>
      <th>Avg Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Overall Performance</td>
      <td>{{ findAnswersByAssessment('managerReviewRating', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
    </tr>
    <tr>
      <td>Impact</td>
      <td>{{ findAnswersByAssessment('impactCompetency', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
    </tr>
    <tr>
      <td>Collaboration</td>
      <td>{{ findAnswersByAssessment('collaborationCompetency', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
    </tr>
    <tr>
      <td>Knowledge</td>
      <td>{{ findAnswersByAssessment('knowledgeCompetency', 'H1 2025 Review').filter{jobFilter}.mean{value} }}</td>
    </tr>
  </tbody>
</table>
```

## Tips

- **Always use&#x20;**`ch-table-1` as the class — this applies ChartHop's built-in table styling and keeps it visually consistent with native chart tables.
- **Column headers are static HTML** — only the cell values need template expressions. Keep headers as plain text in `<th>` tags.
- **Expressions are evaluated server-side** — if a formula returns no data (e.g., no responses exist for a filter), the cell will render as empty rather than erroring.
- **This does not replace native Table charts** for most use cases — if the built-in Table chart type works for you, use it. This approach shines when you need custom layouts, multi-source data in one table, or highly specific expression logic that the native chart type can't express.
