Automating Alt Text in Power BI with DAX User‑Defined Functions

I still remember the first time I saw a demo of DAX User‑Defined Functions (UDFs) in Power BI. I was genuinely fascinated.

For years, DAX has been incredibly powerful, but it has also encouraged repetition. We copy patterns, duplicate logic, and make small wording changes across multiple measures. When UDFs were introduced, it felt like Power BI had taken a meaningful step toward a more modular and maintainable way of working, something many of us had wanted for a long time.

A few months after witnessing this feature for the very first time, I came across a blog post titled “Don’t Make My Mistakes: A Key Lesson on Alt Texts for Power BI & DataViz Contests”, published on the Power BI Community blog.

Within this article was a suggestion that immediately caught my attention, the idea of using DAX User‑Defined Functions to help automate and standardise alt text.

At the time, I hadn’t seen any follow‑up examples or deeper exploration of that idea. I haven’t found any updates since either. But the more I thought about it, the more it made sense, especially as I’d already been spending a lot of time thinking about how to make alt text easier to write, more consistent, and less fragile as reports evolve.

So I decided to investigate the possibility myself.

Why Alt Text Still Feels Hard

Alt text is one of those things we know we should do properly, particularly if we care about inclusive design. Yet in practice, it often feels awkward and time‑consuming. Writing alt text repeatedly across visuals can quickly become inconsistent, and maintaining it as reports change is harder still.

What I’ve also noticed is that manual alt text tends to drift over time. One visual mentions filters clearly, another omits them entirely, and a third uses slightly different language to describe the same concept. None of this is intentional, it’s simply a by‑product of writing narrative text in lots of different places.

At the same time, good alt text forces us to answer a deceptively simple design question: What is this visual actually telling the user?

If that’s difficult to express clearly, it’s often a sign that the visual itself needs simplifying. That’s one of the reasons I’ve started to see alt text not just as an accessibility requirement, but as a design quality signal. This is where UDFs began to feel like a natural fit.

The Idea: Standardise the Narrative, Not the Visual

The key realisation for me was that most alt text follows a repeatable narrative structure.

Regardless of the visual type, we usually want to describe what is being shown, what selections or filters apply, and what time period the data covers. The values change, but the sentence structure rarely does.

That’s exactly the kind of problem UDFs are designed to solve.

Rather than embedding narrative logic directly inside every measure, I could define that structure once, pass in dynamic values, and reuse it everywhere. This doesn’t remove the need for thoughtful design, but it does remove a lot of unnecessary repetition.

Building Context and Narrative UDFs

The first function I created (with a helping hand from Copilot) was the AltText_Context, a reusable way of describing what the user is looking at, based on slicers and the current date range.

Here’s the function I ended up with:

DEFINE
/// AltText_Context
/// Standardised context string for alt text.
/// Labels slicer selections so screen-reader users hear what each value represents.
FUNCTION AltText_Context = (
ProductText: STRING,
RegionText: STRING,
StartDate: DATETIME,
EndDate: DATETIME
) =>
"Context: "
& "Product: " & ProductText & ", "
& "Region: " & RegionText & ". "
& "Period between "
& FORMAT ( StartDate, "dd MMM yyyy" )
& " and "
& FORMAT ( EndDate, "dd MMM yyyy" )
& "."

Instead of a screen reader announcing a sequence of values with no labels, it now clearly explains what each selection represents. That extra structure makes a real difference for anyone relying on assistive technology, and it also makes the alt text easier to reason about as a report author.

Once the context was clearly defined, I realised that the next challenge wasn’t what the user was looking at, but how the result itself was being explained. Most visuals still needed a short, consistent narrative to describe the metric being shown, something that worked equally well for a KPI card, a chart, or a more detailed breakdown.

The idea behind this UDF was to standardise that opening sentence. Rather than repeatedly writing variations of “Total sales is £X” or “Sales have increased compared to last quarter” across multiple measures, I wanted a single, reusable function that could generate that narrative based on a small number of inputs.

So, again with the help of Copilot, I created the UDF AltText_ChangeNarrative. This function takes the metric label, the current value, an optional previous value, and a pre‑formatted string. If a previous value is provided, it adds a simple directional comparison, increased, decreased, or unchanged. If no previous value is passed, the comparison is deliberately omitted. This turned out to be particularly important for “to‑date” metrics, where referencing a previous quarter would be misleading rather than helpful.

