Last active
October 30, 2024 09:00
-
-
Save Gomah/cb2b0b3f7cb9838a0efd6508a42c3eda to your computer and use it in GitHub Desktop.
Responsive Variants with CVA, class-variance-authority
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 { useMediaQuery } from 'usehooks-ts' | |
import { useMemo } from 'react'; | |
import { screens } from '.generated/screens'; | |
/* | |
// You can also define the screens object manually if you don't want to use a prebuilt script. | |
const screens = { | |
sm: '640px', | |
md: '768px', | |
lg: '1024px', | |
xl: '1280px', | |
'2xl': '1536px', | |
} as const; | |
*/ | |
type Breakpoints = keyof typeof screens; | |
type ResponsiveValue<T> = T extends boolean ? boolean : T extends string ? T : keyof T; | |
type ResponsiveProps<T> = { | |
[K in Breakpoints]?: ResponsiveValue<T>; | |
} & { initial: ResponsiveValue<T> }; | |
function getScreenValue(key: string) { | |
return Number.parseInt(screens[key as Breakpoints]); | |
} | |
/** | |
* Custom hook for handling responsive behavior based on breakpoints. | |
* @param props - The responsive props containing breakpoints and initial value. | |
* @returns The responsive value based on the current breakpoint. | |
*/ | |
export function useResponsiveVariant<T>(props: ResponsiveProps<T>) { | |
const { initial, ...breakpoints } = props; | |
const [matchedBreakpoint] = Object.keys(breakpoints) | |
.sort((a, b) => getScreenValue(b) - getScreenValue(a)) | |
.map((breakpoint) => | |
useMediaQuery(`(min-width: ${screens[breakpoint as Breakpoint]})`) | |
? breakpoints[breakpoint as Breakpoint] | |
: undefined | |
) | |
.filter((value) => value !== undefined); | |
const size = useMemo(() => { | |
return matchedBreakpoint ?? initial; | |
}, [initial, matchedBreakpoint]); | |
return size as ResponsiveValue<T>; | |
} |
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 { Button, ButtonProps } from '@/ui'; | |
function Example() { | |
const buttonSize = useResponsiveVariant<ButtonProps['size']>({ initial: 'sm', sm: 'lg' }); | |
return <Button size={buttonSize}>Button</Button> | |
} |
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 fs from 'node:fs'; | |
import path from 'node:path'; | |
import resolveConfig from 'tailwindcss/resolveConfig'; | |
import tailwindConfig from './tailwind.config'; | |
const fullConfig = resolveConfig(tailwindConfig); | |
const screens = `export const screens = ${JSON.stringify( | |
fullConfig.theme.screens, | |
null, | |
2 | |
)} as const;`; | |
// Make sure the folder exists | |
if (!fs.existsSync(path.join(__dirname, './src/.generated'))) { | |
fs.mkdirSync(path.join(__dirname, './src/.generated')); | |
} | |
fs.writeFileSync(path.join(__dirname, './src/.generated/screens.ts'), screens, 'utf8'); |
Great solution, thanks!
@mnzsss You're welcome, I made a quick revision as it wasn't fully working with boolean values ✌️
Actually, using resolveConfig
will transitively pull in a lot of our build-time dependencies, resulting in bigger client-side bundle size, as stated here: https://tailwindcss.com/docs/configuration#referencing-in-java-script
I added a prebuild script, screens can also be hardcoded
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great solution, thanks!