We desire a single source of truth for the styles of each component. This becomes complex when we want to support:
- Overriding styles of any element rendered by the component
- Responsive styles
- Variants
Mantime is able to achieve this by leveraging a custom createStyles
function
however this is more complex in PandaCSS due to the static nature of the
library.
type V = {
size: 'sm' | 'md' | 'lg';
variant: 'outline' | 'solid';
}
type S = 'root' | 'icon';
const useStyles = createStyles<S,V>({ size, variant } => ({
// Since this needs to be statically analyzable, these conditional styles
// would need to be evaluated at build time via babel plugin or similar
// and to keep type safety, I think the types V and S would also need to be
// evaluated
root: {
padding: '8px 16px',
borderRadius: '4px',
// Single conditional css rule
color: variant === 'outline'
? 'red.200'
: 'white',
},
icon: {
width: '24px',
height: '24px',
// Multiple conditional css rules
...(variant === 'outline' ? { color: 'red.200', backgroundColor: 'white' } : { })
),
},
});
interface ButtonProps extends VariantProps<V>, ClassNamesProps<S> {
// Some props
}
function Button({
size,
variant,
classNames,
}: ButtonProps) {
// Passing classNames here automatically merges styles with cx
const { classes } = useStyles(classNames, { size, variant });
return (
<button className={classes.root}>
<span className={classes.icon}>
{children}
</span>
</button>
);
}
const useStyles = createStyles((theme) => ({
wrapper: {
// subscribe to color scheme changes right in your styles
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
maxWidth: rem(400),
width: '100%',
height: rem(180),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 'auto',
marginRight: 'auto',
borderRadius: theme.radius.sm,
// Pseudo classes
'&:hover': {
backgroundColor: theme.colors.blue[9],
},
// Dynamic media queries, define breakpoints in theme, use anywhere
[theme.fn.smallerThan('sm')]: {
// Child reference in nested selectors via ref
[`& .${getStylesRef('child')}`]: {
fontSize: theme.fontSizes.xs,
},
},
},
child: {
// assign ref to element
ref: getStylesRef('child'),
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white,
padding: theme.spacing.md,
borderRadius: theme.radius.sm,
boxShadow: theme.shadows.md,
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
},
}));
function Demo() {
const { classes } = useStyles();
return (
<div className={classes.wrapper}>
<div className={classes.child}>createStyles demo</div>
</div>
);
}
const useStyles = createStyles((theme, { color, radius }: ButtonProps) => ({
button: {
color: theme.white,
backgroundColor: theme.colors[color][6],
borderRadius: radius,
padding: theme.spacing.md,
margin: theme.spacing.md,
border: 0,
cursor: 'pointer',
},
}));
function Button({ color, radius }: ButtonProps) {
const { classes } = useStyles({ color, radius });
return (
<button type="button" className={classes.button}>
{color} button with {radius} radius
</button>
);
}
function Demo() {
return (
<>
<Button color="blue" radius={5} />
<Button color="violet" radius={50} />
</>
);
}
You can then use these useStyles
hooks and integrate your component with a
variety of different styling options in usage:
sx
prop - an emotion object applied to the root element of the componentstyles
prop - a map of styling point names and emotion objectsclassName
prop - a string of class names applied to the root element of the componentclassNames
prop - a map of styling point names and class namesstyles
prop - a map of styling point names and emotion objects- Styled Components - you can use
styled(Component)'...'
to create a styled component
const button = cva({
base: {
display: 'flex'
},
variants: {
visual: {
solid: { bg: 'red.200', color: 'white' },
outline: { borderWidth: '1px', borderColor: 'red.200' }
},
size: {
sm: { padding: '4', fontSize: '12px' },
lg: { padding: '8', fontSize: '24px' }
}
}
})
export function Button({
children
}) {
return (
<button className={button({ visual: 'solid', size: 'sm' })}>
{children}
</button>
)
}
@layer utilities {
.d_flex {
display: flex;
}
.bg_red_200 {
background-color: #fed7d7;
}
.color_white {
color: #fff;
}
.border_width_1px {
border-width: 1px;
}
/* ... */
}
import { cva } from '../styled-system/css'
const button2 = cva({
base: {
},
variants: {
layer: {
root: {
padding: '8px 16px',
borderRadius: '4px',
},
icon: {
width: '24px',
height: '24px',
}
},
size: {
md: { },
lg: { }
},
},
// compound variants
compoundVariants: [
// root layer get's slightly different styles when size is large
{
layer: 'root',
size: 'lg',
css: {
padding: '16px 32px',
borderRadius: '8px',
}
},
// icon layer get's slightly different styles when size is large
{
layer: 'icon',
size: 'lg',
css: {
width: '32px',
height: '32px',
}
}
]
})
export function Button2({
children,
size='md',
}) {
return (
<button className={button2({ layer: 'root', size })}>
<span className={button2({ layer: 'icon', size })}>
{children}
</span>
</button>
)
}
From the docs: Compound variants are a way to combine multiple variants together to create more complex sets of styles. They are defined using the
compoundVariants
property, which takes an array of objects as its argument. Each object in the array represents a set of conditions that must be met in order for the corresponding styles to be applied.
- Leverage css variables in the base styles as much as possible. Makes it easier to theme the component with JS
- Don't mix styles by writing complex selectors. Separate concerns and group them in logical variants
- Use the compoundVariants property to create more complex sets of styles
- Recipes created from cva cannot have responsive or conditional values. Only layer recipes can have responsive or conditional values.
- Due to static nature of Panda, it's not possible to track the usage of the recipes in all cases.
const Demo = () => (
<div
className={
css({
bg: 'red.400',
_hover: {
bg: 'orange.400'
}
})
}
/>
)
Panda provides a cx function to manage classnames. It accepts a list of classnames and returns a string
import { css, cx } from '../styled-system/css'
const styles = css({
borderWidth: '1px',
borderRadius: '8px',
paddingX: '12px',
paddingY: '24px'
})
const Card = ({ className, ...props }) => {
const rootClassName = cx(styles, className)
return <div className={rootClassName} {...props} />
}
export type UseStyles<TVariantRecord, ElementName> =
(
variantProps: TVariantRecord,
element: ElementName,
) => SystemStyleObject
type ExampleTVariantRecord = {
color: 'red' | 'blue'
size: 'sm' | 'md' | 'lg'
}
export type CreateStylesInput<TVariantRecord, TElementName> = {
[elementName in TElementName]: {
base: SystemStyleObject
variants: {
[variantName in keyof TVariantRecord]: {
[variantValue in TVariantRecord[variantName]]: SystemStyleObject
}
}
}
}
export type CreateStyles<TVariantRecord, TElementName>: (styleDef: CreateStylesInput<TVariantRecord, TElementName>) => UseStyles<TVariantRecord, TElementName>