0.2.19 • Published 7 months ago

react-styled-classnames v0.2.19

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

This package is continued under a new name: react-classmate

Check it out

react-styled-classnames

A tool for managing react component class names and variants with the simplicity of styled-components. Designed for use with utility-first CSS libraries and SSR.

🚩 Transform this

const SomeButton = ({ isLoading, ...props }) => {
  const activeClass = isLoading ? 'bg-blue-400 text-white' : 'bg-blue-800 text-blue-200'

  return (
    <button
      {...props}
      className={`transition-all mt-5 border-1 md:text-lg text-normal ${someConfig.transitionDurationEaseClass} ${activeClass} ${props.className || ''}`}
    >
      {props.children}
    </button>
  )
}

🌤️ Into this

const ButtonBase = rsc.button`
  text-normal
  md:text-lg
  mt-5
  border-1
  transition-all
  ${someConfig.transitionDurationEaseClass}
  ${(p) => (p.$isLoading ? "opacity-90 pointer-events-none" : "")}
`

Features

  • Dynamic class names
  • Variants
  • Extend components
  • React, no other dependencies
  • TypeScript support
  • SSR compatibility

Contents

re-inventing the wheel?

Yes kind of, while twin.macro requires styled-components, and tailwind-styled-components isn’t fully compatible with Vike - See Issue here.

Getting started

Let's assume you have installed React (> 16.8.0)

npm i react-styled-classnames --save-dev
# or
yarn add react-styled-classnames --dev

Basic

create a component by calling rsc with a tag name and a template literal string.

import rsc from 'react-styled-classnames'

const Container = rsc.div`
  py-2
  px-5
  min-h-24
`
// transforms to: <div className="py-2 px-5 min-h-24" />

Extend

Extend a component directly by passing the component and the tag name.

import MyOtherComponent from './MyOtherComponent' // () => <button className="text-lg mt-5" />
import rsc from 'react-styled-classnames'

const Container = rsc.extend(MyOtherComponent)`
  py-2
  px-5
  min-h-24
`
// transforms to: <button className="text-lg mt-5 py-2 px-5 min-h-24" />

Use with props

Pass props to the component and use them in the template literal string and in the component prop validation.

// hey typescript
interface ButtonProps {
  $isActive?: boolean
  $isLoading?: boolean
}
const SomeButton = rsc.button<ButtonProps>`
  text-lg
  mt-5
  ${p => p.$isActive ? 'bg-blue-400 text-white' : 'bg-blue-400 text-blue-200'}
  ${p => p.$isLoading ? 'opacity-90 pointer-events-none' : ''}
`
// transforms to <button className="text-lg mt-5 bg-blue-400 text-white opacity-90 pointer-events-none" />

Prefix incoming props with $

Note how we prefix the props incoming to dc with a $ sign. This is a important convention to distinguish dynamic props from the ones we pass to the component.

This pattern should also avoid conflicts with reserved prop names.

Create Variants

Create variants by passing an object to the variants key like in cva. The key should match the prop name and the value should be a function that returns a string. You could also re-use the props in the function.

interface AlertProps extends HTMLAttributes<HTMLDivElement> {
  $severity: "info" | "warning" | "error";
  $isActive?: boolean;
}
const Alert = rsc.div.variants<AlertProps>({
  // optional
  base: p => `
    ${isActive ? 'custom-active' : 'custom-inactive'}
    p-4
    rounded-md
  `,
  // required
  variants: {
    $severity: {
      info: (p) => `bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}`,
      warning: (p) => `bg-yellow-100 text-yellow-800 ${p.$isActive ? "font-bold" : ""}`,
      error: (p) => `bg-red-100 text-red-800 ${p.$isActive ? "ring ring-red-500" : ""}`,
    },
  },
  // optional - used if no variant was found
  defaultVariant: {
    $severity: "info",
  }
});

export default () => <Alert $severity="info" $isActive />
// outputs: <div className="custom-active p-4 rounded-md bg-blue-100 text-blue-800 shadow-lg" />

due to a current limitiation the extension ... extends HTMLAttributes<HTMLDivElement>is needed for the variants to infer the intrinsic props down to the implemented component

Receipes for rsc.extend

With rsc.extend, you can build upon any base React component—adding new styles and even supporting additional props. This makes it easy to create reusable component variations without duplicating logic.

