Prologue
A while ago, I decided to develop a fully accessible main navigation component in React and write a series of articles documenting the steps it took to create a non-trivial accessible component.
In my last ; a combination I realized has not been commonly considered by many of the developers I've worked with.
This article focuses once again on the screen's perceivability, with design considerations at the forefront.
Note: This article is one of a series demonstrating how to build a React navigational component from scratch while considering accessibility through the process. The articles are accompanied by a .
Examples have been updated in this release, enabling keyboard handling for a single list. Examples include a vertically aligned single list and horizontally aligned components with links and buttons for verifying operability.
While code examples are written in JavaScript for brevity, all actual code is written in Typescript and targets React 19.x. Examples use Next.js 16.x, which is not required to run the navigation component.
The and styling, whether through browser zoom or when a user changes the base font size. Without a solid layout, any resizing required by
The image above, using the browser default font size of 16px, is unstyled except for the styling applied to the individual base components.
A horizontal navigation can be considered a system component, so its styling is placed in the system-component layer. In a nested CSS structure, everything can be styled with a single class applied to the top-level element, in this case <nav />. Top and bottom padding are applied to the nav element to provide some space. A design token --min-list-width, to be discussed later, is set purely as a default.
Working on the top row first, rules are set for the nav element's only direct descendant, the topmost unordered list; The list element already sets display: flex and the basics for both horizontal and vertical displays, so the only necessary rules here are around alignment and setting a 24 relative-pixel column gap. Only list items that are directly descended from the element's top unordered list are given position: relative and have some flex alignment rules applied. The top-row buttons and links remove any left padding.
.
Return to Content Links
SubList Layout
@layer system-component {
nav.horizontal-navigation {
/* Layout */
/* Top Row nav > ul > li */
& > ul {
... & > li {
position: relative;
...
/* sub navigation (not top row) */
& > ul {
min-width: calc(var(--sp-px) * var(--min-list-width));
padding: 0;
position: absolute;
top: calc(var(--sp-px) * 36);
width: fit-content;
z-index: 3;
& li {
width: 100%;
&:first-child {
padding-top: calc(var(--sp-px) * 8);
}
&:last-child {
padding-bottom: calc(var(--sp-px) * 8);
}
& button,
& a[href] {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
padding:
calc(var(--sp-px) * 8)
calc(var(--sp-px) * 16)
0
calc(var(--sp-px) * 8);
}
}
& ul {
padding: 0 calc(var(--sp-px) * 16) 0 0;
position: relative;
& > li {
padding-left: calc(var(--sp-px) * 16);
}
}
}
}
}
}
}
GitHub (release 0.4.0) - , using the style attribute to communicate information between JavaScript and CSS. In this case, I want to guarantee the minimum width for a sublist is the same as the width of its button in the top row. So modifications need to be made to the SubNavigation component.
SubNavigation
export default function SubNavigation({...}) {
...
const [isSubListOpen, setIsSubListOpen] = useState(false);
const [listWidth, setListWidth] = useState(1);
…
useLayoutEffect(() => {
setListWidth( buttonRef.current!.offsetWidth);
}, [buttonRef, setListWidth]);
...
const listItemProps = {
cx: cx,
style: { "--min-list-width": listWidth } as CSSProperties,
};
…
}
GitHub (release 0.4.0) -
The read-only offsetWidth property is available on any HTML element. It returns a pixel measurement of an element's width, including borders and padding. Sublists can use the returned width, as defined by the top-row button, to set a relative pixel width, guaranteeing the list appears at least as wide as the button that controls it.
Buttons and Links
The component-level gap is unset for each ul directly descended from the top-level ul, and buttons and links have their appearance standardized. Background and border colors are set to transparent, allowing the elements to blend into the designated background color. A border-width is applied, along with a transparent border color, to prevent state changes from causing label shifting. Colors are applied through the .
Some changes to the underlying component styling are necessary. A gap originally set up in the styles associated with the list component is unset, and text alignment is reset to left instead of the original button style of center. Additionally, any item within a list item is set to keep text on a single line rather than wrap over multiple lines.
A background for links and buttons is finally applied, so text under the absolutely positioned sublists no longer bleeds through. Font weights are applied along with a border.
Is it perfect? There's always more to do, but both the top row and the displayed sublists are readable, and each focusable element in the sublists has a target area larger than the minimum target size.
Return to Content Links
Summary
The initial layout of a horizontal navigation component is simplified by nesting the CSS under a single class and using direct descendant selectors to target the top row and its sublists. Layout is separated from appearance, and the component remains consistent in proportions and ratios when a larger browser font is applied.
SOCIAL SHARE CARD GENERATOR