From 168b69edd4a0b9479436553698937ed88cf5e6f7 Mon Sep 17 00:00:00 2001 From: Shayne Preston Date: Wed, 7 Jan 2026 12:23:49 +0100 Subject: [PATCH] feat: add visually hidden style utility --- .changeset/tricky-wolves-shop.md | 5 ++ .../components/primitives/Input/input.css.ts | 2 +- packages/lunar/src/index.ts | 7 +- packages/lunar/src/themes/styles/utilities.ts | 75 +++++++++++++++---- 4 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 .changeset/tricky-wolves-shop.md diff --git a/.changeset/tricky-wolves-shop.md b/.changeset/tricky-wolves-shop.md new file mode 100644 index 0000000..8845029 --- /dev/null +++ b/.changeset/tricky-wolves-shop.md @@ -0,0 +1,5 @@ +--- +'@lunar-js/lunar': minor +--- + +feat: add visually hidden style utility diff --git a/packages/lunar/src/components/primitives/Input/input.css.ts b/packages/lunar/src/components/primitives/Input/input.css.ts index df0be57..3736ff8 100644 --- a/packages/lunar/src/components/primitives/Input/input.css.ts +++ b/packages/lunar/src/components/primitives/Input/input.css.ts @@ -32,7 +32,7 @@ const input = style([ '&[aria-invalid="true"]': { borderColor: themeContract.colors.border.error, }, - ...withCustomOutline(themeContract.colors.shadow.destructive, '&[aria-invalid="true"]'), + ...withCustomOutline(themeContract.colors.shadow.destructive, '&[aria-invalid="true"]:focus-visible'), }, '::file-selector-button': { diff --git a/packages/lunar/src/index.ts b/packages/lunar/src/index.ts index 6713ebe..8b28ad7 100644 --- a/packages/lunar/src/index.ts +++ b/packages/lunar/src/index.ts @@ -83,7 +83,12 @@ export { useDialog } from './hooks/dialog.js'; /** * Styling Utility Exports */ -export { withCustomOutline, withSafeTransition, withBreakpoint } from './themes/styles/utilities.js'; +export { + withCustomOutline, + withSafeTransition, + withBreakpoint, + withVisuallyHidden, +} from './themes/styles/utilities.js'; export { BREAKPOINT__SM, BREAKPOINT__MD, BREAKPOINT__LG } from './constants/theming.js'; export { diff --git a/packages/lunar/src/themes/styles/utilities.ts b/packages/lunar/src/themes/styles/utilities.ts index 18d96b1..7587ec6 100644 --- a/packages/lunar/src/themes/styles/utilities.ts +++ b/packages/lunar/src/themes/styles/utilities.ts @@ -35,14 +35,10 @@ const withSafeTransition = (styles: StyleRule): StyleRule => ({ /** * Helper function to create a custom focus outline style object. - * Returns a style object with focus-visible outline that follows the design system's outline pattern. - * Supports an optional selector parameter for targeting specific pseudo-selectors or child elements. - * - * This function returns a style object that must be used with vanilla-extract's `style()` function or `recipe()` function. * * @param outlineColor - The color for the focus outline (e.g., '#0066cc', 'rgb(255, 0, 0)', CSS custom properties) - * @param selector - Optional selector prefix for the focus-visible state (e.g., '&', '& > button', defaults to '') - * @returns A style object with custom focus outline styling that uses dynamic selector keys + * @param selector - Optional CSS selector for the outline state (e.g., ':focus-visible', '&:hover', '& > button:focus-visible', defaults to ':focus-visible') + * @returns A style object with custom focus outline styling that uses the provided selector * * @example * import { style } from '@vanilla-extract/css'; @@ -58,10 +54,11 @@ const withSafeTransition = (styles: StyleRule): StyleRule => ({ * ]); * * // Example with custom selector - * const parentWithFocusableChild = style([ - * withCustomOutline('#0066cc', '& > button'), + * const hoverOutlineButton = style([ + * withCustomOutline('#00cc66', ':hover'), * { - * padding: '12px' + * padding: '8px 16px', + * background: 'white' * } * ]); * @@ -78,8 +75,8 @@ const withSafeTransition = (styles: StyleRule): StyleRule => ({ * } * }); */ -const withCustomOutline = (outlineColor: string, selector = ''): Record<`${string}:focus-visible`, CSSProperties> => ({ - [`${selector}:focus-visible`]: { +const withCustomOutline = (outlineColor: string, selector = ':focus-visible'): Record => ({ + [selector]: { boxShadow: `0px 0px 0px 0px, ${COLORS__PURE.transparent} 0px 0px 0px 0px, ${COLORS__PURE.transparent} 0px 0px 0px 0px, ${outlineColor} 0px 0px 0px 3px, ${outlineColor} 0px 1px 2px 0px`, outline: '2px solid transparent', outlineOffset: '2px', @@ -88,10 +85,6 @@ const withCustomOutline = (outlineColor: string, selector = ''): Record<`${strin /** * Helper function to create a responsive style object with breakpoint media queries. - * Returns a style object that applies the provided styles only when the viewport width - * meets or exceeds the specified breakpoint value. - * - * This function returns a style object that must be used with vanilla-extract's `style()` function or `recipe()` function. * * @param breakpoint - The minimum viewport width for the media query (e.g., '768px', '1024px', '48rem') * @param styles - CSS properties object to apply at the breakpoint (e.g., { fontSize: '1.5rem', padding: '24px' }) @@ -137,4 +130,54 @@ const withBreakpoint = (breakpoint: string, styles: CSSProperties): StyleRule => }, }); -export { withSafeTransition, withCustomOutline, withBreakpoint }; +/** + * Helper function to create a visually hidden style object for screen reader accessibility. + * Returns a style object that hides content visually while keeping it accessible to screen readers. + * This is useful for providing descriptive text, skip links, or other content that should only + * be available to assistive technologies. + * + * @returns A style object that visually hides content while maintaining screen reader accessibility + * + * @example + * import { style } from '@vanilla-extract/css'; + * import { recipe } from '@vanilla-extract/recipes'; + * import { withVisuallyHidden } from './utilities.css.ts'; + * + * const skipLink = style([ + * withVisuallyHidden(), + * { + * // Additional styles can be added here + * zIndex: 1000 + * } + * ]); + * + * const srOnly = style([ + * withVisuallyHidden() + * ]); + * + * const buttonWithHiddenText = recipe({ + * base: [ + * withVisuallyHidden(), + * { + * padding: '8px 12px', + * background: 'blue' + * } + * ], + * variants: { + * // variant styles + * } + * }); + */ +const withVisuallyHidden = (): StyleRule => ({ + position: 'absolute', + width: '1px', + height: '1px', + padding: '0', + margin: '-1px', + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: '0', +}); + +export { withSafeTransition, withCustomOutline, withBreakpoint, withVisuallyHidden };