V1.0

Base Components

Tag

Efficient tool for categorizing and labeling content within your application.

Tag

Installation

Install the following dependencies:

terminal
npm install @radix-ui/react-slot

Create a tag.tsx file and paste the following code into it.

/components/ui/tag.tsx
// AlignUI Tag v0.0.0
 
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { RiCloseFill } from '@remixicon/react';
 
import { PolymorphicComponentProps } from '@/utils/polymorphic';
import { recursiveCloneChildren } from '@/utils/recursive-clone-children';
import { tv, type VariantProps } from '@/utils/tv';
 
const TAG_ROOT_NAME = 'TagRoot';
const TAG_ICON_NAME = 'TagIcon';
const TAG_DISMISS_BUTTON_NAME = 'TagDismissButton';
const TAG_DISMISS_ICON_NAME = 'TagDismissIcon';
 
export const tagVariants = tv({
  slots: {
    root: [
      'group/tag inline-flex h-6 items-center gap-2 rounded-md px-2 text-label-xs text-text-sub-600',
      'transition duration-200 ease-out',
      'ring-1 ring-inset',
    ],
    icon: [
      // base
      '-mx-1 size-4 shrink-0 text-text-soft-400 transition duration-200 ease-out',
      // hover
      'group-hover/tag:text-text-sub-600',
    ],
    dismissButton: [
      // base
      'group/dismiss-button -ml-1.5 -mr-1 size-4 shrink-0',
      // focus
      'focus:outline-none',
    ],
    dismissIcon: 'size-4 text-text-soft-400 transition duration-200 ease-out',
  },
  variants: {
    variant: {
      stroke: {
        root: [
          // base
          'bg-bg-white-0 ring-stroke-soft-200',
          // hover
          'hover:bg-bg-weak-50 hover:ring-transparent',
          // focus-within
          'focus-within:bg-bg-weak-50 focus-within:ring-transparent',
        ],
        dismissIcon: [
          // hover
          'group-hover/dismiss-button:text-text-sub-600',
          // focus
          'group-focus/dismiss-button:text-text-sub-600',
        ],
      },
      gray: {
        root: [
          // base
          'bg-bg-weak-50 ring-transparent',
          // hover
          'hover:bg-bg-white-0 hover:ring-stroke-soft-200',
        ],
      },
    },
    disabled: {
      true: {
        root: 'pointer-events-none bg-bg-weak-50 text-text-disabled-300 ring-transparent',
        icon: 'text-text-disabled-300 [&:not(.remixicon)]:opacity-[.48]',
        dismissIcon: 'text-text-disabled-300',
      },
    },
  },
  defaultVariants: {
    variant: 'stroke',
  },
});
 
type TagSharedProps = VariantProps<typeof tagVariants>;
 
type TagProps = VariantProps<typeof tagVariants> &
  React.HTMLAttributes<HTMLDivElement> & {
    asChild?: boolean;
  };
 
const TagRoot = React.forwardRef<HTMLDivElement, TagProps>(
  (
    { asChild, children, variant, disabled, className, ...rest },
    forwardedRef,
  ) => {
    const uniqueId = React.useId();
    const Component = asChild ? Slot : 'div';
    const { root } = tagVariants({ variant, disabled });
 
    const sharedProps: TagSharedProps = {
      variant,
      disabled,
    };
 
    const extendedChildren = recursiveCloneChildren(
      children as React.ReactElement[],
      sharedProps,
      [TAG_ICON_NAME, TAG_DISMISS_BUTTON_NAME, TAG_DISMISS_ICON_NAME],
      uniqueId,
      asChild,
    );
 
    return (
      <Component
        ref={forwardedRef}
        className={root({ class: className })}
        aria-disabled={disabled}
        {...rest}
      >
        {extendedChildren}
      </Component>
    );
  },
);
TagRoot.displayName = TAG_ROOT_NAME;
 
function TagIcon<T extends React.ElementType>({
  className,
  variant,
  disabled,
  as,
  ...rest
}: PolymorphicComponentProps<T, TagSharedProps>) {
  const Component = as || 'div';
  const { icon } = tagVariants({ variant, disabled });
 
  return <Component className={icon({ class: className })} {...rest} />;
}
TagIcon.displayName = TAG_ICON_NAME;
 
type TagDismissButtonProps = TagSharedProps &
  React.ButtonHTMLAttributes<HTMLButtonElement> & {
    asChild?: boolean;
  };
 
const TagDismissButton = React.forwardRef<
  HTMLButtonElement,
  TagDismissButtonProps
>(
  (
    { asChild, children, className, variant, disabled, ...rest },
    forwardedRef,
  ) => {
    const Component = asChild ? Slot : 'button';
    const { dismissButton } = tagVariants({ variant, disabled });
 
    return (
      <Component
        ref={forwardedRef}
        className={dismissButton({ class: className })}
        {...rest}
      >
        {children ?? (
          <TagDismissIcon
            variant={variant}
            disabled={disabled}
            as={RiCloseFill}
          />
        )}
      </Component>
    );
  },
);
TagDismissButton.displayName = TAG_DISMISS_BUTTON_NAME;
 
function TagDismissIcon<T extends React.ElementType>({
  className,
  variant,
  disabled,
  as,
  ...rest
}: PolymorphicComponentProps<T, TagSharedProps>) {
  const Component = as || 'div';
  const { dismissIcon } = tagVariants({ variant, disabled });
 
  return <Component className={dismissIcon({ class: className })} {...rest} />;
}
TagDismissIcon.displayName = TAG_DISMISS_ICON_NAME;
 
export {
  TagRoot as Root,
  TagIcon as Icon,
  TagDismissButton as DismissButton,
  TagDismissIcon as DismissIcon,
};

Update the import paths to match your project setup.

Examples

Stroke (Default)

Tag
Customer

Gray

Tag
Customer

Disabled

Tag
Customer
Tag
Customer

With Image

Apex
Figma

With Avatar

James Brown

Dismissable

Tag
Customer
Tag
Customer

API Reference

Tag.Root

This component is based on the <div> element and supports all of its props. And adds:

PropTypeDefault
variant
"stroke"|"gray"
"stroke"
disabled
boolean
asChild
boolean

Tag.Icon

The Tag.Icon component is polymorphic, allowing you to change the underlying HTML element using the as prop.

PropTypeDefault
as
React.ElementType
div

Tag.DismissButton

This component is based on the <button> element and supports all of its props. And adds:

PropTypeDefault
asChild
boolean

Tag.DismissIcon

The Tag.DismissIcon component is polymorphic, allowing you to change the underlying HTML element using the as prop.

PropTypeDefault
as
React.ElementType
div
© 2024 AlignUI Design System. All rights reserved.