# Use outline-hidden, Not outline-hidden

**MUST** · **ID:** `interactions-outline-hidden` · **Category:** interactions
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/interactions-outline-hidden

> MUST: In Tailwind v4, `outline-hidden` sets a real `outline-style: none` and KILLS the focus ring in Windows forced-colors / high-contrast mode, where custom `ring-*` shadows are stripped away — leaving those users with no focus indicator at all. v3's `outline-hidden` emitted an invisible outline that still showed up in forced colors; that behavior was renamed `outline-hidden`. So the ubiquitous `focus:outline-hidden` + custom ring pattern silently regresses on upgrade: use `focus-visible:outline-hidden` PLUS a visible replacement ring. (Distinct from `interactions-clear-focus` — having a ring — and `interactions-focus-ring-shadow` — box-shadow vs outline for radius.)

In v4 outline-hidden really removes the outline and kills the focus ring in forced-colors mode — outline-hidden is the utility that used to be safe

This is a real accessibility regression introduced purely by a rename. `focus:outline-hidden` + a custom `ring-*` is the single most common focus pattern on the web, and in v3 it was safe: `outline-hidden` did not remove the outline, it emitted a *transparent* one, which Windows forced-colors / high-contrast mode repaints as a visible system outline. In v4 that same class name now sets a literal `outline-style: none`. Forced-colors mode also overrides your colors and drops `box-shadow`, so the `ring` you replaced the outline with is not there to save you — the user is left with a focused control and no indicator at all, on the exact configuration that most depends on one. Nothing errors, nothing changes in your browser, and the upgrade ships. The fix is the rename Tailwind documents: `focus-visible:outline-hidden` (the old, transparent-outline behaviour) **plus** the visible replacement ring, `focus-visible:ring-2 focus-visible:ring-ring`. Reserve the new `outline-hidden` for the rare case where you genuinely want no outline in any mode. This is a different rule from `interactions-clear-focus`, which is about having a visible ring at all, and from `interactions-focus-ring-shadow`, which is about drawing that ring with box-shadow so it follows the border radius; both assume the forced-colors fallback you are about to delete here.

## Rule snippet

```tsx
// v4: strips the forced-colors fallback outline
<button class="focus:outline-hidden focus:ring-2 focus:ring-ring" />
// keeps it
<button class="focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" />
```

## Bad — do not do this

`interactions-outline-hidden-bad`

```tsx
export function OutlineHiddenBad() {
  return (
    <div className="w-full max-w-md">
      <div className="bg-card border border-border rounded-lg p-6 space-y-6">
        <p className="text-xs text-muted-foreground">
          Tab into both buttons. Same markup —{' '}
          <code>focus:outline-none</code> plus a custom ring — rendered as your browser paints it, and as
          Windows forced-colors mode paints it.
        </p>

        <div className="space-y-2">
          <p className="text-[11px] uppercase tracking-wide text-muted-foreground">Your browser</p>
          <div className="border border-border rounded-md p-5 flex justify-center">
            {/* In v4, outline-none now sets a REAL outline-style: none. */}
            <button className="px-5 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium focus:outline-none focus:ring-2 focus:ring-ring">
              Save changes
            </button>
          </div>
          <p className="text-xs text-muted-foreground">The ring is there. Nothing looks wrong.</p>
        </div>

        <div className="space-y-2">
          <p className="text-[11px] uppercase tracking-wide text-muted-foreground">
            Forced colors / high contrast (simulated)
          </p>
          {/* Forced-colors overrides author colors and drops box-shadow — so the ring is gone.
              outline-none removed the transparent outline that used to be repainted here.
              This button therefore has NO focus indicator left: it renders exactly that. */}
          <div className="bg-foreground rounded-md p-5 flex justify-center">
            <button className="px-5 py-2 rounded-md bg-foreground text-background text-sm font-medium border border-background focus:outline-none">
              Save changes
            </button>
          </div>
          <p className="text-xs text-muted-foreground">
            The system stripped the <code>box-shadow</code> ring, and <code>outline-none</code> deleted the
            outline it would have repainted. Focus is on the button — and completely invisible.
          </p>
        </div>
      </div>
      <p className="text-xs text-error mt-4">
        <code>outline-none</code> in v4 kills the forced-colors focus indicator — a silent a11y regression on upgrade
      </p>
    </div>
  );
}
```

## Good — do this

`interactions-outline-hidden-good`

```tsx
export function OutlineHiddenGood() {
  return (
    <div className="w-full max-w-md">
      <div className="bg-card border border-border rounded-lg p-6 space-y-6">
        <p className="text-xs text-muted-foreground">
          Tab into both buttons. Same markup —{' '}
          <code>focus-visible:outline-hidden</code> plus a replacement ring — rendered as your browser paints
          it, and as Windows forced-colors mode paints it.
        </p>

        <div className="space-y-2">
          <p className="text-[11px] uppercase tracking-wide text-muted-foreground">Your browser</p>
          <div className="border border-border rounded-md p-5 flex justify-center">
            {/* outline-hidden is v3's old outline-hidden: it hides the outline visually
                but keeps a transparent one for forced-colors to repaint. */}
            <button className="px-5 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
              Save changes
            </button>
          </div>
          <p className="text-xs text-muted-foreground">The ring is there — identical to what you had in v3.</p>
        </div>

        <div className="space-y-2">
          <p className="text-[11px] uppercase tracking-wide text-muted-foreground">
            Forced colors / high contrast (simulated)
          </p>
          {/* The ring's box-shadow is still dropped by forced-colors — but the transparent
              outline survived, and the system repaints it in its own high-contrast color.
              Rendered here as a real outline in the panel's contrasting token. */}
          <div className="bg-foreground rounded-md p-5 flex justify-center">
            <button className="px-5 py-2 rounded-md bg-foreground text-background text-sm font-medium border border-background focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-background">
              Save changes
            </button>
          </div>
          <p className="text-xs text-muted-foreground">
            The <code>box-shadow</code> ring is gone here too, but the outline <code>outline-hidden</code> kept
            is repainted by the system. The focused button is still unmistakable.
          </p>
        </div>
      </div>
      <p className="text-xs text-success mt-4">
        <code>outline-hidden</code> + a visible ring: sighted users get your ring, forced-colors users keep theirs
      </p>
    </div>
  );
}
```

## References

- [Tailwind v4: Upgrade guide (renamed outline utility)](https://tailwindcss.com/docs/upgrade-guide)
- [Tailwind v4: outline-style](https://tailwindcss.com/docs/outline-style)
- [MDN: forced-colors](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors)
