DEV Community

Caleb Hearth
Caleb Hearth

Posted on • Originally published at calebhearth.com on

CSS-only Submenu Navigation with Post Tags

See this in action at https://calebhearth.com

What's in the navigation

Our posts have categories, which I've used as a "collection" or "series" of posts that are on related topics. Recently, I've been writing up campaign notes for my D&D session as the characters themselves in a category simply called "Campaign Logs". I'm also hoping to continue to write this type of post as part of a rebuilding in Rails / redesigning the site series, which would be a new category as well.

Besides categories, I also use tags to give hints for "related articles"-type features to encourage readers to continue to other articles when they've completed reading one. There are two tags that I currently consider to be "special" in that they are more general tags that encompass most of the content on the blog between them - Programming and D&D.

Given these categories, tags, and special tags, for this first pass at navigation I want a taxonomy that includes a "home" link, links with submenus for each of Programming and D&D, and a non-linked submenu for all categories, similar to:

Caleb Hearth Programming       D&D             Collections
              Jekyll            Campaign Log    Fake It β€˜Til You Make It
              Rails             Reviews         Appendix E
              Best Practices    Books           Campaign Logs
              Postgres                          Play The Heroes
Enter fullscreen mode Exit fullscreen mode

To accomplish this, I hard-code the Programming and D&D tags into the view. I iterate over an array %w(programming dnd) and create the markup for those menu items. I use Post.tagged to pull all posts with the current tag, flat_map all of the posts in that collection into their respective tags, use Ruby 2.7's new Array#tally to count incidents of each tag, discard any that have only 1 associated post and the current special tag, and create a submenu showing each of those remaining tags. I'm not concerned about cases where a tag shows up under more than one special tag - if that comes up then I think it's fine for that item to be duplicated in multiple submenus as those posts may be related to those "categories".

Post
  .tagged(special_tag)
  .flat_map(&:tags)
  .tally
  .reject { |tag, count| tag == special_tag || count < 2 }
  .each do |tag, _|
Enter fullscreen mode Exit fullscreen mode

This should probably be pulled into a method or helper - one thing I considered was to create some sort of collection proxy object similar to Rails' ActiveRecord::Base objects, so that I can call "scopes" on it and this could be simplified somewhat, but for now I'll leave it as a bit of a mess.

For the Collections submenu, I simply create links for each entry in Post.categories.

The full ERB as of now is:

<nav aria-label="Main">
  <ul role="menubar" aria-hidden="false">
    <li role="menuitem">
      <%= link_to "Caleb Hearth", root_path %>
    </li>
    <% %w(programming dnd).each do |group| %>
      <li role="menuitem" aria-haspopup="true">
        <%= link_to group.titleize, tag_path(group) %>
        <ul role="menu">
          <% Post.tagged(group).map(&:tags).flatten.tally.reject { _2 < 2 || _1 == group }.each do |tag, _| %>
            <li role="menuitem">
              <%= link_to tag.titleize, tag_path(tag) %>
            </li>
          <% end %>
        </ul>
      </li>
    <% end %>
    <li role="menuitem" aria-haspopup="true">
      <%= link_to "Collections", "#collections" %>
      <ul role="menu">
        <% Post.categories.each do |category, _| %>
          <li role="menuitem" id="collections">
            <%= link_to category.titleize, category_path(category.parameterize) %>
          </li>
        <% end %>
      </ul>
    </li>
  </ul>
</nav>
Enter fullscreen mode Exit fullscreen mode

Note that most tags include ARIA attributes&emdash;I took my cues on how to build that out from the Navigation section of the A11y Style Guide.

CSS-only submenu styles

This change involves very minimal styles as I was focused more on functionality of the menu than on appearance, which I'll come back to later.

I wanted each menu item in the main nav to have approximately equal space, for the logo/main page link (which is simply my name) to have a little more space, and for the nav to take up the whole screen. CSS Flex is a perfect candidate for that - I set display: flex on the main ul, flex: 2 for the first list item there (which is the main page link), and flex: 1 for the remaining list items:

nav > ul
  display: flex

    > li
      flex: 1

      &:first-child
        flex: 2
Enter fullscreen mode Exit fullscreen mode

With this markup, I get the spacing I want horizontally, but the submenus are still visible. With help from Una Kravets' post Solved with CSS! Dropdown Menus on CSS Tricks, I ended up using display, opacity, and visible directives to hide submenus with the selector nav > ul > li ul and show them with nav > ul > li:hover ul, nav > ul > li:focus-within ul, nav > ul > li ul:hover. This covers showing the submenu in the cases where a cursor is hovering over the menu item, when either the menu item has focus or focus is anywhere within the menu item (used for screen readers and keyboard navigation with tabstop), and when the submenu has a hover state.

With the content of the navigation generated and the CSS-only styles for the submenu navigation functionality in place, I'm pretty happy with the new version of navigation here.

One difference between this nav and the one on the old site is that social links for Twitter and GitHub are absent, as are the Hotline Webring back and forward links. I intend to move these to a footer-only nav that will be implemented later, as I am no longer in love with using identical header and footer navs as I did on the old site.

Top comments (0)