Skip to content
Consultation

Flat Gray Tag Chips Never Invited a Click

Adityo Guni Waluyo

Flat gray tag chips all carry equal weight. A 10% primary tint plus a hairline inset ring makes them feel alive and clickable again.

I was staring at the row of tag chips in my blog sidebar. Flat gray rectangles, each one the same as the next, like bathroom tiles lined up with no reason to pick any. Every tag carried equal weight, none invited a click. The active chip was orange, sure, but the rest looked like neutral labels someone forgot to style.

Before this commit, the chips used bg-muted with text-muted-foreground, and on hover they darkened a bit to bg-muted/70. The count inside each chip had its own class text-muted-foreground/60, a fainter gray on top of the gray. It worked, but it read as a default, not a decision. My first move was to just crank the hover darkening, thinking that would make them feel interactive. Didn't. The row still looked dead because every chip started from the same mute base.

Why a soft tint beats flat gray

The fix swaps the flat gray for a soft primary tint. bg-primary/10 gives each chip a 10% wash of the brand color. That alone gives identity without noise. The Tailwind CSS docs on background-color note the opacity modifier controls the opacity of the background color, so /10 is exactly that, not a separate shade I have to maintain.

To give a subtle edge, I added ring-1 ring-primary/15 ring-inset. In Tailwind v3, ring-1 is a 1px box-shadow and ring-inset forces it inside the element, so layout never shifts (v3 ring-width docs). The hairline reads as a quiet border, not a loud outline. It also helps chips at the sidebar edge stay crisp without eating padding. Since the sidebar sits flush to the content column, an outer ring would have clipped, another reason inset won.

// Before: flat gray, every tag the same weight
<Link className={cn(
  "rounded-[6px] px-2.5 py-0.5 text-[11px]",
  "bg-muted text-muted-foreground hover:bg-muted/70 hover:text-foreground",
)}>
  #{tag}
  <span className="ml-1 text-muted-foreground/60">{count}</span>
</Link>

// After: soft primary tint + hairline inset ring, hover deepens
<Link className={cn(
  "rounded-[6px] bg-primary/10 px-2.5 py-0.5 text-[11px] text-primary/80 ring-1 ring-primary/15 ring-inset transition-colors hover:bg-primary/15 hover:text-primary",
  activeTag === tag && `${ACCENT_BG} font-medium ${ACCENT}`,
)}>
  #{tag}
  <span className="ml-1 opacity-60">{count}</span>
</Link>

The small detail that tidies it: the count no longer uses a separate color class. It just sits at opacity-60, so it inherits the chip's text color. Previously the count was a faded gray of its own, which meant three color stops to reason about. Now it's one. On hover, when the chip text rises to full text-primary, the count strengthens too. Consistent, no extra rules. Hover deepens the tint to bg-primary/15 rather than switching to a new color, which keeps the motion calm. I like that the active chip still gets the accent via ACCENT_BG and ACCENT, so the selected state pops without breaking the family.

Tailwind v4 and the ring-inset question

I run Tailwind v4. The v4 box-shadow docs ship a new inset-ring utility family designed for inside rings. The v4 upgrade guide shows the renamed-utilities table does not list ring-inset, but in the installed v4 code ring-inset still exists and works. So both are valid. I picked to keep ring-inset because the codebase already uses it; the v4 way would be inset-ring-1. Pick one and stay consistent, that's my call.

Flat gray isn't neutral, it has no opinion. Even tiny chips in a sidebar deserve a deliberate choice. I'll keep the soft tint and the existing ring-inset utility until I refactor the whole styling layer, then maybe switch to inset-ring-1. The point is to decide, not accept the muted default.

Related articles