fix: improve header navigation accessibility

- Current page in nav is now a span instead of a link (no self-links)
- Logo links to home page, except when already on home
- Use aria-current="page" with accent underline for current page indicator
- Extract logo_content, logo_inner, and nav_item helper components
- Update CSS to target both a and span elements in .shop-nav

This follows WCAG accessibility guidelines - links that point to
the current page are confusing for screen reader users.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamey Greenwood 2026-01-19 23:43:54 +00:00
parent fe29c1ad36
commit 50d7f135bc
2 changed files with 142 additions and 54 deletions

View File

@ -221,17 +221,19 @@
} }
/* Nav link styling with active state indicator */ /* Nav link styling with active state indicator */
.shop-nav a { .shop-nav a,
.shop-nav span {
padding: 0.5rem 0; padding: 0.5rem 0;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: border-color 0.2s ease, color 0.2s ease; transition: border-color 0.2s ease, color 0.2s ease;
}
&:hover { .shop-nav a:hover {
color: var(--t-text-primary); color: var(--t-text-primary);
} }
&[aria-current="page"] { .shop-nav a[aria-current="page"],
.shop-nav span[aria-current="page"] {
color: var(--t-text-primary); color: var(--t-text-primary);
border-bottom-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-bottom-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
} }
}

View File

@ -281,55 +281,25 @@ defmodule SimpleshopThemeWeb.ShopComponents do
<% end %> <% end %>
<div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;"> <div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;">
<%= case @theme_settings.logo_mode do %> <.logo_content
<% "text-only" -> %> theme_settings={@theme_settings}
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);"> logo_image={@logo_image}
<%= @theme_settings.site_name %> active_page={@active_page}
</span> mode={@mode}
<% "logo-text" -> %>
<%= if @logo_image do %>
<img
src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name}
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
/> />
<% end %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% "logo-only" -> %>
<%= if @logo_image do %>
<img
src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name}
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
/>
<% else %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
<% _ -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
</div> </div>
<nav class="shop-nav hidden md:flex" style="gap: 1.5rem; position: relative; z-index: 1;"> <nav class="shop-nav hidden md:flex" style="gap: 1.5rem; position: relative; z-index: 1;">
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<a href="#" phx-click="change_preview_page" phx-value-page="home" aria-current={if @active_page == "home", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Home</a> <.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} />
<a href="#" phx-click="change_preview_page" phx-value-page="collection" aria-current={if @active_page in ["collection", "pdp"], do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Shop</a> <.nav_item label="Shop" page="collection" active_page={@active_page} mode={:preview} active_pages={["collection", "pdp"]} />
<a href="#" phx-click="change_preview_page" phx-value-page="about" aria-current={if @active_page == "about", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">About</a> <.nav_item label="About" page="about" active_page={@active_page} mode={:preview} />
<a href="#" phx-click="change_preview_page" phx-value-page="contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;">Contact</a> <.nav_item label="Contact" page="contact" active_page={@active_page} mode={:preview} />
<% else %> <% else %>
<a href="/" aria-current={if @active_page == "home", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Home</a> <.nav_item label="Home" href="/" active_page={@active_page} page="home" />
<a href="/collections/all" aria-current={if @active_page in ["collection", "pdp"], do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Shop</a> <.nav_item label="Shop" href="/collections/all" active_page={@active_page} page="collection" active_pages={["collection", "pdp"]} />
<a href="/about" aria-current={if @active_page == "about", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">About</a> <.nav_item label="About" href="/about" active_page={@active_page} page="about" />
<a href="/contact" aria-current={if @active_page == "contact", do: "page"} style="color: var(--t-text-secondary); text-decoration: none;">Contact</a> <.nav_item label="Contact" href="/contact" active_page={@active_page} page="contact" />
<% end %> <% end %>
</nav> </nav>
@ -375,6 +345,83 @@ defmodule SimpleshopThemeWeb.ShopComponents do
defp logo_url(logo_image, _), do: "/images/#{logo_image.id}" defp logo_url(logo_image, _), do: "/images/#{logo_image.id}"
# Logo content that links to home, except when already on home page.
# This follows accessibility best practices - current page should not be a link.
attr :theme_settings, :map, required: true
attr :logo_image, :map, default: nil
attr :active_page, :string, default: nil
attr :mode, :atom, default: :live
defp logo_content(assigns) do
is_home = assigns.active_page == "home"
assigns = assign(assigns, :is_home, is_home)
~H"""
<%= if @is_home do %>
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
<% else %>
<%= if @mode == :preview do %>
<a
href="#"
phx-click="change_preview_page"
phx-value-page="home"
style="display: flex; align-items: center; text-decoration: none;"
>
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
</a>
<% else %>
<a href="/" style="display: flex; align-items: center; text-decoration: none;">
<.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} />
</a>
<% end %>
<% end %>
"""
end
attr :theme_settings, :map, required: true
attr :logo_image, :map, default: nil
defp logo_inner(assigns) do
~H"""
<%= case @theme_settings.logo_mode do %>
<% "text-only" -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% "logo-text" -> %>
<%= if @logo_image do %>
<img
src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name}
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
/>
<% end %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% "logo-only" -> %>
<%= if @logo_image do %>
<img
src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name}
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
/>
<% else %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
<% _ -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
"""
end
defp header_background_style(settings, header_image) do defp header_background_style(settings, header_image) do
"position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <> "position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <>
"background-image: url('/images/#{header_image.id}'); " <> "background-image: url('/images/#{header_image.id}'); " <>
@ -383,6 +430,45 @@ defmodule SimpleshopThemeWeb.ShopComponents do
"background-repeat: no-repeat; z-index: 0;" "background-repeat: no-repeat; z-index: 0;"
end end
# Navigation item that renders as a span (not a link) when on the current page.
# This follows accessibility best practices - current page should not be a link.
attr :label, :string, required: true
attr :page, :string, required: true
attr :active_page, :string, required: true
attr :href, :string, default: nil
attr :mode, :atom, default: :live
attr :active_pages, :list, default: nil
defp nav_item(assigns) do
# Allow matching multiple pages (e.g., "Shop" is active for both collection and pdp)
active_pages = assigns.active_pages || [assigns.page]
is_current = assigns.active_page in active_pages
assigns = assign(assigns, :is_current, is_current)
~H"""
<%= if @is_current do %>
<span aria-current="page" style="color: var(--t-text-secondary); text-decoration: none;">
{@label}
</span>
<% else %>
<%= if @mode == :preview do %>
<a
href="#"
phx-click="change_preview_page"
phx-value-page={@page}
style="color: var(--t-text-secondary); text-decoration: none; cursor: pointer;"
>
{@label}
</a>
<% else %>
<a href={@href} style="color: var(--t-text-secondary); text-decoration: none;">
{@label}
</a>
<% end %>
<% end %>
"""
end
@doc """ @doc """
Renders the cart drawer (floating sidebar). Renders the cart drawer (floating sidebar).