DEV Community

Cover image for Your Chevron Looks Fine. Your Button Has No Name.
Svg/icons
Svg/icons

Posted on

Your Chevron Looks Fine. Your Button Has No Name.

A chevron can be perfectly aligned, beautifully drawn, and completely unhelpful when its button receives keyboard focus.

You see a control that opens something. Assistive technology needs a name that explains what that control does.

That gap is easy to miss in a screenshot review. It is also easy to repeat across an entire application when the button lives inside a reusable component.

Before polishing another icon, inspect the name of the button around it.

A real report, a familiar integration mistake

On November 4, 2026, a shadcn/ui issue reported an unnamed chevron trigger in the Base UI version of ComboboxInput. The reporter described an axe button-name violation and a generic button announcement.

A proposed fix adds visually hidden text to trigger and clear controls. Both the issue and proposal were open when checked on November 5, 2026.

This is a useful starting point for a smaller question: what should your own icon-button component require from its caller?

The examples below are original, simplified React examples. They demonstrate button naming, not a replacement implementation of shadcn/ui's combobox. No independent screen-reader recordings or axe results are claimed here.

Three buttons. The same pixels. Different names.

Start with a deliberately simple chevron:

function ChevronDown() {
  return (
    <svg
      width="20"
      height="20"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      focusable="false"
    >
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
}
Enter fullscreen mode Exit fullscreen mode

The SVG is decorative here: the surrounding button will carry the action's name.

1. The version that leaves the action unnamed

<button type="button">
  <ChevronDown />
</button>
Enter fullscreen mode Exit fullscreen mode

There is no text, aria-label, or naming reference. With the SVG hidden from assistive technology, this button has no accessible name.

Its shape cannot tell the accessibility tree whether the action is “Show filters,” “Expand invoice,” or “Choose country.”

2. Give the button an explicit label

<button type="button" aria-label="Show filters">
  <ChevronDown />
</button>
Enter fullscreen mode Exit fullscreen mode

For this example, the expected accessible name is Show filters.

Notice where the label goes: on the interactive element. Name the action, rather than describing the asset as “down chevron.”

3. Keep the label in the button's content

<button type="button">
  <ChevronDown />
  <span className="sr-only">Show filters</span>
</button>
Enter fullscreen mode Exit fullscreen mode

With suitable CSS, the text remains available to assistive technology while being visually hidden:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
Enter fullscreen mode Exit fullscreen mode

The class name itself does nothing unless your stylesheet defines it. Do not replace this with display: none, visibility: hidden, or the hidden attribute: those would hide this text from the accessibility tree too.

Version Expected accessible name Expected naming outcome
SVG only, as shown Empty Fails button-name
Button with aria-label Show filters Satisfies the naming rule
Button with visually hidden text Show filters Satisfies the naming rule

These are expected outcomes for the snippets, not measured test results. Passing a naming rule does not prove that a complete component is accessible.

Put the requirement in your React API

A code review comment fixes one instance. A required component prop makes the requirement visible at every call site.

Here is a deliberately small TypeScript wrapper for decorative icons:

import type { ComponentPropsWithoutRef, ReactNode } from "react";

type IconButtonProps = Omit<
  ComponentPropsWithoutRef<"button">,
  "children" | "aria-label" | "aria-labelledby" | "dangerouslySetInnerHTML"
> & {
  label: string;
  icon: ReactNode;
};

export function IconButton({
  label,
  icon,
  type = "button",
  ...buttonProps
}: IconButtonProps) {
  if (!label.trim()) {
    throw new Error("IconButton requires a non-empty action label.");
  }

  return (
    <button {...buttonProps} type={type} aria-label={label}>
      <span aria-hidden="true">{icon}</span>
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Usage makes the intended action explicit:

<IconButton
  label="Clear search"
  icon={<ClearIcon />}
  onClick={clearSearch}
/>
Enter fullscreen mode Exit fullscreen mode

ClearIcon and clearSearch represent your application's icon component and handler. Pass decorative icon markup, never another interactive control, into icon.

The wrapper requires a label and rejects whitespace-only strings. It cannot judge whether “Clear search” accurately describes your handler. That still belongs in your review—and your translation workflow.

For repeated row actions, add useful context: Delete invoice INV-1042 is more distinguishable than twenty buttons named Delete.

A name does not implement a combobox

Our snippets intentionally isolate one defect.

If your chevron opens a combobox popup, adding a label does not implement keyboard navigation, focus management, selection, or expanded-state reporting. Preserve the behavior and ARIA relationships supplied by the component library, following the WAI-ARIA combobox pattern.

Do not copy a standalone button's tab behavior into a composite widget without checking that widget's keyboard model.

Likewise, aria-expanded communicates state; it does not supply the missing action name.

Verify the rendered control

After integrating a fix:

  1. Inspect the actual rendered button in your browser's accessibility tools. Check its role and computed name.
  2. Run axe and inspect the specific button-name result. Keep other failures separate.
  3. Exercise the component with the keyboard, including opening, closing, and returning focus where appropriate.
  4. Check it with your supported browser and screen-reader combinations. Record the combination before quoting an announcement.

A useful regression assertion targets the action users need:

// React Testing Library, after rendering your component:
expect(
  screen.getByRole("button", { name: "Clear search" })
).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

This assertion uses React Testing Library's screen and the jest-dom matchers. It checks that a named button exists; it does not replace interaction tests or manual testing.

Choose the icon. Name the action.

When browsing SVGicons, you may choose the same chevron for several parts of your interface. Its visual consistency is useful. Its accessible label still needs to come from each action's context.

A file called chevron-down.svg cannot know whether your button reveals filters, shipping options, or account settings.

Build that decision into the component that uses it.

The next time you review an icon-only button, ask: if I could not see this icon, would its name tell me what happens when I activate it?


References

Top comments (0)