Skip to content

Conversation

@uzmannazari
Copy link
Contributor

Summary

This PR refactors the BreadCrumb skin object to render semantic, list-based markup using <nav>, <ol>, and <li> elements instead of inline spans and separators.

The updated output improves accessibility and aligns breadcrumb rendering with common WCAG and ARIA breadcrumb patterns, while preserving existing behavior and customization options.

Fixes #6609

Details

  • Updated the BreadCrumb ASCX to wrap the generated breadcrumb items inside a semantic navigation container (<nav aria-label="Breadcrumb">) and an ordered list (<ol itemscope itemtype="https://schema.org/BreadcrumbList">).
  • Replaced the Label with a Literal to avoid injecting wrapper markup inside the list container (ensuring valid list structure).
  • Refactored backend rendering to output one <li> per breadcrumb item with Schema.org BreadcrumbList / ListItem microdata and position meta.
  • Applied aria-current="page" to the <li> representing the current page (instead of the <a>), matching common ARIA breadcrumb patterns.
  • Rendered separators inside each breadcrumb <li> (only when another crumb follows) and marked them decorative via aria-hidden="true", avoiding extra list items while preserving the existing Separator customization and path resolution.
  • Added default styling in type.scss for nav.dnnBreadcrumb ol to prevent list items stacking vertically after switching to list markup, and to ensure the ordered list does not display numeric markers while keeping the visual breadcrumb layout consistent:
    • list-style: none; (prevents numbering/markers)
    • display: flex; align-items: center; gap: 0.5rem; (keeps crumbs inline and spaced)
    • nav.dnnBreadcrumb ol li span { margin-inline-start: 0.5rem; display: inline-block; } (consistent spacing for the separator/content)
image

Impact

  • Improves accessibility and semantic correctness of breadcrumb markup.
  • Preserves existing breadcrumb features and configuration (RootLevel, UseTitle, HideWithNoBreadCrumb, DisableLink, Profile/Group URL parameters, Separator customization).
  • Visual output remains consistent with previous behavior via the added default styling.

Testing

  • Verified breadcrumb output for multi-level navigation paths renders inline (not stacked) and without ordered-list numbering.
  • Tested root breadcrumb rendering and HideWithNoBreadCrumb behavior.
  • Tested pages with disabled breadcrumb links.
  • Confirmed structured data output remains valid for BreadcrumbList.
image

@uzmannazari uzmannazari changed the title Patch 6 BreadCrumb skin object to render semantic, accessible breadcrumb markup Jan 8, 2026
Copy link
Contributor

@mitchelsellers mitchelsellers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One note/request included on a change to localization.

Additionally however I do believe this would be a substantially breaking change to introduce, so the planning of how this would be released is something we have to take into consideration. @dnnsoftware/approvers anyone else with thoughts on this?

@@ -1,2 +1,6 @@
<%@ Control Language="C#" AutoEventWireup="false" Inherits="DotNetNuke.UI.Skins.Controls.BreadCrumb" Codebehind="BreadCrumb.ascx.cs" %>
<asp:label id="lblBreadCrumb" runat="server" EnableViewState="False" itemprop="breadcrumb" itemscope itemtype="https://schema.org/breadcrumb"/>
<nav class="dnnBreadcrumb" aria-label="Breadcrumb">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is a literal name used for human interaction (via screen readers) I believe this should be pulling from localization

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — agreed. I’ve switched the aria-label value to come from the control’s resx (e.g. BreadCrumbAriaLabel.Text) with a sensible fallback to "Breadcrumb" if the resource is missing. This keeps screen-reader text properly localized.

Copy link
Contributor

@bdukes bdukes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like adding this markup as an option, however I don't think we can change the markup by default, I think it needs to be opt-in (e.g. add a new property for display mode or DisplayAsList or something).

var changed = false;

if (url.StartsWith("/", StringComparison.Ordinal))
if (url.StartsWith("/"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to keep the StringComparison

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — restored StartsWith(..., StringComparison.Ordinal) (and kept StringComparison usage throughout) to avoid culture-sensitive comparisons.

this.breadcrumb.Append(this.separator);
}
breadcrumb.Append("<li itemprop=\"itemListElement\" itemscope itemtype=\"https://schema.org/ListItem\">");
breadcrumb.Append("<a href=\"" + this.homeUrl + "\" class=\"" + this.cssClass + "\" itemprop=\"item\"><span itemprop=\"name\">" + this.homeTabName + "</span></a>");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either use a formattable string or AppendFormat.

Also, the tab name will need to be HTML encoded

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated — replaced string concatenations with AppendFormat/formattable strings and HTML-encoded tab names (HttpUtility.HtmlEncode) before rendering to prevent malformed markup and ensure safe output.

Removed nav and ol tags from breadcrumb control.
Updated the BreadCrumb class to enhance functionality and maintain compatibility with legacy systems. Introduced new properties for cleaner markup and list-based semantic rendering.
@uzmannazari
Copy link
Contributor Author

uzmannazari commented Jan 8, 2026

One note/request included on a change to localization.

Additionally however I do believe this would be a substantially breaking change to introduce, so the planning of how this would be released is something we have to take into consideration. @dnnsoftware/approvers anyone else with thoughts on this?

Totally agree on the breaking-change risk. I’ve updated the implementation so the new semantic/list markup is opt-in via a new property (UseListMarkup, default false).
When UseListMarkup="false" the output stays in the legacy format (to preserve existing skins/CSS), and only when true it renders the <nav>/<ol>/<li> semantic structure.

@uzmannazari
Copy link
Contributor Author

I like adding this markup as an option, however I don't think we can change the markup by default, I think it needs to be opt-in (e.g. add a new property for display mode or DisplayAsList or something).

Agreed — I changed this to opt-in. Added UseListMarkup (default false) so legacy markup remains the default behavior. Setting UseListMarkup="true" enables the semantic list-based breadcrumb output.

@uzmannazari
Copy link
Contributor Author

uzmannazari commented Jan 8, 2026

@bdukes and @mitchelsellers Thanks for the feedback!

I’ve updated the PR to minimize breaking changes and address review notes:

Added UseListMarkup (default false) so legacy output remains the default, and semantic markup is opt-in.

Kept legacy wrapper <span itemprop="breadcrumb" ...> when UseListMarkup=false to preserve existing skin/CSS behavior.

Switched the control output to Literal to avoid invalid nesting; list-mode now starts with<nav>directly.

Localized the aria-label via resx with fallback.

Restored StringComparison.Ordinal, kept doc comments, and ensured tab names are HTML-encoded with AppendFormat usage.

Comment on lines +48 to +58
nav.dnnBreadcrumb ol {
list-style: none;
display: flex;
align-items: center;
gap: 0.5rem;
}

nav.dnnBreadcrumb ol li span {
margin-inline-start: 0.5rem;
display: inline-block;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a big fan of adding this css here, could we create a "skinobjects" folder similar to the "modules" folder and put in there one file per skinobject maybe?

Image

Copy link
Contributor

@mitchelsellers mitchelsellers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry upon further review one small changes suggested.

// This mimics the old asp:Label output wrapper and keeps schema breadcrumb attrs.
var breadcrumb = new StringBuilder(
"<span itemprop=\"breadcrumb\" itemscope itemtype=\"https://schema.org/breadcrumb\">" +
"<span itemscope itemtype=\"http://schema.org/BreadcrumbList\">");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be broken out into either one initialization & one Append call or just one big string to avoid the concatenated string being allocated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Breadcrumb WCAG compliant

4 participants