DEFINE
/// AltText_ChangeNarrative
/// Creates a reusable alt-text sentence:
/// label + current formatted value + direction/percent change vs previous quarter + optional context.
/// If PreviousValue is BLANK(), the comparison clause is omitted.
FUNCTION AltText_ChangeNarrative =
(
MetricLabel: STRING,
CurrentValue: NUMERIC,
PreviousValue: NUMERIC,
CurrentFormatted: STRING,
ContextText: STRING
) =>
VAR HasPrev = NOT ISBLANK ( PreviousValue )
VAR Delta = CurrentValue - PreviousValue
VAR DeltaPct = DIVIDE ( Delta, PreviousValue )
VAR Direction =
IF (
NOT HasPrev,
BLANK (),
IF ( Delta > 0, "increased",
IF ( Delta < 0, "decreased", "remained flat" )
)
)
VAR PctText =
IF (
NOT HasPrev || Delta = 0,
BLANK (),
FORMAT ( ABS ( DeltaPct ), "0.0%" )
)
VAR ChangeClause =
IF (
NOT HasPrev,
"",
IF (
Delta = 0,
" It is unchanged versus the previous quarter.",
" It has " & Direction & " by " & PctText & " versus the previous quarter."
)
)
VAR ContextClause =
IF ( ISBLANK ( ContextText ), "", " " & ContextText )
RETURN
MetricLabel & " is " & CurrentFormatted & "." & ChangeClause & ContextClause

I created two separate UDFs because they serve two distinct purposes in the alt‑text story: one explains what data the user is looking at, and the other explains what the number means. The context UDF focuses purely on describing the filter and time context, product, region, and reporting period, which is shared across most visuals and rarely changes in structure. The change‑narrative UDF, by contrast, focuses on the metric itself, generating a clear sentence that explains the current value and, only when appropriate, how it compares to a previous period. Keeping these concerns separate makes each function simpler, more reusable, and easier to maintain, while allowing individual measures to combine them flexibly depending on the visual.

Like the context UDF, this is not intended as a finished or universal solution. It’s an example of how UDFs can be used to centralise narrative patterns and reduce repetition, especially when generating alt text that needs to be consistent, intentional, and scalable across a report.

Where Measures Still Matter

It’s important to be clear about one thing: UDFs don’t necessarily remove the need for fairly complex DAX measures.

After iterating a few times to improve the content of the alt text, I realised that the measures I intended to pass into the UDFs weren’t enough on their own. I ended up creating additional variables inside those measures so the alt text could carry more meaningful analytical detail.

I haven’t explored whether the UDFs themselves could be optimised yet, and this raises a bigger question for me about how far we can realistically standardise alt text without oversimplifying the nuance each visual needs. It’s a limitation of this early experiment, and I’m very aware of it.

This Is Just an Example (and That’s the Point)

This approach is intentionally simple. It’s not meant to be a finished framework or a definitive solution. It’s one example of how UDFs can be used to rethink alt text in Power BI.

You could take this much further by creating different context functions for different report sections, building a small library of narrative helpers, or even defining organisational standards for how alt text should be structured.

The real value here isn’t the specific wording, it’s the shift in mindset. UDFs make it possible to treat alt text as a reusable component of report design rather than a manual afterthought.

Download the Demo Power BI File

To make this easier to explore, I’m also making the Power BI file used in this example available for download. The file includes:

  • the context UDF
  • the narrative UDF
  • and the supporting DAX measures used for conditional alt text

I’d encourage you to treat it as a sandbox rather than a template. Change the wording, adjust the logic, or break it entirely and rebuild it, that’s where the learning really happens.

You can test the alt text output by turning on Windows Narrator using Windows + Ctrl + Enter, then navigating to the visual to hear how the alt text is read aloud.

Wrapping-Up

DAX User‑Defined Functions didn’t just add a new technical feature to Power BI. They introduced a way to standardise narrative patterns across a model.

Alt text was simply the first place where that really clicked for me.

If this post does anything, I hope it encourages you to experiment, not just with UDFs, but with how you think about accessibility and narrative in your reports. Sometimes the most impactful improvements come from small, thoughtful abstractions applied in the right place.

Thank you for joining me on this journey. Until next time, let’s keep crafting accessible and ethical insights that make a difference!

One comment

Leave a Reply

Discover more from Smart Frames UI

Subscribe now to keep reading and get access to the full archive.

Continue reading