Skip to content

Instantly share code, notes, and snippets.

@cchaos
Last active May 2, 2022 21:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cchaos/b3543c8008da9273f481298538aeb3a0 to your computer and use it in GitHub Desktop.
Save cchaos/b3543c8008da9273f481298538aeb3a0 to your computer and use it in GitHub Desktop.
Matching focus outlines to element and only on :focus-visible (React and Emotion for example only)

This gist creates a universal function that allows focus outlines to match the currently focused element and displays the outline only when the user is navigating via keyboard.

Gif example

Tabbing between the two examples show the first with a simple text-color outline, the second with a wider blue outline. Clicking the buttons with a mouse shows no outline.
import React from 'react';
import { Global, css } from '@emotion/react';
import { focusRing } from './focus';
export default () => (
<>
<Global
styles={css`
// Apply the default styles to all focusable elements
*:focus {
${focusRing()}
}
`}
/>
<p>
<button>
I am an unstyled button with default outline styles
</button>
</p>
<p>
<button
css={css`
&:focus {
${focusRing('outset', '#07C')}
}
`}
>
I am an unstyled button with an outer red outline
</button>
</p>
</>
);
import { CSSProperties } from 'react';
export type _FocusRingOffset =
| 'inset'
| 'outset'
| 'center'
| CSSProperties['outlineOffset'];
/**
* It is best practice to utilize the browser's default `outline` property for handling focus rings.
* However, some components need to be forced to have the same behavior, or adjust the display.
* This function re-applies the same default outline with a couple parameters
* @param offset Accepts a specific measurement or 'inset', 'outset' or 'center' to adjust outline position
* @param color Accepts any CSS color, **Note: only works in -webkit-**
*/
export const focusRing = (
offset: _FocusRingOffset = 'center',
color?: CSSProperties['outlineColor']
) => {
// Width can be enforced as a constant at the global theme layer
const outlineWidth = '2px';
// Using currentColor allows the focus ring to match the element's text color
const outlineColor = color || 'currentColor';
let outlineOffset = offset;
if (offset === 'inset') {
outlineOffset = `-${outlineWidth}`;
} else if (offset === 'outset') {
outlineOffset = `${outlineWidth}`;
} else if (offset === 'center') {
outlineOffset = `calc(${outlineWidth} / -2);`;
}
// This function utilizes `focus-visible` to turn on focus outlines.
// But this is browser-dependend:
// πŸ‘‰ Safari and Firefox innately respect only showing the outline with keyboard only
// πŸ’” But they don't allow coloring of the 'auto'/default outline, so contrast is no good in dark mode.
// πŸ‘‰ For these browsers we use the solid type in order to match with `currentColor`.
// 😦 Which does means the outline will be square
return `
outline: ${outlineWidth} solid ${outlineColor};
outline-offset: ${outlineOffset};
// πŸ‘€ Chrome respects :focus-visible and allows coloring the \`auto\` style
&:focus-visible {
outline-style: auto;
}
// πŸ™…β€β™€οΈ But Chrome also needs to have the outline forcefully removed from regular \`:focus\` state
&:not(:focus-visible) {
outline: none;
}
`;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment