sannajammeh / tw-classed

https://tw-classed.vercel.app
MIT License
518 stars 8 forks source link

[Feature Request] Complex/Advanced Compound Variants #94

Open randallmlough opened 1 year ago

randallmlough commented 1 year ago

Feature Request

I'm looking for more functionality around compound variants. I would love a way to use a keyed value from a component as the key for a variant in compound variants.

If the above doesn't make a lot of sense, here's an example of a workaround that I have that achieves what I am after. Hopefully, this will illustrate the problem better.

Below is a button that can have multiple states: plain text, a solid color, or outlined

<Button>Text Button</Button>
<Button solid>Solid Primary Button</Button>
<Button color="secondary" solid>Solid Secondary Button</Button>
<Button outline>Outlined Primary Button</Button>
<Button color="secondary" outline>Outlined Secondary Button</Button>

To have the above functionality my button component needs to look like this

// Note: in my project I have customized classed to leverage tailwind merge: https://tw-classed.vercel.app/docs/guide/configuring-classed#using-tailwind-merge
// import { classed } from "./ui";
import { classed } from "@tw-classed/react"

const solidVariants = {
  true: "",
  primary: "bg-indigo-600 hover:bg-indigo-700 text-white",
  secondary: "bg-gray-600 hover:bg-gray-700 text-white",
};

const Button = classed(
  "button",
  "inline-flex items-center border border-transparent font-medium",
  {
    variants: {
      color: {
        primary: "text-indigo-600",
        secondary: "text-gray-600",
      },
      solid: solidVariants,
      outline: {
        true: "",
      },
    },
    compoundVariants: [
      {
        solid: true,
        color: "primary",
        className: solidVariants["primary"],
      },
      {
        solid: true,
        color: "secondary",
        className: solidVariants["secondary"],
      },
      {
        outline: true,
        color: "primary",
        className:
          "border border-indigo-600 hover:border-indigo-700 text-indigo-600",
      },
      {
        outline: true,
        color: "secondary",
        className: "border border-gray-600 hover:border-gray-700 text-gray-600",
      },
    ],
    defaultVariants: {
      color: "primary",
    },
  }
);

As you can see, I'm abusing the compound variant array and the boolean variant attribute. In a perfect world, condensing the "solid" and "outline" variants into one would be awesome.

An idea that could solve this is to have class/className accept either a string or a function that passes a variants object and returns a string.

Example:

compoundVariants: [
      {
        solid: true,
        className: (variants) => solidVariants[variants.color],
        // variants is a key/value object
        // <Button solid> -> variants: {color: primary, solid: true}
        // <Button color="secondary" solid> -> variants: {color: secondary, solid: true}
      },
      {
        outline: true,
        className: (variants) => outlineVariants[variants.color], 
      },
]
sannajammeh commented 1 year ago

I've thought about something similar. Essentially introducing Stitches.js variant serialization. It would be make your "abuse" much simpler, but I will consider this too. I like the callback idea :)

I have an advanced button component if you're looking for something similar here: https://github.com/sannajammeh/opendash/blob/main/packages/ui/src/Button.tsx

This uses plenty of compound variants. I wouldn't classify it as abuse as Stitches.js recommends the same thing. Keep in mind the template literal class names require tailwind-merge or a custom merger in classed.config.ts, otherwise the DOM will look very ugly.

randallmlough commented 1 year ago

Cool. I looked at your example and yeah, very similar. The current process works for a couple of items, but the cracks begin to show when have a much larger variant object like the real one I have below.

const outlineCompoundVariants = [
  {
    outline: true,
    color: "primary",
    className: outlineVariants["primary"],
  },
  {
    outline: true,
    color: "primary-light",
    className: outlineVariants["primary-light"],
  },
  {
    outline: true,
    color: "primary-dark",
    className: outlineVariants["primary-dark"],
  },
  {
    outline: true,
    color: "secondary",
    className: outlineVariants["secondary"],
  },
  {
    outline: true,
    color: "secondary-light",
    className: outlineVariants["secondary-light"],
  },
  {
    outline: true,
    color: "secondary-dark",
    className: outlineVariants["secondary-dark"],
  },
  {
    outline: true,
    color: "light",
    className: outlineVariants["light"],
  },
  {
    outline: true,
    color: "dark",
    className: outlineVariants["dark"],
  },
  {
    outline: true,
    color: "success",
    className: outlineVariants["success"],
  },
  {
    outline: true,
    color: "danger",
    className: outlineVariants["danger"],
  },
];

This is just for outline styling. I have the same object size for solid colors and focus too. That said, I wouldn't recommend what I'm doing for a one-off project, but this is part of a UI package that I use across all my projects and with other people, so the need to have uniformity (along with leveraging stories) outweighs the decrease in readability.

Regardless, I acknowledge it is a monster of my own creation

sannajammeh commented 1 year ago

Going to start the work on this feature! Thanks for the request

randallmlough commented 1 year ago

Hey @sannajammeh, any progress on this feature? I have some time in the next couple of weeks if I can be of any help.

sannajammeh commented 1 year ago

Hey @randallmlough. Apologies for the long wait time on this issue, I've been pretty swamped with work. Still going to be a bit swamped the coming weeks I'm afraid. Feel free to fork and submit a PR.

ioss commented 11 months ago

@randallmlough couldn't you do something like:

const outlineCompoundVariants = Object.keys(outlineVariants).map((color) => ({
  outline: true,
  color,
  className: outlineVariants[color],
}));

?