use-sx
TypeScript icon, indicating that this package has built-in type declarations

0.1.0 • Public • Published

use-sx

CSS-in-JS, inside out and upside down.

What if?

What if instead of mapping selectors-to-properties-to-values...

<button css={{
  borderStyle: 'solid',
  borderColor: 'black',
  ':hover': {
    borderColor: 'red',
  }
}}>

You mapped properties-to-selectors-to-values?

<Button sx={{
  borderStyle: 'solid',
  borderColor: {
    default: 'black',
    ':hover': 'red',
  }
}}>

If your css attributes could accept functions as values, you could even specify colors for all states in a single theme object...

<Button sx={{
  borderStyle: 'solid',
  borderColor: theme => theme.buttons.borderColor
}}>

And what if instead of tying your pseudo-selectors to individual elements:

<button css={{
  ':hover': {
    borderColor: 'red'
  }
}}>

You could specified a control boundary, and nested elements would automatically place the :hover selector where it makes sense?

<ButtonControl onClick={() => {}}>
  <Icon
    glyph={TranslationsMenu}
    sx={{
      fill: {
        default: 'black' 
        hover: 'red',
      }
    }}
  />
  <Caret sx={{
    borderColor: {
      default: 'black' 
      hover: 'red',
    }
  }} />
</ButtonControl>

If you could create custom style properties, you could even compose your styled components without a care for where they'll eventually be used...

<LinkControl href='/start'>
  <ButtonBody sx={{
    color: {
      default: 'gray',
      hover: 'red',
    },
    margin: '8px 4px',
  }}>
    <ButtonContent
      caret
      glyph={Play}
      label="Play"
      sx={{
        color: 'white'
      }}
    />
  </ButtonBody>
</LinkControl>

See this as a live example.

How it works

It's just a hook.

This package gives you a useSx() hook that takes styles in the format of the sx prop above, and returns styles in the format expected by a css prop -- as supported by Styled Components.

Note: Currently, the useSx hook requires Styled Components, as it reads directly from the styled-components theme context. Support for emotion would make a great Pull Request! 😉

useSx(options)

Basic usage

export const Button = React.forwardRef(({ sx, ...rest }, ref) => {
  const css = useSx({
    // The `sx` option accepts an array of styles
    sx: [
      {
        // Pass your default styles first
        backgroundColor: 'transparent',
        border: '1px solid black',
        borderColor: {
          focus: 'blue',
        }
        color: 'black'
      },
      // Then pass in styles from the component's `sx` prop
      sx,
      {
        // Finally, pass in any override styles -- things you don't want to be
        // customizable by the `sx` prop.
        cursor: 'pointer',
      },
    ],
  })
 
  return <button {...rest} css={css} ref={ref} />
})
 
// Then render your component like any other
ReactDOM.render(
  <Button sx={{
    opacity: {
      default: 1,
      hover: 0.9,
      active: 0.8,
    }
  }}>
    Hello, world!
  </Button>,
  document.body
)

Mapped props

By passing a maps property to useSx, you can add custom style properties to your components. These custom properties can then be used with theme functions and selector objects -- just as with standard CSS properties.

For example, this <Caret> component accepts three custom style props:

  • color, which maps directly to the borderTopColor prop
  • direction, which maps to varying rotate transforms
  • size, which sets borderWidth and a corresponding negative margin
export const Caret = React.forwardRef(({ sx, ...rest }, ref) => {
  const css = useSx({
    sx: [
      {
        color: (theme = {}) => theme.foregroundColor || 'black',
        direction: 'down',
        width: 5,
      },
      sx,
      {
        borderColor: 'transparent',
        borderStyle: 'solid',
        height: 0,
        width: 0,
      },
    ],
    maps: {
      color: (value: string) => ({
        borderTopColor: value,
      }),
      direction: (value: CaretDirection) => ({
        transform: {
          down: `rotate(0)`,
          up: `rotate(180deg)`,
          left: `rotate(-90deg)`,
          right: `rotate(90deg)`,
        }[value],
      }),
      width: (value: number) => ({
        borderWidth: value,
        marginBottom: -value,
      }),
    },
  })
 
  return <div {...rest} css={css} ref={ref} />
})
 
ReactDOM.render(
  <Caret sx={{
    color: {
      default: 'black',
      hover: 'red',
    },
    width: 5,
  }}>
    Hello, world!
  </Button>,
  document.body
)

control(StyledComponent)

This is a Higher Order Function which expects a Styled Component, returning an identical component that acts as a control boundary -- i.e. the component to which :hover pseudoselectors on nested elements will be applied.

mergeSx(sx)

This function merges anything that can be passed into the sx option for useSx into a single object.

const mergedSx = mergeSx([
  null,
  {
    color: 'inherit', 
  },
  {
    color: 'black',
  }
])
 
console.log(mergedSx.color) // black

This comes in handy when creating composite components that accept an sx prop, and pass individual style properties down to specific children.

For example, here's how you might use mergeSx() to extract the color style from a <ButtonContent> component's props, and pass it down to nested <Icon> and <Caret> elements:

export const ButtonContent = React.forwardRef((props, ref) => {
  const { caret, glyph, label, sx, ...rest } = props
 
  const css = useSx({ sx })
  const color = mergeSx(sx).color
 
  return (
    <div css={css} ref={ref} {...rest}>
      <Icon
        glyph={glyph}
        label={label}
        sx={{
          color,
          size: 16,
        }}
      />
      {label}
      {caret && (
        <>
          <Space width={4} />
          <Caret sx={{ color }} />
        </>
      )}
    </div>
  )
})

License

MIT licensed.

Package Sidebar

Install

npm i use-sx

Weekly Downloads

0

Version

0.1.0

License

MIT

Unpacked Size

222 kB

Total Files

49

Last publish

Collaborators

  • jamesknelson