diff --git a/README.md b/README.md index bdb527c..309ad6f 100644 --- a/README.md +++ b/README.md @@ -1,145 +1,93 @@ # Tailwind Buddy: Your Friendly Helper for Composing Tailwind Classes 🎨 -Welcome! If you are looking for: +## What problem does Tailwind Buddy solve? + +Tailwind Buddy addresses common challenges in managing Tailwind classes for complex, responsive designs. It offers a streamlined approach to: + +- Organize and compose Tailwind classes for different component variants +- Handle responsive designs with ease, reducing potential screen flickering in SSR applications +- Manage compound variants that work responsively +- Utilize a slot-based system for breaking down components into smaller, manageable parts + +## Key Features -- The fastest Tailwind variant utility, as demonstrated in [our benchmarks](./packages/benchmark/README.md) or [see here](#benchmarks) - Tools for building Tailwind variant components - Support for responsive variant props -- Ease of use, balancing developer experience for both library builders and users -- Compound variants that work responsively, overriding classes based on variant values and other props - Framework-agnostic solutions (works well with frameworks other than React) - SSR-friendly class generation, both responsive and non-responsive -- The ability to use slots (to break down components into smaller parts while using the same props) -- An actively maintained package used by our company for our new design system -- This library is opinionated and inspired by - -This library is opinonated and inspired by [CVA](https://cva.style/docs) and [tailwind-variants](https://github.com/nextui-org/tailwind-variants). - -## Minimum setup (no responsive values, no compounds) - - -### Vscode settings for Tailwind Autocomplete - -For the best experience, set up your VSCode settings as follows: - -`.vscode/settings.json` - -```json -{ - "editor.quickSuggestions": { - "strings": "on" - }, - "css.validate": false, - "editor.inlineSuggest.enabled": true, - "tailwindCSS.classAttributes": [ - "class", - "className", - ".*Styles.", - ".*Classes." - ], - "tailwindCSS.experimental.classRegex": [ - "@tw\\s\\*\/\\s+[\"'`]([^\"'`]*)" - ] -} -``` +- Ability to use slots for component composition +- Compound variants that work responsively, overriding classes based on variant values and other props +- High-performance variant utility, as demonstrated in [our benchmarks](./packages/benchmark/README.md) -The key part is `tailwindCSS.experimental.classRegex`, which autocompletes the string when you put `/** @tw */` in front. You will see how we use it in the label example. +This library is inspired by [CVA](https://cva.style/docs) and [tailwind-variants](https://github.com/nextui-org/tailwind-variants), offering our unique approach to solving common Tailwind challenges. -### Installation +## Installation -``` +```bash pnpm add @busbud/tailwind-buddy ``` -### Create your first component +## Usage -Component typing: +Let's create a button component with two variants, featuring different background colors on mobile and desktop. -``` tsx -export interface LabelBaseProps - extends React.HTMLAttributes { +```tsx +import { compose } from "@busbud/tailwind-buddy"; +import type { VariantsProps } from "@busbud/tailwind-buddy"; + +interface ButtonBaseProps + extends React.ButtonHTMLAttributes { as?: React.ElementType; - disabled?: boolean; } -``` - -Component variant definition -``` tsx -import { compose } from "@busbud/tailwind-buddy" -import { LabelBaseProps } from "./Label.types" -import type { VariantsProps } from "@busbud/tailwind-buddy" - -export const labelVariants = compose({ - "slots": { // you will always have at least the root slot to define - "root": /** @tw */ "text-blue-500" // We do use /** @tw */ to be able to have auto complete from tailwind +export const buttonVariants = compose({ + slots: { + root: "px-4 py-2 rounded", + }, + variants: { + intent: { + primary: "bg-blue-500 text-white", + secondary: "bg-gray-200 text-gray-800", }, - "variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": /** @tw */ "text-7xl" - }, - "fontWeight": { - xxl: { - "root": /** @tw */ "font-extrabold" - } - } + size: { + small: "text-sm", + large: "text-lg", }, - "defaultVariants": { // all variants should have a default values - "size": "small", - "fontWeight": "xxl" - } -})() - -export type LabelProps = VariantsProps -``` - -Key Takeaways: - -- You need at least one slot, root. -- You must define default values for all variants. -- We use `/** @tw */` for Tailwind autocomplete. - -To maximize benefits, we recommend using [tailwind merge](#adding-tailwind-merge-to-minify-the-string-generated) - -## Adding Tailwind Merge to Minify the Generated String - -Our package does not optimize the class string size. As tailwind-merge is highly efficient for this purpose, we chose not to create another solution. This allows you to use tailwind-merge outside the design system and manage one version of it. - -### Install tailwind merge - -`pnpm add tailwind-merge` - -now you can use it in two ways - -1) without updating tailwind merge + }, + defaultVariants: { + intent: "primary", + size: "small", + }, + responsiveVariants: ["intent"], +})(); -```tsx -import React from "react"; +export type ButtonProps = VariantsProps; -import { PropsWithChildren } from "react"; -import { LabelProps, labelVariants } from "./Label.variants"; -import { twMerge } from "tailwind-merge" +// Usage in a React component +import { twMerge } from "tailwind-merge"; -export const Label: React.FC> = ({ - as: Component = "span", +export const Button: React.FC> = ({ + as: Component = "button", + intent, + size, className, children, - fontWeight, - size, - disabled, ...restProps }) => { - const { root } = labelVariants + const { root } = buttonVariants; return ( {children} @@ -148,506 +96,126 @@ export const Label: React.FC> = ({ }; ``` -2) By extending the default tailwind merge - -```tsx -import {extendTailwindMerge} from "tailwind-merge"; - -export const COMMON_UNITS = ["small", "medium", "large"]; - -export const twMergeConfig = { - theme: { - opacity: ["disabled"], - spacing: [ - "divider", - "unit", - ], - borderWidth: COMMON_UNITS, - borderRadius: COMMON_UNITS, - }, - classGroups: { - shadow: [{shadow: COMMON_UNITS}], - "font-size": [{text: ["tiny", ...COMMON_UNITS]}], - "bg-image": ["bg-stripe-gradient"], - "min-w": [ - { - "min-w": ["unit", "unit-2", "unit-4", "unit-6", "unit-8", "unit-10", "unit-12", "unit-14"], - }, - ], - }, -}; -export const twMerge = extendTailwindMerge(twMergeConfig); -``` - -Then use it as the first example but instead of importing from `tailwind-merge` you would import from this file. +In this example, we've created a button component with `intent` and `size` variants. The `intent` variant is responsive, changing from `primary` on mobile to `secondary` on desktop. -## className override +## Working with Slots -Here the example +Slots allow you to break down components into smaller parts while using the same props. Here's an example: ```tsx -
- Root element -
-``` - -You will see here the `className` we pass here. This property will always be place at the end of the string. In terms of pure css and selectors this has no impact but whats matter its where its positionned in your css. - -Tailwind is taking the order in count when creating its css. So we have decided to put className as a the latest override. We will explain in the next point how overrides are working with "How classes string is built" - -## How classes string is built - -Basically what you need to remember is this order: - -- slot values define in slots object -- variants definition if no props passed for a variant it will take the default variant value (take not that the order of variant is not reliable) -- if responsive variants responsive defintion -- compound variants -- You will see later but for specific use case you may have compound responsive -- className override - -If we have to create a mental class string it would be: - -`slot-class variant-class md:variant-class compound-classes md:compound-classes className` - -## Working with slots - -Lets take our latest example and add one other slot. - -```ts -export const labelVariants = compose({ - "slots": { - "root": /** @tw */ "text-blue-500", - "otherSlot": /** @tw */ "text-blue-500" // here our new slot - }, - "variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": { - root: /** @tw */ "text-7xl", - otherSlot: /** @tw */ "text-12xl" - } - }, - "fontWeight": { - xxl: { - "root": /** @tw */ "font-extrabold" - } - } - }, - "defaultVariants": { - "size": "small", - "fontWeight": "xxl" - } -})() -``` - -And lets update the usage - -```tsx - -const { root, otherSlot } = labelVariants - - return ( - <> -
- Root element -
-
- Other slot -
- - ); -``` - -As you see its really easy to add and compose with slots as by default you will always have a root slot. - -## Working with compound variants - -Compound variant are conditions to apply when you have multiple variant values. But you can also pass any values you want. - -Note that for compound variants as its an array we do apply the classes in the same order in the array. - -```ts -export const labelVariants = compose({ - "slots": { - "root": /** @tw */ "text-blue-500", - "otherSlot": /** @tw */ "text-blue-500" // here our new slot - }, - "variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": { - root: /** @tw */ "text-7xl", - otherSlot: /** @tw */ "text-12xl" - } - }, - "fontWeight": { - xxl: { - "root": /** @tw */ "font-extrabold" - } - } - }, - "defaultVariants": { - "size": "small", - "fontWeight": "xxl" - }, - "compoundVariants": [ - { - "conditions": { - "size": "large", - }, - "class": /** @tw */ "bg-red-500 text-blue-500" // as soon as size is large it will apply this to all the slots +export const cardVariants = compose({ + slots: { + root: "rounded overflow-hidden", + header: "p-4 bg-gray-100", + body: "p-4", + }, + variants: { + size: { + small: { + root: "max-w-sm", + header: "text-sm", + body: "text-base", + }, + large: { + root: "max-w-lg", + header: "text-lg", + body: "text-xl", }, - { - "conditions": { - fontWeight: "xxl", - size: "small" - }, - class: { - otherSlot: /** @tw */ "bg-gray-500 border-red-500" // as soon as conditions are met it will apply this to only otherSlot - } - } - ] -})() -``` -By default if you do not pass `` you will have only auto complete on the variant property when using the slot function - -```tsx -
-``` - -If you want to have other props that are not variants you need to add the `` to be able to have the auto complete and not have typescript error. - -```tsx -
-``` - -Note: We have an existing issue when you are creating conditions the other props are not auto complete, only variants are. But you won't have typescript issues as it accept any other key: string / boolean - -## Working with responsive Variants - -First you need to understand why we do have this feature. in SSR (server side rendering) when you render the page -to the user, you technically don't know the page of the user. And lets say you do mobile first. You can endup with some screen flickering if you do css in js. - -To solve that we have added this feature that will generate the good tailwind responsive values base on what you want. - -### Enable responsive - -Before looking at usage, you will need to add two things: - -1) add responsiveVariant definition - -```ts -export const labelVariants = compose({ - "slots": { - "root": /** @tw */ "text-blue-500", - "otherSlot": /** @tw */ "text-blue-500" // here our new slot - }, - "variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": { - root: /** @tw */ "text-7xl", - otherSlot: /** @tw */ "text-12xl" - } - }, - "fontWeight": { - xxl: { - "root": /** @tw */ "font-extrabold" - } - } - }, - "defaultVariants": { - "size": "small", - "fontWeight": "xxl" }, - "responsiveVariants": ["size"] -})() -``` - -this is important you add responsiveVariants so we will be able to generate tailwind classes. - -2) Update tailwind config to have safeList - -Safelist in tailwind is a way to force tailwind to add the classes we need to be able to handle responsivness. As tailwind is doing static analysis to add the classes to the output or not. In our case we will not have those available in the code so we need to add them via safeList options: - -```ts - -export const screens: Screens[] = ["sm", "md", "lg", "xl"] - -export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,mdx}"], - theme: { - extend: { - screens - } }, - safelist: generateSafeList([labelVariants], screens) // import your variant definition the generateSafeList is taking care of the rest -}; -``` - -Note: that we have added a screens property here. Make sure to add it here also. We have an on going issue to make the screens overridable in typescript and also in the config. Adding inside the config is easy but we do not have typescript auto complete working yet. So for now please use the default tailwind screen definition - -3) Simple Usage - -Now everything is defined and you understand how it works lets see the usage: - -```ts -root({ - "size": { - "initial": "small", // this is mandatory to have an intiial option to respect mobile first approach - "md": "large" + defaultVariants: { + size: "small", }, - "fontWeight": "xxl" // this would not accept a responsive value as you didnt' define it in the responsiveVariants array -}) -``` +}); -With our latest definition - -```ts -"slots": { - "root": /** @tw */ "text-blue-500", - "otherSlot": /** @tw */ "text-blue-500" // here our new slot -}, -"variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": { - root: /** @tw */ "text-7xl", - otherSlot: /** @tw */ "text-12xl" - } - }, - "fontWeight": { - xxl: { - "root": /** @tw */ "font-extrabold" - } - } -}, -"defaultVariants": { - "size": "small", - "fontWeight": "xxl" -}, -"responsiveVariants": ["size"] +// Usage +const { root, header, body } = cardVariants({ size: "large" }); ``` -This would produce a class string like - -`text-blue-500 text-xs md:text-7xl font-extrabold` - -As you see we do regroup the responsive values right after their initial values. +## Compound Variants -4) Complex usage 1. How one responsive work with Compound +Compound variants allow you to apply styles when multiple variant conditions are met: -here our definition with compound - -```ts -"slots": { - "root": /** @tw */ "text-blue-500", - "otherSlot": /** @tw */ "text-blue-500" // here our new slot -}, -"variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": { - root: /** @tw */ "text-7xl", - otherSlot: /** @tw */ "text-12xl" - } - }, - "fontWeight": { - xxl: { - "root": /** @tw */ "font-extrabold" - } - } -}, -"defaultVariants": { - "size": "small", - "fontWeight": "xxl" -}, -"responsiveVariants": ["size"], -"compoundVariants": [ +```tsx +export const buttonVariants = compose({ + // ... previous definitions ... + compoundVariants: [ { - "conditions": { - "size": "large", + conditions: { + intent: "primary", + size: "large", }, - "class": /** @tw */ "bg-red-500 text-blue-500" + class: "font-bold", }, - { - "conditions": { - fontWeight: "xxl", - size: "small" - }, - class: /** @tw */ "bg-gray-500 border-red-500" - } -] -``` - -When we do have responsive props what we would do generally is creating an object to all breakpoints you have defined. - -Fill the values with their initial values if there is no other responsive props. - -So in our use case - -```ts -root({ - "size": { - "initial": "small", // this is mandatory to have an intiial option to respect mobile first approach - "md": "large" - }, - "fontWeight": "xxl", // this would not accept a responsive value as you didnt' define it in the responsiveVariants array, - className: "awesome-class" -}) + ], +}); ``` -would become - -`text-blue-500 text-xs md:text-7xl md:bg-red-500 md:text-blue-500 bg-gray-500 border-red-500 awesome-class` +## Responsive Variants -5) Complex usage 2. How multiple responsive work with Compound +To enable responsive variants: -here our definition with compound +1. Add the variant to the `responsiveVariants` array in your compose function. +2. Update your Tailwind config to include necessary classes in the safelist. -```ts -"slots": { - "root": /** @tw */ "text-blue-500", - "otherSlot": /** @tw */ "text-blue-500" -}, -"variants": { - "size": { - "small": /** @tw */ "text-xs", - "large": { - root: /** @tw */ "text-7xl", - otherSlot: /** @tw */ "text-12xl" - } - }, - "fontWeight": { - md: /** @tw */ "super-small" - xxl: { - "root": /** @tw */ "font-extrabold" - }, - } -}, -"defaultVariants": { - "size": "small", - "fontWeight": "xxl" -}, -"responsiveVariants": ["size", "fontWeight"], // note we have added also the fontWeight here -"compoundVariants": [ - { - "conditions": { - "size": "large", - }, - "class": /** @tw */ "bg-red-500 text-blue-500" - }, - { - "conditions": { - fontWeight: "xxl", - size: "small" - }, - class: /** @tw */ "bg-gray-500 border-red-500" - } -] -``` +```tsx +// In your variant definition +export const buttonVariants = compose({ + // ... other configurations ... + responsiveVariants: ["intent"], +}); -Usage: +// In your Tailwind config +import { generateSafeList } from "@busbud/tailwind-buddy"; +import { buttonVariants } from "./path-to-your-variants"; -```ts -root({ - "size": { - "initial": "small", // this is mandatory to have an intiial option to respect mobile first approach - "md": "large" - }, - "fontWeight": { - "initial": "md", - "md": "xxl", - }, // this would not accept a responsive value as you didnt' define it in the responsiveVariants array, - className: "awesome-class" -}) +export default { + // ... other Tailwind configurations ... + safelist: generateSafeList([buttonVariants], ["sm", "md", "lg", "xl"]), +}; ``` -Before we show the ouput you need to understand how we will operate here. - -We do check if the conditions are met at the same breakpoint. If yes we put the breakpoint in front otherwise we do nothing. - -So to the conditions to met we would need here conditions with - -`size = large & fontWeight = md` or `size = large & fontWeight xxl` +## Tailwind Autocomplete in VSCode (Optional) -here our conditions +For Tailwind class autocomplete in VSCode, add the following to your `.vscode/settings.json`: -```ts -{ - "conditions": { - "size": "large", - // as the fontWeight not defined here. we will take the initial - }, - "class": /** @tw */ "bg-red-500 text-blue-500" -}, +```json { - "conditions": { - fontWeight: "xxl", - size: "large" + "editor.quickSuggestions": { + "strings": "on" }, - class: /** @tw */ "bg-gray-500 border-red-500" + "css.validate": false, + "editor.inlineSuggest.enabled": true, + "tailwindCSS.classAttributes": [ + "class", + "className", + ".*Styles.", + ".*Classes." + ], + "tailwindCSS.experimental.classRegex": ["@tw\\s\\*/\\s+[\"'`]([^\"'`]*)"] } ``` -Here only the the second condition will met and its a `md` breakpoint. - -output - -`text-blue-500 text-xs md:text-7xl super-small md:font-extrabold md:bg-gray-500 md:border-red-500 awesome-class` - -## local development - -Generally this is the way you will want to work - -Make sure install pnpm if you don't have it `npm i -g pnpm` - -at root folder: -- `nvm use` -- `pnpm install` - -going to core folder - -- `pnpm build -w` -- if you want to work with only unit tests. `pnpm test:unit` - -if you want to work with a "real world example" +With this setup, you can use `/** @tw */` before your Tailwind classes to enable autocompletion. -go to `ui` folder: -- `pnpm install` -- `pnpm build -w` +## Local Development -And if you want to see a usage of the lib consuming `tailwind-buddy` +1. Install pnpm: `npm i -g pnpm` +2. In the root folder: + - `nvm use` + - `pnpm install` +3. In the `core` folder: + - `pnpm build -w` + - For unit tests: `pnpm test:unit` -go to `sandbox` folder: -- `pnpm install` -- `pnpm dev` +For a "real world example": +1. In the `ui` folder: + - `pnpm install` + - `pnpm build -w` +2. In the `sandbox` folder: + - `pnpm install` + - `pnpm dev` ## Contributing @@ -659,4 +227,4 @@ After that make sure to look at [good first issue label on github.](https://gith TCA is our lib. -![](./packages/benchmark/benchmarks.png) \ No newline at end of file +![](./packages/benchmark/benchmarks.png)