Lzh on GitHub

Slots 允许您将一个组件拆分为多个部分。

基本用法

您可以使用 slots 键来添加插槽。可以添加的插槽数量没有限制。

import { tv } from 'tailwind-variants';
 
const card = tv({
  slots: {
    base: 'md:flex bg-slate-100 rounded-xl p-8 md:p-0 dark:bg-gray-900',
    avatar:
      'w-24 h-24 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg',
    wrapper: 'flex-1 pt-6 md:p-8 text-center md:text-left space-y-4',
    description: 'text-md font-medium',
    infoWrapper: 'font-medium',
    name: 'text-sm text-sky-500 dark:text-sky-400',
    role: 'text-sm text-slate-700 dark:text-slate-500'
  }
});
 
const { base, avatar, wrapper, description, infoWrapper, name, role } = card();
 
return (
  <figure className={base()}>
    <img
      className={avatar()}
      src="/intro-avatar.png"
      alt=""
      width="384"
      height="512"
    />
    <div className={wrapper()}>
      <blockquote>
        <p className={description()}>
          “Tailwind variants allows you to reduce repeated code in your project
          and make it more readable. They fixed the headache of building a
          design system with TailwindCSS.”
        </p>
      </blockquote>
      <figcaption className={infoWrapper()}>
        <div className={name()}>Zoey Lang</div>
        <div className={role()}>Full-stack developer, HeroUI</div>
      </figcaption>
    </div>
  </figure>
);

带变体的插槽

您还可以使用 variants 来更改整个组件及其插槽。更多关于变体的信息,请参见 此处

Tailwind Variants 将自动为每个插槽选择正确的变体。
import { useState } from 'react';
import { tv } from 'tailwind-variants';
// 导入您的组件
import { RadioGroup, Radio } from '@components';
 
const item = tv({
  slots: {
    base: 'flex flex-col mb-4 sm:flex-row p-6 bg-white dark:bg-stone-900 drop-shadow-xl rounded-xl',
    imageWrapper:
      'flex-none w-full sm:w-48 h-48 mb-6 sm:mb-0 sm:h-auto relative z-10 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:rounded-xl before:bg-[#18000E] before:bg-gradient-to-r before:from-[#010187]',
    img: 'sm:scale-125 absolute z-10 top-2 sm:left-2 inset-0 w-full h-full object-cover rounded-lg',
    title:
      'relative w-full flex-none mb-2 text-2xl font-semibold text-stone-900 dark:text-white',
    price: 'relative font-semibold text-xl dark:text-white',
    previousPrice: 'relative line-through font-bold text-neutral-500 ml-3',
    percentOff: 'relative font-bold text-green-500 ml-3',
    sizeButton:
      'cursor-pointer select-none relative font-semibold rounded-full w-10 h-10 flex items-center justify-center active:opacity-80 dark:text-white peer-checked:text-white',
    buyButton:
      'text-xs sm:text-sm px-4 h-10 rounded-lg shadow-lg uppercase font-semibold tracking-wider text-white active:opacity-80',
    addToBagButton:
      'text-xs sm:text-sm px-4 h-10 rounded-lg uppercase font-semibold tracking-wider border-2 active:opacity-80'
  },
  variants: {
    color: {
      primary: {
        buyButton: 'bg-blue-500 shadow-blue-500/50',
        sizeButton: 'peer-checked:bg-blue',
        addToBagButton: 'text-blue-500 border-blue-500'
      },
      secondary: {
        buyButton: 'bg-purple-500 shadow-purple-500/50',
        sizeButton: 'peer-checked:bg-purple',
        addToBagButton: 'text-purple-500 border-purple-500'
      },
      success: {
        buyButton: 'bg-green-500 shadow-green-500/50',
        sizeButton: 'peer-checked:bg-green',
        addToBagButton: 'text-green-500 border-green-500'
      }
    }
  }
});
 
const itemSizes = ['xs', 's', 'm', 'l', 'xl'];
 
const App = () => {
  const [size, setSize] = useState('xs');
  const [color, setColor] = useState('primary');
 
  const {
    base,
    imageWrapper,
    img,
    title,
    price,
    previousPrice,
    percentOff,
    sizeButton,
    buyButton,
    addToBagButton
  } = item({ color });
 
  return (
    <div>
      <div className={base()}>
        <div className={imageWrapper()}>
          <img alt="" className={img()} loading="lazy" src="/shoes-1.png" />
        </div>
        <div className="flex-auto pl-4 sm:pl-8">
          <div className="relative flex flex-wrap items-baseline">
            <h1 className={title()}>Nike Adapt BB 2.0</h1>
            <div className={price()}>$279.97</div>
            <div className={previousPrice()}>$350</div>
            <div className={percentOff()}>20% off</div>
          </div>
          <div className="my-4 flex items-baseline">
            <div className="flex space-x-3 text-sm font-medium">
              {itemSizes.map((itemSize) => (
                <label key={itemSize}>
                  <input
                    checked={size === itemSize}
                    className="peer sr-only"
                    name="size"
                    type="radio"
                    value={itemSize}
                    onChange={() => setSize(itemSize)}
                  />
                  <div className={sizeButton()}>{itemSize.toUpperCase()}</div>
                </label>
              ))}
            </div>
          </div>
          <div className="flex space-x-4">
            <button className={buyButton()}>Buy now</button>
            <button className={addToBagButton()}>Add to bag</button>
          </div>
        </div>
      </div>
      <RadioGroup label="Select color:" value={color} onChange={setColor}>
        <Radio value="primary">Primary</Radio>
        <Radio value="secondary">Secondary</Radio>
        <Radio value="success">Success</Radio>
      </RadioGroup>
    </div>
  );
};
 
