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.
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.
Last active
May 2, 2022 21:52
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
</> | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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