BIBB
DEVX
•

DAX + UDF = the React of Power BI

Oscar MartĂ­nez
Oscar MartĂ­nez Data, BI and AI | Operational Lead | Power BI & Azure Expert | Governance-Driven Strategy | Product Owner

What is React? (for Power BI users)

React is an open-source JavaScript library, built by Meta, that helps developers compose interactive user interfaces out of small, reusable components. Each component accepts props (inputs) and manages how markup, styles, and behavior are combined before rendering to the screen.

If you spend most of your time in Power BI, think of React components as cousins of your DAX measures: they hide complexity behind a friendly interface, let you reuse logic across many visuals, and guarantee consistent presentation.

Throughout this walkthrough you’ll map those React ideas to DAX user-defined functions. A single UDF will play the role of a React component—encapsulating the HTML, parameterizing the dynamic bits, and making it effortless to reuse the visual shell anywhere in your report.

Build reusable HTML cards with DAX UDFs

Power BI’s DAX user-defined functions (UDFs) let you centralize HTML snippets and reuse them across visuals. In this guide you’ll assemble a flexible KPI card experience—complete with progress bars—that slots neatly into any HTML visual.

What you’ll build

  • A calculated table with sample product data to feed your “components”
  • A BuildCard DAX function that returns a styled HTML card—your React-like component in DAX
  • A measure that concatenates those cards for an HTML visual, effectively “rendering” the component tree
  • An optional table-row variation for tabular layouts when you need a different component shape

Prerequisites

  • Enable DAX user-defined functions under File → Options and settings → Options → Preview features
  • Power BI Desktop with the HTML Content visual (or similar) from AppSource
  • Basic familiarity with DAX measures and the model view

Rendered KPI cards generated by DAX UDFs Preview of the reusable KPI card layout in an HTML visual

Step-by-step build

Step 1. Create the demo data table

Add a calculated table in Model view → New table and paste the following definition to keep the sample lightweight.

Calculated table: Products
Products =
DATATABLE(
    "Product", STRING,
    "Units", INTEGER,
    "Target", INTEGER,
    {
        {"Widget A", 85, 100},
        {"Widget B", 50, 100},
        {"Widget C", 120, 100},
        {"Widget D", 95, 100}
    }
)

Use slicers or filters to swap in your real dataset later; the UDF and measure will scale automatically.

Step 2. Author the HTML card function

Create the DAX function in DAX Query View → New query, then choose Update model → Add new function so it becomes callable from your measures. This function is your component blueprint: parameters mirror props and the returned HTML is the JSX-like template.

Function: BuildCard
DEFINE
    FUNCTION BuildCard = (
        Product : STRING,
        Units   : NUMERIC,
        Target  : NUMERIC
    ) =>
    VAR pct   = DIVIDE(Units, Target, 0)
    VAR color = IF(pct >= 1, "#16a34a", IF(pct >= 0.8, "#f59e0b", "#dc2626"))
    RETURN
        "<div style='font-family:Segoe UI;display:inline-block;vertical-align:top;width:220px;margin:8px;padding:12px;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.15);background:#fff'>
            <div style='font-weight:600;margin-bottom:6px'>" & Product & "</div>
            <div style='font-size:12px;margin-bottom:6px'>" & FORMAT(Units,"#,0") & " / " & FORMAT(Target,"#,0") & "</div>
            <div style='height:10px;background:#eee;border-radius:6px;overflow:hidden'>
                <div style='width:" & FORMAT(pct*100,"0") & "%;height:100%;background:" & color & ";'></div>
            </div>
            <div style='font-size:12px;margin-top:6px'>" & FORMAT(pct,"0%") & "</div>
        </div>"

The function chooses a color based on goal attainment, handles divide-by-zero with DIVIDE, and emits a complete HTML card string ready for the visual.

Step 3. Concatenate cards for the HTML visual

Create a measure that loops through your dataset, calls BuildCard, and wraps the result in a flexible container so cards flow naturally. Think of it as the render phase that instantiates your component for each row and mounts them in a parent container.

Measure: KPI Cards HTML
KPI Cards HTML :=
VAR Cards =
    ADDCOLUMNS(
        Products,
        "__html", BuildCard(Products[Product], Products[Units], Products[Target])
    )
RETURN
    "<div style='display:flex;flex-wrap:wrap;gap:12px'>" &
        CONCATENATEX(Cards, [__html], "") &
    "</div>"

CONCATENATEX keeps the output as a single string—a requirement for HTML visuals—while the flex container manages responsive layout.

Step 4. Render the cards inside an HTML visual
  1. Add the HTML Content (or comparable) visual to your report canvas.
  2. Place the KPI Cards HTML measure into the visual’s Values field.
  3. Disable sanitization or script blocking if the visual exposes that setting; this layout uses plain HTML and CSS only.
  4. Adjust card widths, font sizes, or colors directly inside BuildCard for quick brand customization.

Optional variations

Output a full HTML table

Prefer a tabular layout? Create a row-level UDF and reuse the same pattern to stitch rows together for the visual—consider it an alternate component that renders a table row instead of a card.

Function: BuildRow
DEFINE
    FUNCTION BuildRow = (
        Product : STRING,
        Units   : NUMERIC,
        Target  : NUMERIC
    ) =>
    VAR pct = DIVIDE(Units, Target, 0)
    RETURN
        "<tr>
            <td style='padding:6px 12px;border-bottom:1px solid #eee'>" & Product & "</td>
            <td style='padding:6px 12px;border-bottom:1px solid #eee;text-align:right'>" & FORMAT(Units,"#,0") & "</td>
            <td style='padding:6px 12px;border-bottom:1px solid #eee;text-align:right'>" & FORMAT(Target,"#,0") & "</td>
            <td style='padding:6px 12px;border-bottom:1px solid #eee'>" & FORMAT(pct,"0%") & "</td>
        </tr>"
Measure: HTML Table
HTML Table :=
VAR Rows =
    ADDCOLUMNS(
        Products,
        "__row", BuildRow(Products[Product], Products[Units], Products[Target])
    )
RETURN
    "<table style='font-family:Segoe UI;border-collapse:collapse;width:100%'>
        <thead>
            <tr>
                <th style='text-align:left;padding:6px 12px;border-bottom:2px solid #ccc'>Product</th>
                <th style='text-align:right;padding:6px 12px;border-bottom:2px solid #ccc'>Units</th>
                <th style='text-align:right;padding:6px 12px;border-bottom:2px solid #ccc'>Target</th>
                <th style='text-align:left;padding:6px 12px;border-bottom:2px solid #ccc'>% of Target</th>
            </tr>
        </thead>
        <tbody>" &
            CONCATENATEX(Rows, [__row], "") &
        "</tbody>
    </table>"

Use slicers to control row volume and avoid overwhelming the HTML visual.

Troubleshooting and pro tips

Common pitfalls
  • Blank output: Confirm the measure returns a single string. Columns or tables won’t render.
  • Function unavailable: After adding a UDF in DAX Query View, choose Update model → Add new function.
  • Service differences: Preview features may evolve; test in the Power BI Service before rolling out.
  • Styling tweaks: Turn inline styles into variables at the top of your UDF for quick brand swaps.

Wrap-up

DAX UDFs unlock reusable HTML interfaces inside Power BI, letting you refactor one component and refresh every visual powered by it. Just like React components, you can swap styles, add variants, or compose new layouts without rewriting markup. Adapt the card, mix in SVG sparklines, or build tabular layouts—the pattern stays the same, and your visuals stay delightfully consistent.