Nathan Ortiz walks through what happens when data goes missing and developers don’t plan for it. Here are five patterns for handling data-driven content that stays accessible even when the data isn’t there.
Data isn’t always there. An editor skips the subtitle, a blog post has no author image, a showtime sells out. Handle those gaps gracefully and nobody notices. Handle them badly, by forgetting a fallback or padding layout with empty DOM nodes, and you’ve quietly broken the page for screen reader and keyboard users.
Data-driven conditional content is interface content that is rendered, hidden, updated, or removed based on the state of a data value. Think null, undefined, counts of 0, empty strings, or missing object keys that come from the data, not from something a user did. For example, a showtime sells out so the button to buy tickets is removed, or a blog post has no author image so a generic avatar is used instead. The pitfalls arrive when developers don’t handle those gaps gracefully.
The patterns outlined here are fundamental to building any front end that pulls content from an external source, such as a headless CMS, where editors fill in fields for things like headings, images, and URLs. But they apply just as well to non-CMS scenarios, anywhere data can come back null, empty, or missing entirely.
The code examples in this article are written in C# using Razor syntax.
Accessible Patterns
1. Check that the data is there
This one seems obvious, but I’ve seen it forgotten many times. Something as simple as an empty <p> element sitting in the DOM, potentially taking up space on the page. When there is a possibility that a data value might be null or empty, it’s critical to include logic that tells the software how to handle it. This often takes the form of an if statement or a ternary operator, but it can also include early returns, fallback values, explicitly rendering an alternative state or simply rendering nothing at all. In a perfect world, design will direct you on what should happen in these cases.
Using these techniques will avoid meaningless or confusing elements ending up in the accessibility tree. Otherwise, you may end up with screen reader users navigating a page and hearing, “blank, blank, blank” when they come across empty elements, or keyboard users tabbing to buttons and links that don’t do anything. The goal is to only render optional markup when the content exists. CMS fields that are optional should not output empty semantic DOM nodes.
Code example
@if (!string.IsNullOrWhiteSpace(Model.Subtitle))
{
<p>@Model.Subtitle</p>
}
2. Don’t rely on empty semantic DOM nodes for your styling
I’ve commonly seen developers rely on empty semantic elements to provide layout and styling, such as borders, spacing and more. For example, using empty list items (<li>) in an unordered list (<ul>) as fillers to maintain layout.
I understand first-hand about unreasonable requests from design and other stakeholders. It’s our job as developers to find a way to make these requests happen without compromising accessibility. Consider the empty list items situation.
Design wants a group of separate lists with equal styling and layout. Each list needs to always have six visible rows, including when there are less than six items. Best practice would be to use CSS Grid layout or Flexbox layout to achieve this. If that option is not available for constraints outside your control, the least-worst option is to add aria-hidden=”true” to any empty list items. This will ensure they are hidden from screen readers and removed from the accessibility tree, which will also correct the list count.
Code example
@{
int TOTAL_ROWS = 6;
}
<ul>
@* Iterate up to TOTAL_ROWS rather than Model.Items to ensure filler rows are generated *@
@for (int i = 0; i < TOTAL_ROWS; i++)
{
@* Pull the item at the current index if it exists *@
var item = i < Model.Items.Count ? Model.Items[i] : null;
@* Hide from screen readers if the item is null or empty *@
if (string.IsNullOrWhiteSpace(item.Name))
{
<li aria-hidden="true"></li>
}
else
{
<li>@item.Name</li>
}
}
</ul>
3. Always define a default
Always define defaults! Whether you’re initializing variables to a specific value or including a default case in a switch statement, make sure you explicitly set default values. Even if the field is optional and doesn’t render when left blank, downstream logic may depend on a value.
Imagine a content editor leaves the background colour field blank when creating a widget. The field is optional, so nothing looks obviously wrong at first. However, the background colour class variable still needs a value because it gets applied to the wrapping element’s class list. Without a default, the variable is unassigned and the class never gets added, potentially leaving the component with no background colour. If the overlaid text is a light colour and the page background is also a light colour, this creates a contrast issue. By defining black as the default background colour, the component always has a predictable, safe fallback regardless of what the editor fills in or leaves blank.
Code example
string backgroundColourClass;
switch (Model.BackgroundColour)
{
case "red":
backgroundColourClass = "bg-red";
break;
case "black":
backgroundColourClass = "bg-black";
break;
default:
backgroundColourClass = "bg-black";
break;
}
4. Provide meaningful fallbacks
Sometimes it’s a great idea to include fallback data but don’t go wild with this one just yet. While providing more information for users can be helpful, it can also add unnecessary noise for screen reader users. Be mindful and ensure it’s appropriate before providing fallbacks.
Bad example
Adding “Image description not available” to alt text for images when alt text was not provided. Screen reader users don’t need to hear this. Leave the alt text empty to ensure screen readers skip the announcement.
Good example
Dynamically injecting a resource title for a download PDF link text and using the string “resource” as a fallback if a title is not provided. This avoids fragmented link text like “Download PDF for ”.
@{
string title = resource.Title ?? "resource";
string altText = $"Download PDF for {title}.";
}
<a href="@resource.URL" …>
<img alt="@altText" …>
</a>
5. Apply styles to grouped containers
Where possible, apply styles like spacing to top-level container elements. Consider the presence of two separate widgets on a page. The first top widget has a required heading field and an optional subtitle field, where the subtitle renders below the heading. The second widget is a grid of cards. There needs to be spacing between this text content and the card grid. So where is the best place to put it?
A common mistake I’ve seen is applying the spacing to the subtitle element directly above the card grid. If your code is checking for the data before rendering markup (as it should be), the spacing will disappear along with the subtitle element when no subtitle is provided, breaking your layout.
Instead, wrap all the text content for the first widget in a container <div> and apply the bottom spacing there. This keeps the layout consistent whether a subtitle is provided or not, and requires no extra conditional logic to make it work.
Bad code example (spacing ‘mb-4’ class applied to the optional subtitle)
<div>
<h2>Heading</h2>
@if (!string.IsNullOrWhiteSpace(Model.Subtitle))
{
<p class="mb-4">@Model.Subtitle</p>
}
</div>
<div class="card-grid">...</div>
Good code example (spacing ‘mb-4’ class applied to the top-level container)
<div class="mb-4">
<h2>Heading</h2>
@if (!string.IsNullOrWhiteSpace(Model.Subtitle))
{
<p>@Model.Subtitle</p>
}
</div>
<div class="card-grid">...</div>
Without these patterns, screen reader users are left navigating pages full of noise while keyboard users tab to dead ends. Building these habits into the way you write code will give you a solid foundation for handling data-driven conditional content gracefully and accessibly. Happy (and accessible) coding!