---
area: "Learn"
parent: "Scrolling & focus chapter"
url: "https://unpoly.com/learn/focus-visibility"
---
<nav aria-label="Unpoly docs">[All docs](https://unpoly.com/index.md) · [Scrolling & focus chapter](https://unpoly.com/learn/scrolling-and-focus.md)</nav>

# Focus ring visibility

Unpoly lets you control whether a [focused](https://unpoly.com/learn/focus.md) fragment shows a visible focus ring. Elements focused by Unpoly get a CSS class that says whether a ring is wanted, so your stylesheet can show rings for keyboard users and hide them for mouse and touch users.

Because Unpoly [often focuses new content](https://unpoly.com/learn/focus.md#auto), you may see focus outlines appear in unexpected places. Try to resist an initial instinct to just remove focus rings globally using CSS. Focus rings are important for users of keyboards and screen readers to orient themselves as the focus moves on the page. However, mouse and touch users often dislike the visual effect of a focus ring.

<nav aria-label="Contents">

#### Contents

- [Visibility classes](#classes)
- [Hiding unwanted focus rings with CSS](#hide)
- [Styling focus rings on a new component](#show)
- [Customizing focus ring visibility](#visibility-config)
- [Overriding visibility for a single interaction](#per-interaction)

</nav>

## Visibility classes {#classes}

To help your CSS show or hide focus rings in the right situation, Unpoly assigns CSS classes to the elements it focuses:

- If the user [interacted with the keyboard](https://unpoly.com/api/up.event.inputDevice.md), or if the focused element is a [form field](https://unpoly.com/api/up.form.config.md#config.fieldSelectors), Unpoly sets an [`.up-focus-visible`](https://unpoly.com/api/up-focus-visible.md) class.
- If the user interacted via mouse, touch or stylus, Unpoly sets an [`.up-focus-hidden`](https://unpoly.com/api/up-focus-hidden.md) class instead.

```html
<main class="up-focus-hidden" tabindex="-1">
  Content focused after a mouse click
</main>
```

> #### Note
>
> The web platform uses the [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo-class to indicate focus ring visibility. However, browsers often incorrectly apply `:focus-visible` during script-driven navigations like Unpoly.
>
> Unpoly tries to force or unset `:focus-visible` whenever it sets a visibility class, but can only do so in [some browsers](https://caniuse.com/mdn-api_htmlelement_focus_options_focusvisible_parameter).

## Hiding unwanted focus rings with CSS {#hide}

You may see unwanted focus rings that you inherited from a [user agent stylesheet](https://bitsofco.de/a-look-at-css-resets-in-2018/) or from a CSS framework like Bootstrap. You can remove these outlines for most mouse and touch interactions using CSS like this:

```css
:focus:not(:focus-visible, .up-focus-visible),
.up-focus-hidden {
  outline: none !important;
}
```

[By default](https://github.com/unpoly/unpoly/blob/master/src/unpoly/viewport.sass), Unpoly removes the `outline` CSS property from elements with an [`.up-focus-hidden`](https://unpoly.com/api/up-focus-hidden.md) class.

> #### Tip
>
> CSS frameworks might render focus rings using properties other than `outline`. For example, Bootstrap uses a `box-shadow` to produce a blurred outline.

## Styling focus rings on a new component {#show}

When creating a new interactive component, you should make it focusable using the keyboard's `Tab` key by assigning a [`[tabindex]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) attribute:

```html
<span class="my-button" tabindex="0">
  ...
</span>
```

To show a focus ring for keyboard users only, use CSS like this:

```css
.my-button {
  &:focus-visible:not(.up-focus-hidden),
  &.up-focus-visible {
    outline: 1px solid royalblue;
  }
}
```

## Customizing focus ring visibility {#visibility-config}

Instead of [hiding unwanted focus rings with CSS](#hide), you can configure the [`up.viewport.config.autoFocusVisible`](https://unpoly.com/api/up.viewport.config.md#config.autoFocusVisible) function. This function decides whether a given element gets an [`.up-focus-visible`](https://unpoly.com/api/up-focus-visible.md) or [`.up-focus-hidden`](https://unpoly.com/api/up-focus-hidden.md) class.

The default strategy is implemented like this:

```js
up.viewport.config.autoFocusVisible = ({ element, inputDevice }) =>
  inputDevice === 'key' || up.form.isField(element)
```

See [`up.event.inputDevice`](https://unpoly.com/api/up.event.inputDevice.md) for a list of values for the `{ inputDevice }` property.

You can replace or extend the default strategy. For example, the following configuration keeps the default strategy, but never shows a focus ring on a [main fragment](https://unpoly.com/api/main.md):

```js
let defaultVisible = up.viewport.config.autoFocusVisible
up.viewport.config.autoFocusVisible = (options) =>
  defaultVisible(options) && !up.fragment.matches(options.element, ':main')
```

## Overriding visibility for a single interaction {#per-interaction}

If you are happy with your default strategy, but want to override it for a single interaction, set an [`[up-focus-visible]`](https://unpoly.com/api/up-follow.md#up-focus-visible) attribute on any link or form:

```html
<a href="/path" up-follow up-focus="main" up-focus-visible="false">Click me</a>
```

When rendering from JavaScript, pass a `{ focusVisible }` option:

```js
up.navigate({
  url: '/path',
  focus: 'main',
  focusVisible: false
})
```

The same option is accepted by [`up.focus()`](https://unpoly.com/api/up.focus.md).

<nav aria-label="Reading order">

- [Previous: Controlling focus](https://unpoly.com/learn/focus.md)
- [Next: Network & caching](https://unpoly.com/learn/network.md)

</nav>

