Skip to content

Instantly share code, notes, and snippets.

@spigelli
Last active July 26, 2023 15:18
Show Gist options
  • Save spigelli/1d1b66c68e4a440f1a635ba1a17e1e4f to your computer and use it in GitHub Desktop.
Save spigelli/1d1b66c68e4a440f1a635ba1a17e1e4f to your computer and use it in GitHub Desktop.
cool styles

Advanced Style Behavior in PandaCSS

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.

Possible Syntaxes

The Ideal Syntax

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>
  );
}

How Mantine Styles Look

createStyles

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>
  );
}

createStyles Parameters

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} />
    </>
  );
}

Component Styles API Integrations

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 component
  • styles prop - a map of styling point names and emotion objects
  • className prop - a string of class names applied to the root element of the component
  • classNames prop - a map of styling point names and class names
  • styles prop - a map of styling point names and emotion objects
  • Styled Components - you can use styled(Component)'...' to create a styled component

Panda Recipes

Applicable Usages

Single insert point with variants

Recipe Declaration
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' }
    }
  }
})
Recipe Usage
export function Button({
  children
}) {
  return (
    <button className={button({ visual: 'solid', size: 'sm' })}>
      {children}
    </button>
  )
}
Generated CSS
@layer utilities {
  .d_flex {
    display: flex;
  }
 
  .bg_red_200 {
    background-color: #fed7d7;
  }
 
  .color_white {
    color: #fff;
  }
 
  .border_width_1px {
    border-width: 1px;
  }
  /* ... */
}

Multiple insert points with variants

Recipe Declaration
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',
      }
    }
  ]
})
Recipe Usage
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.

Takeaways

Intended cva Usage

  • 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

Limitations

  • 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.

Panda Pseudo Props

const Demo = () => (
  <div
    className={
      css({
        bg: 'red.400',
        _hover: {
          bg: 'orange.400'
        }
      })
    }
  />
)

Panda Class Merging

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} />
}

Panda Version of Create Styles

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment