Skip to main content
Web Dev and Web a11y

Creating a more accessible Pico CSS Focus Indicator

The code to accompany this post is "Pico CSS Focus Indicator""

In "Creating a more accessible focus indicator", we covered the accessibility requirements and techniques for improving upon browser default focus indicators. Pico CSS is commonly used in FastHTML examples and provides very useful styling and layout defaults. However, its focus indicators could be improved for better accessibility.

Pico CSS focus indicators

Pico CSS has theme switching functionality to let a user change the current page theme via an on page toggle that changes the <html data-theme="dark">. As you can see from the below screen recordings of both themes, identifying which button is currently in focus is difficult.

Light theme - [data-theme=light]

Pico CSS light theme focus indicator on primary, secondary and contrast buttons

Dark theme - [data-theme=dark]

Pico CSS dark theme focus indicator on primary, secondary and contrast buttons

Low Colour Contrast Ratios

For both the light and dark themes in PicoCSS, the colours used for focus indicators fall below the 3:1 contrast ratio required for non-text UI components. The examples below use the primary button styles, but the same process applies to secondary and contrast variants. The CSS values were taken from the current version 2.1.1 (September 2025) of Pico CSS.

Light theme contrast ratios

--pico-background-color: #fff;
--pico-primary-focus: rgba(2, 154, 232, 0.5);
--pico-primary-background: #0172ad;
/* primary button background-color when focused */
--pico-primary-hover-background: #017fc0;

Dark theme contrast ratios

--pico-background-color: rgb(19, 22.5, 30.5);
--pico-primary-focus: rgba(1, 170, 255, 0.375);
--pico-primary-background: #0172ad;
/* primary button background-color when focused */
--pico-primary-hover-background: #017fc0;

Reducing the transparency is not enough

For the light theme, an increase in the alpha value from 0.5 to 1.0 is a ratio of 3.09:1

For the dark theme, increasing 0.375 to 1.0 provides a high contrast of 7:1.

These changes would need to be made and tested for each button (and possibly other controls) that your site uses to maintain at least a 3:1 contrast ratio. This does not resolve a larger underlying problem with how Pico CSS handles focus indicators.

The problem with Pico CSS's use of outline: none

A previous post, "Creating a more accessible focus indicator", highlighted how the built-in browser focus indicators use the outline CSS property to indicate which interactive element - buttons, inputs etc - currently has focus. And how to improve those defaults by overriding them with WCAG compliant values.

By default, Pico CSS removes native focus indicators by setting outline: none on interactive elements and uses box-shadow. In some cases (particularly with buttons as shown in the previous contrast ratio calculations), it slightly changes the background-color of the element when in focus. While this provides a more styling options and effects, WCAG specifically advises against doing this i.e. "Avoid setting outline: none to use box-shadow on its own." because in certain "forced-color modes" e.g. Windows High Contrast Mode the box-shadow won't be rendered by the browser.

Invisible in Windows High Contrast mode

Windows High Contrast (Forced Colors) is an operating-system level setting that overrides site and application UI colour palettes with a limited, user-selected high-contrast theme. Its goal is to ensure maximum readability for the user.

The first example shows Pico CSS's default behaviour before High Contrast mode is enabled. As focus is moved with the Tab key, the text at the top updates to display the currently focused element.

Now here is the same group of controls with Windows High Contrast mode enabled. Windows provides several high contrast themes; this example uses High Contrast #1.

Notice the following:

This happens because, as mentioned in the previous section, Pico CSS removes the default outline and relies on a box-shadow, which is suppressed in high contrast modes. While the CSS below applies to links, buttons and other inputs use a similar approach with different values for box-shadow colours and border:

/* Pico CSS <a> specific focus indicator */
:where(a:not([role=button])):focus-visible {
  box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus);
}

The simplest and most robust fix is to reintroduce outline (with outline-offset), which works consistently across all elements and adapts automatically to high contrast themes.

Restoring the outline

As covered in the previous focus indicator post, using outline with outline-offset provides a clear and consistent focus indicator around interactive elements. The outline-offset creates separation from the element, making the focused area larger and easier to see.

This approach simplifies colour selection: a single high-contrast colour can be applied to all interactive elements, avoiding the need to choose separate colours for each button, link etc variant.

High contrast outline colours

Focus outlines must have a contrast ratio of at least 3:1 against their background. Choosing colours with higher contrast improves visibility and works better across different displays.

For example, the following colours provide strong contrast for Pico's themes:

[data-theme=dark] {
    :focus-visible {
        outline: 0.1875rem solid #71a3d1;  /* 3px */
        outline-offset: 0.125rem; /* 2px */
    }
}

[data-theme=light] {
    :focus-visible {
        outline: 0.1875rem solid #1a5d86;  /* 3px */
        outline-offset: 0.125rem; /* 2px */
    }
}

Updated light theme

Pico CSS light theme on primary, secondary and contrast buttons

Updated dark theme

Pico CSS dark theme on primary, secondary and contrast buttons

Windows High Contrast mode

In forced colors or high contrast mode, the operating system overrides CSS colours with those of the predefined system-wide high contrast theme. That is why the browser renders the outline based on the colours defined in the High Contrast #1 theme and not from the CSS above.

With this method, all interactive elements receive a clear, accessible focus indicator that works in both normal and forced colors modes, solving the issues demonstrated in the previous section.

Avoiding CSS repetition

Since the outline style is the same across themes, you only need to change the outline-colour. You can simplify the CSS by setting the common rules once and overriding just the colour per theme:

:focus-visible {
    outline: 0.1875rem solid;   /* 3px */
    outline-offset: 0.125rem; /* 2px */
}

/* Pico's theme attribute */
[data-theme=dark] :focus-visible {
    outline-color: #71a3d1;
}

[data-theme=light] :focus-visible {
    outline-color: #1a5d86;
}

/* If not using Pico's theme attribute */
@media (prefers-color-scheme: dark) {
    :focus-visible {
        outline-color: #71a3d1;
    }
}

@media (prefers-color-scheme: light) {
    :focus-visible {
        outline-color: #1a5d86;
    }
}

Removing box-shadow?

An additional step might be to remove Pico CSS's box-shadow indicators entirely (or override them with box-shadow:none;) and rely on outline and outline-offset. Pico CSS applies different box-shadow styles, colours and transitions across the various elements, removing them will require careful unpicking, replacing multiple rules and testing those changes. That level of work is outside the scope of this post.