import { ArrowBigDown } from 'lucide-react'
import rsc from 'react-styled-classnames'

const StyledLucideArrow = rsc.extend(ArrowBigDown)`
  md:-right-4.5
  right-1
  slide-in-r-20
`

// note how we can pass props which are only accessible on a Lucid Component
export default () => <StyledLucideArrow stroke="3" />

⚠️ Having problems by extending third party components, see: Extending other lib components

Now we can define a base component and extend it with additional styles and classes and pass properties. You can pass the types to the extend function to get autocompletion and type checking on the way.

import rsc from 'react-styled-classnames'

interface StyledSliderItemBaseProps {
  $active: boolean
}
const StyledSliderItemBase = rsc.button<StyledSliderItemBaseProps>`
  absolute
  h-full
  w-full
  left-0
  top-0
  ${p => (p.$active ? 'animate-in fade-in' : 'animate-out fade-out')}
`

interface NewStyledSliderItemProps extends StyledSliderItemBaseProps {
  $secondBool: boolean
}
const NewStyledSliderItemWithNewProps = rsc.extend(StyledSliderItemBase)<NewStyledSliderItemProps>`
  rounded-lg
  text-lg
  ${p => (p.$active ? 'bg-blue' : 'bg-red')}
  ${p => (p.$secondBool ? 'text-underline' : 'some-class-here')}
`

export default () => <NewStyledSliderItemWithNewProps $active $secondBool={false} />
// outputs: <button className="absolute h-full w-full left-0 top-0 animate-in fade-in rounded-lg text-lg bg-blue" />

Use rsc for creating base component

Extend a component directly by passing the component and the tag name.

const BaseButton = rsc.extend(rsc.button``)`
  text-lg
  mt-5
`

extend from variants

interface ButtonProps extends InputHTMLAttributes<HTMLInputElement> {
  $severity: "info" | "warning" | "error";
  $isActive?: boolean;
}

const Alert = rsc.input.variants<ButtonProps>({
  base: "p-4",
  variants: {
    $severity: {
      info: (p) => `bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}`,
    },
  },
});

const ExtendedButton = rsc.extend(Alert)<{ $test: boolean }>`
  ${p => p.$test ? "bg-green-100 text-green-800" : ""}
`

export default () => <ExtendedButton $severity="info" $test />
// outputs: <input className="p-4 bg-blue-100 text-blue-800 shadow-lg bg-green-100 text-green-800" />

custom mapping function for props

  • this is deprecated, since we have the extend function
interface NoteboxProps {
  $type?: 'info' | 'warning' | 'error' | 'success' | 'aside'
}

const typeClass = (type: NoteboxProps['$type']) => {
  switch (type) {
    case 'warning':
      return 'border-warningLight bg-warningSuperLight'
    case 'error':
      return 'border-errorLight bg-errorSuperLight'
    case 'success':
      return 'border-successLight bg-successSuperLight'
    case 'aside':
      return 'border-graySuperLight bg-light'
    // info
    default:
      return 'border-graySuperLight bg-white'
  }
}

const Notebox = rsc.div<NoteboxProps>`
  p-2
  md:p-4
  rounded
  border-1
  ${p => typeClass(p.$type || 'info')}
`

export default Notebox

Auto infer types for props

By passing the component, we can validate the component to accept tag related props. This is useful if you wanna rely on the props for a specific element without the $ prefix.

// if you pass rsc component it's types are validated
const ExtendedButton = rsc.extend(rsc.button``)`
  some-class
  ${p => p.type === 'submit' ? 'font-normal' : 'font-bold'}
`

// infers the type of the input element + add new props
const MyInput = ({ ...props }: HTMLAttributes<HTMLInputElement>) => (
  <input {...props} />
)
const StyledDiv = rsc.extend(MyInput)<{ $trigger?: boolean }>`
  bg-white
  ${p => p.$trigger ? "!border-error" : ""}
  ${p => p.type === 'submit' ? 'font-normal' : 'font-bold'}
`

Extending other lib components / Juggling with components that are any

Unfortunately we cannot infer the type directly of the component if it's any or loosely typed. But we can use a intermediate step to pass the type to the extend function.

