diff --git a/.changeset/migrate-focus-visible.md b/.changeset/migrate-focus-visible.md new file mode 100644 index 00000000000..8e1e5074dd0 --- /dev/null +++ b/.changeset/migrate-focus-visible.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': minor +--- + +Migrate from `:focus` to `:focus-visible` so focus rings only appear during keyboard navigation diff --git a/packages/ui/src/baseTheme.ts b/packages/ui/src/baseTheme.ts index ed85bffb7f0..998c9a94202 100644 --- a/packages/ui/src/baseTheme.ts +++ b/packages/ui/src/baseTheme.ts @@ -123,6 +123,15 @@ const clerkTheme: Appearance = { theme.shadows.$focusRing.replace('{{color}}', theme.colors.$colorRing), ].toString(), }, + '&:focus:not(:focus-visible)': { + boxShadow: BUTTON_SOLID_SHADOW(theme.colors.$primary500), + }, + '&:focus-visible': { + boxShadow: [ + BUTTON_SOLID_SHADOW(theme.colors.$primary500), + theme.shadows.$focusRing.replace('{{color}}', theme.colors.$colorRing), + ].toString(), + }, }, '&[data-color="danger"]': { boxShadow: BUTTON_SOLID_SHADOW(theme.colors.$danger500), @@ -132,6 +141,15 @@ const clerkTheme: Appearance = { theme.shadows.$focusRing.replace('{{color}}', theme.colors.$dangerAlpha200), ].toString(), }, + '&:focus:not(:focus-visible)': { + boxShadow: BUTTON_SOLID_SHADOW(theme.colors.$danger500), + }, + '&:focus-visible': { + boxShadow: [ + BUTTON_SOLID_SHADOW(theme.colors.$danger500), + theme.shadows.$focusRing.replace('{{color}}', theme.colors.$dangerAlpha200), + ].toString(), + }, }, }, '&[data-variant="outline"]': { @@ -143,6 +161,15 @@ const clerkTheme: Appearance = { theme.shadows.$focusRing.replace('{{color}}', theme.colors.$colorRing), ].toString(), }, + '&:focus:not(:focus-visible)': { + boxShadow: BUTTON_OUTLINE_SHADOW(theme.colors.$borderAlpha100), + }, + '&:focus-visible': { + boxShadow: [ + BUTTON_OUTLINE_SHADOW(theme.colors.$borderAlpha100), + theme.shadows.$focusRing.replace('{{color}}', theme.colors.$colorRing), + ].toString(), + }, }, '&[data-variant="bordered"]': { borderWidth: 0, @@ -153,6 +180,15 @@ const clerkTheme: Appearance = { theme.shadows.$focusRing.replace('{{color}}', theme.colors.$colorRing), ].toString(), }, + '&:focus:not(:focus-visible)': { + boxShadow: BUTTON_OUTLINE_SHADOW(theme.colors.$borderAlpha100), + }, + '&:focus-visible': { + boxShadow: [ + BUTTON_OUTLINE_SHADOW(theme.colors.$borderAlpha100), + theme.shadows.$focusRing.replace('{{color}}', theme.colors.$colorRing), + ].toString(), + }, }, }, badge: { @@ -214,7 +250,7 @@ const clerkTheme: Appearance = { }, selectSearchInput__countryCode: { boxShadow: 'none', - '&:focus': { boxShadow: 'none' }, + '&:focus-visible': { boxShadow: 'none' }, }, cardBox: { borderWidth: 0, diff --git a/packages/ui/src/elements/ArrowBlockButton.tsx b/packages/ui/src/elements/ArrowBlockButton.tsx index 7ebc83e5407..dadcb25bb5d 100644 --- a/packages/ui/src/elements/ArrowBlockButton.tsx +++ b/packages/ui/src/elements/ArrowBlockButton.tsx @@ -67,14 +67,14 @@ export const ArrowBlockButton = React.forwardRef diff --git a/packages/ui/src/elements/Navbar.tsx b/packages/ui/src/elements/Navbar.tsx index 538a83474cd..8baf13d07da 100644 --- a/packages/ui/src/elements/Navbar.tsx +++ b/packages/ui/src/elements/Navbar.tsx @@ -296,7 +296,7 @@ const NavButton = (props: NavButtonProps) => { '&:hover': { backgroundColor: isActive ? undefined : t.colors.$neutralAlpha25, }, - '&:focus': { + '&:focus-visible': { backgroundColor: isActive ? undefined : t.colors.$neutralAlpha50, }, }), diff --git a/packages/ui/src/elements/PhoneInput/index.tsx b/packages/ui/src/elements/PhoneInput/index.tsx index 7882e0623be..e46258865f2 100644 --- a/packages/ui/src/elements/PhoneInput/index.tsx +++ b/packages/ui/src/elements/PhoneInput/index.tsx @@ -78,8 +78,8 @@ const PhoneInputBase = forwardRef @@ -119,7 +119,7 @@ const PhoneInputBase = forwardRef { borderRadius: 0, padding: `${t.space.$2x5} ${t.space.$0x25}`, width: 'fit-content', - '&:hover, :focus': { backgroundColor: t.colors.$transparent, boxShadow: 'none' }, + '&:hover, &:focus-visible': { backgroundColor: t.colors.$transparent, boxShadow: 'none' }, }), sx, ]} diff --git a/packages/ui/src/primitives/Button.tsx b/packages/ui/src/primitives/Button.tsx index 28d1eb2ad68..c00d911c89c 100644 --- a/packages/ui/src/primitives/Button.tsx +++ b/packages/ui/src/primitives/Button.tsx @@ -86,7 +86,7 @@ const { applyVariants, filterProps } = createVariants( '&:hover': { backgroundColor: vars.accentHover, }, - '&:focus': props.hoverAsFocus + '&:focus-visible': props.hoverAsFocus ? { backgroundColor: vars.accentHover, } @@ -98,7 +98,7 @@ const { applyVariants, filterProps } = createVariants( borderColor: theme.colors.$borderAlpha150, color: theme.colors.$neutralAlpha600, '&:hover': { backgroundColor: theme.colors.$neutralAlpha50 }, - '&:focus': props.hoverAsFocus + '&:focus-visible': props.hoverAsFocus ? { backgroundColor: theme.colors.$neutralAlpha50, borderColor: theme.colors.$borderAlpha300 } : undefined, }, @@ -109,14 +109,16 @@ const { applyVariants, filterProps } = createVariants( color: vars.accentContrast, backgroundColor: vars.accent, '&:hover': { backgroundColor: vars.accentHover }, - '&:focus': props.hoverAsFocus + '&:focus-visible': props.hoverAsFocus ? { backgroundColor: vars.accentHover, borderColor: theme.colors.$borderAlpha300 } : undefined, }, ghost: { color: vars.accent, '&:hover': { backgroundColor: vars.alpha, color: vars.accentHover }, - '&:focus': props.hoverAsFocus ? { backgroundColor: vars.alpha, color: vars.accentHover } : undefined, + '&:focus-visible': props.hoverAsFocus + ? { backgroundColor: vars.alpha, color: vars.accentHover } + : undefined, }, link: { minHeight: 'fit-content', @@ -126,7 +128,7 @@ const { applyVariants, filterProps } = createVariants( padding: 0, color: theme.colors.$primary500, '&:hover': { textDecoration: 'underline' }, - '&:focus': props.hoverAsFocus ? { textDecoration: 'underline' } : undefined, + '&:focus-visible': props.hoverAsFocus ? { textDecoration: 'underline' } : undefined, }, linkDanger: { minHeight: 'fit-content', @@ -136,7 +138,7 @@ const { applyVariants, filterProps } = createVariants( padding: 0, color: theme.colors.$danger500, '&:hover': { textDecoration: 'underline' }, - '&:focus': props.hoverAsFocus ? { textDecoration: 'underline' } : undefined, + '&:focus-visible': props.hoverAsFocus ? { textDecoration: 'underline' } : undefined, }, unstyled: {}, roundWrapper: { padding: 0, margin: 0, height: 'unset', width: 'unset', minHeight: 'unset' }, diff --git a/packages/ui/src/primitives/Link.tsx b/packages/ui/src/primitives/Link.tsx index 0709cb54e2b..1bd36ee4f44 100644 --- a/packages/ui/src/primitives/Link.tsx +++ b/packages/ui/src/primitives/Link.tsx @@ -36,7 +36,7 @@ const { applyVariants, filterProps } = createVariants(theme => ({ }, focusRing: { true: { - '&:focus': { + '&:focus-visible': { outline: 'none', ...common.focusRing(theme), }, diff --git a/packages/ui/src/styledSystem/common.ts b/packages/ui/src/styledSystem/common.ts index 7b0cf4f9eac..9abc7b7173c 100644 --- a/packages/ui/src/styledSystem/common.ts +++ b/packages/ui/src/styledSystem/common.ts @@ -94,42 +94,50 @@ const borderVariants = (t: InternalTheme, props?: any) => { boxShadow: hoverBoxShadow, }, }; + const borderColor = !props?.hasError + ? !props?.hasWarning + ? t.colors.$borderAlpha150 + : t.colors.$warningAlpha300 + : t.colors.$dangerAlpha500; + + const boxShadow = !props?.hasError + ? !props?.hasWarning + ? t.colors.$borderAlpha100 + : t.colors.$warningAlpha50 + : t.colors.$borderAlpha150; + + const defaultBoxShadow = t.shadows.$input.replace('{{color}}', boxShadow); + + const focusStyleValues = { + borderColor: hoverBorderColor, + WebkitTapHighlightColor: 'transparent', + boxShadow: [ + hoverBoxShadow, + t.shadows.$focusRing.replace( + '{{color}}', + !props?.hasError ? t.colors.$borderAlpha150 : (t.colors.$dangerAlpha200 as string), + ), + ].toString(), + }; const focusStyles = props?.focusRing === false ? {} : { - '&:focus': { - borderColor: hoverBorderColor, - WebkitTapHighlightColor: 'transparent', - boxShadow: [ - hoverBoxShadow, - t.shadows.$focusRing.replace( - '{{color}}', - !props?.hasError ? t.colors.$borderAlpha150 : (t.colors.$dangerAlpha200 as string), - ), - ].toString(), + '&:focus': focusStyleValues, + '&:focus:not(:focus-visible)': { + borderColor, + boxShadow: defaultBoxShadow, }, + '&:focus-visible': focusStyleValues, }; - const borderColor = !props?.hasError - ? !props?.hasWarning - ? t.colors.$borderAlpha150 // Default border color - : t.colors.$warningAlpha300 // Warning border color - : t.colors.$dangerAlpha500; // Error border color - - const boxShadow = !props?.hasError - ? !props?.hasWarning - ? t.colors.$borderAlpha100 // Default box shadow color - : t.colors.$warningAlpha50 // Warning box shadow color - : t.colors.$borderAlpha150; // Error box shadow color - return { normal: { borderRadius: t.radii.$md, borderWidth: t.borderWidths.$normal, borderStyle: t.borderStyles.$solid, borderColor, - boxShadow: t.shadows.$input.replace('{{color}}', boxShadow), + boxShadow: defaultBoxShadow, transitionProperty: t.transitionProperty.$common, transitionTimingFunction: t.transitionTiming.$common, transitionDuration: t.transitionDuration.$focusRing, @@ -161,6 +169,12 @@ const focusRing = (t: InternalTheme) => { '&:focus': { ...focusRingStyles(t), }, + '&:focus:not(:focus-visible)': { + boxShadow: 'none', + }, + '&:focus-visible': { + ...focusRingStyles(t), + }, } as const; }; diff --git a/packages/ui/src/themes/neobrutalism.ts b/packages/ui/src/themes/neobrutalism.ts index 1b14ee69676..ecd296d3035 100644 --- a/packages/ui/src/themes/neobrutalism.ts +++ b/packages/ui/src/themes/neobrutalism.ts @@ -3,7 +3,7 @@ import { createTheme } from './createTheme'; const buttonStyle = { boxShadow: '3px 3px 0px #000', border: '2px solid #000', - '&:focus': { + '&:focus-visible': { boxShadow: '4px 4px 0px #000', border: '2px solid #000', transform: 'scale(1.01)', @@ -49,7 +49,7 @@ export const neobrutalism = createTheme({ ...buttonStyle, ...shadowStyle, transition: 'all 0.2s ease-in-out', - '&:focus': { + '&:focus-visible': { boxShadow: '4px 4px 0px #000', border: '2px solid #000', transform: 'scale(1.01)', @@ -69,7 +69,7 @@ export const neobrutalism = createTheme({ formFieldInput: { ...shadowStyle, transition: 'all 0.2s ease-in-out', - '&:focus': { + '&:focus-visible': { boxShadow: '4px 4px 0px #000', border: '2px solid #000', transform: 'scale(1.01)', @@ -102,7 +102,7 @@ export const neobrutalism = createTheme({ footerActionLink: { fontWeight: '700', borderBottom: '3px solid', - '&:focus': { + '&:focus-visible': { boxShadow: 'none', }, },