export default App;

带复合变体的插槽

像变体一样,在使用复合变体时,您也可以为每个插槽定义样式。插槽可以作为对象传递给 classclassName

import { tv } from 'tailwind-variants';
 
const alert = tv({
  slots: {
    root: 'rounded py-3 px-5 mb-4',
    title: 'font-bold mb-1',
    message: ''
  },
  variants: {
    variant: {
      outlined: {
        root: 'border'
      },
      filled: {
        root: ''
      }
    },
    severity: {
      error: '',
      success: ''
    }
  },
  compoundVariants: [
    {
      variant: 'outlined',
      severity: 'error',
      class: {
        root: 'border-red-700 dark:border-red-500',
        title: 'text-red-700 dark:text-red-500',
        message: 'text-red-600 dark:text-red-500'
      }
    },
    {
      variant: 'outlined',
      severity: 'success',
      class: {
        root: 'border-green-700 dark:border-green-500',
        title: 'text-green-700 dark:text-green-500',
        message: 'text-green-600 dark:text-green-500'
      }
    },
    {
      variant: 'filled',
      severity: 'error',
      class: {
        root: 'bg-red-100 dark:bg-red-800',
        title: 'text-red-900 dark:text-red-50',
        message: 'text-red-700 dark:text-red-200'
      }
    },
    {
      variant: 'filled',
      severity: 'success',
      class: {
        root: 'bg-green-100 dark:bg-green-800',
        title: 'text-green-900 dark:text-green-50',
        message: 'text-green-700 dark:text-green-200'
      }
    }
  ],
  defaultVariants: {
    variant: 'filled',
    severity: 'success'
  }
});
 
const { root, message, title } = alert({ severity, variant });
 
return (
  <div className={root()}>
    <div className={title()}>Oops, something went wrong</div>
    <div className={message()}>
      Something went wrong saving your changes. Try again later.
    </div>
  </div>
);

复合插槽

复合插槽允许您一次性对多个插槽应用类。这避免了在多个插槽中重复相同的类。

import { tv } from 'tailwind-variants';
 
const pagination = tv({
  slots: {
    base: 'flex flex-wrap relative gap-1 max-w-fit',
    item: 'data-[active="true"]:bg-blue-500 data-[active="true"]:text-white',
    prev: '',
    next: ''
  },
  variants: {
    size: {
      xs: {},
      sm: {},
      md: {}
    }
  },
  defaultVariants: {
    size: 'md'
  },
  compoundSlots: [
    // 如果您不指定任何变体,它将始终被应用
    {
      slots: ['item', 'prev', 'next'],
      class: [
        'flex',
        'flex-wrap',
        'truncate',
        'box-border',
        'outline-none',
        'items-center',
        'justify-center',
        'bg-neutral-100',
        'hover:bg-neutral-200',
        'active:bg-neutral-300',
        'text-neutral-500'
      ] // --> 这些类将应用于所有插槽
    },
    // 如果您指定一个变体,它将只在变体处于活动状态时应用
    {
      slots: ['item', 'prev', 'next'],
      size: 'xs',
      class: 'w-7 h-7 text-xs' // --> 如果 size 为 xs,这些类将应用于所有插槽
    },
    {
      slots: ['item', 'prev', 'next'],
      size: 'sm',
      class: 'w-8 h-8 text-sm' // --> 如果 size 为 sm,这些类将应用于所有插槽
    },
    {
      slots: ['item', 'prev', 'next'],
      size: 'md',
      class: 'w-9 h-9 text-base' // --> 如果 size 为 md,这些类将应用于所有插槽
    }
  ]
});
 
// React 实现
 
const App = () => {
  const { base, item, prev, next } = pagination();
 
  return (
    <ul aria-label="pagination navigation" className={base()}>
      <li
        aria-label="Go to previous page"
        className={prev()}
        data-disabled="true"
        role="button"
      >
        {'<'}
      </li>
      <li aria-label="page 1" className={item()} role="button">
        1
      </li>
      <li aria-label="page 2" className={item()} role="button">
        2
      </li>
      <li
        aria-label="page 3"
        className={item()}
        data-active="true"
        role="button"
      >
        3
      </li>
      <li aria-label="page 4" className={item()} role="button">
        4
      </li>
      <li aria-label="page 5" className={item()} role="button">
        5
      </li>
      <li aria-hidden="true" className={item()} role="button">
        ...
      </li>
      <li aria-label="page 10" className={item()} role="button">
        10
      </li>
      <li aria-label="Go to next page" className={next()} role="button">
        {'>'}
      </li>
    </ul>
  );
};
 
export default App;

插槽变体覆盖

一些组件库使用类渲染 props 来允许根据内部状态自定义组件的类名。Tailwind Variants 支持插槽级别的变体覆盖,以使其能够简单地根据需要覆盖每个插槽中选择的变体。

import { tv } from 'tailwind-variants';
 
const card = tv({
  slots: {
    base: 'flex gap-2',
    tab: 'rounded'
  },
  variants: {
    color: {
      primary: {
        tab: 'text-blue-500 dark:text-blue-400'
      },
      secondary: {
        tab: 'text-purple-500 dark:text-purple-400'
      }
    },
    isSelected: {
      true: {
        tab: 'font-bold'
      }
    }
  }
});
 
const { base, tab } = card({ color: 'primary' });
 
return (
  <Tabs className={base()}>
    {items.map((item) => (
      <Tab className={({ isSelected }) => tab({ isSelected })} id={item.id}>
        {item.label}
      </Tab>
    ))}
  </Tabs>
);