import { ComponentProps } from 'react'
import { MapContainer } from 'react-leaflet'
import { Field, FieldConfig } from 'formik'
import rsc, { RscBaseComponent } from 'react-styled-classnames'

// we need to cast the type to ComponentProps
type StyledMapContainerType = ComponentProps<typeof MapContainer>
const StyledMapContainer: RscBaseComponent<StyledMapContainerType> = rsc.extend(MapContainer)`
  absolute
  h-full
  w-full
  text-white
  outline-0
`

export const Component = () => <StyledMapContainer bounds={...} />

// or with Formik

import { Field, FieldConfig } from 'formik'

type FieldComponentProps = ComponentProps<'input'> & FieldConfig
const FieldComponent = ({ ...props }: FieldComponentProps) => <Field {...props} />

const StyledField = rsc.extend(FieldComponent)<{ $error: boolean }>`
  theme-form-field
  w-full
  ....
  ${p => (p.$error ? '!border-error' : '')}
`

export const Component = () => <StyledField placeholder="placeholder" as="select" name="name" $error />

⚠️ This is a workaround! This is a bug - we should be able to cast the type directly in the interface in which we pass $error. Contributions welcome.

Upcoming

V1

  • rename rsc (abbreviation for react server components) to react-classmate - rc - rc.div / rc.extends(...)
  • Variants for rsc.extend
  • $ prefix should be optional (at least for variants) ✅
  • default variants ✅

Backlog

  • Integrate more tests focused on SSR and React
  • Advanced IDE integration
    • show generated default class on hover
    • enforce autocompletion and tooltips from the used libs

Version > 0.1

👋 Due to bundle size I removed V1 from this package. it's still available, but unmaintained under this package: https://www.npmjs.com/package/react-dynamic-classnames

Inspiration

0.2.19

7 months ago

0.2.18

7 months ago

0.2.17

7 months ago

0.2.16

7 months ago

0.2.15

7 months ago

0.2.14

7 months ago

0.2.13

7 months ago

0.2.12

7 months ago

0.2.11

7 months ago

0.2.8

7 months ago

0.2.7

7 months ago

0.2.6

7 months ago

0.2.5

7 months ago

0.2.4

7 months ago

0.2.3

7 months ago

0.2.2

7 months ago

0.2.1

7 months ago

0.2.0

7 months ago

0.1.30

7 months ago

0.1.29

7 months ago

0.1.28

7 months ago

0.1.27

7 months ago

0.1.25

7 months ago

0.1.24

7 months ago

0.1.23

7 months ago

0.1.22

7 months ago

0.1.21

7 months ago

0.1.20

7 months ago

0.1.19

7 months ago

0.1.18

7 months ago

0.1.17

7 months ago

0.1.16

7 months ago

0.1.15

7 months ago

0.1.14

7 months ago

0.1.13

7 months ago

0.1.12

7 months ago

0.1.11

7 months ago

0.1.10

7 months ago

0.1.8

7 months ago

0.1.7

7 months ago

0.1.6

7 months ago

0.1.5

7 months ago

0.1.4

7 months ago

0.1.2

7 months ago

0.1.1

7 months ago

0.1.0

7 months ago

0.0.27

7 months ago

1.0.60

9 months ago

1.0.54

9 months ago

1.0.53

9 months ago

1.0.52

9 months ago

1.0.51

9 months ago

1.0.50

9 months ago

1.0.46

9 months ago

1.0.44

9 months ago

1.0.42

9 months ago

1.0.41

9 months ago

1.0.40

9 months ago

1.0.39

9 months ago

1.0.38

9 months ago

1.0.37

9 months ago

1.0.36

9 months ago

1.0.35

9 months ago

1.0.34

9 months ago

1.0.33

9 months ago

1.0.32

9 months ago

1.0.31

9 months ago

1.0.30

9 months ago

1.0.29

9 months ago

1.0.28

9 months ago

1.0.27

9 months ago

1.0.26

9 months ago

1.0.25

9 months ago

1.0.24

9 months ago

1.0.23

9 months ago

1.0.22

9 months ago

1.0.21

9 months ago

1.0.20

9 months ago

1.0.19

9 months ago

1.0.18

9 months ago

1.0.17

9 months ago

1.0.16

9 months ago

1.0.15

9 months ago