Log 01

The Slot Approach

What happens when you have a component that renders multiple children, and you want to expose a way to style those to the parent component? Let’s take this CardLink component as example:

export function CardLink({
  href,
  title,
  description,
  className,
}: {
  href: string
  title: React.ReactNode
  description: React.ReactNode
  className?: string
}) {
  return (
    <Card className={cn('hover:bg-accent/50', className)} asChild>
      <Link href={href}>
        <div>
          {title}
          <ArrowRightIcon />
        </div>
        <p>{description}</p>
      </Link>
    </Card>
  )
}

You’d probably expose a titleClassName and descriptionClassName, in addition to the existing className right?

  export function CardLink({
    href,
    title,
    description,
    className,
++  titleClassName,
++  descriptionClassName
  }: {
    href: string
    title: React.ReactNode
    description: React.ReactNode
    className?: string
++  titleClassName?: string
++  descriptionClassName?: string
  }) {
    return (
      <Card
        className={cn("hover:bg-accent/50", className)}
        asChild
      >
        <Link href={href}>
++	      <div className={titleClassName}>
            {title}
            <ArrowRightIcon />
          </div>
++        <p className={descriptionClassName}>{description}</p>
        </Link>
      </Card>
    )
  }

This has been the classic and most straight forward way of doing it, you can find this in almost every codebase. This is not wrong at all, and it’s good for 1 or 2 extra classNames but it’s not clean and makes props grow making interaction with your components messier. There’s a cleaner, css-first approach I’d like to show you

The data-slot approach

You are not in charge of exposing and connecting props to your elements manually no more. You just name your inner elements with a data-slot=[name] which makes them queryable from it’s parent.

  export function CardLink({
    href,
    title,
    description,
    className,
--  titleClassName,
--  descriptionClassName
  }: {
    href: string
    title: React.ReactNode
    description: React.ReactNode
    className?: string
--  titleClassName?: string
--  descriptionClassName?: string
  }) {
    return (
      <Card
        className={cn("hover:bg-accent/50", className)}
        asChild
      >
        <Link href={href}>
--	      <div className={titleClassName}>
++	      <div data-slot="card-title">
            {title}
            <ArrowRightIcon />
          </div>
--        <p className={descriptionClassName}>{description}</p>
++        <p data-slot="card-description">{description}</p>
        </Link>
      </Card>
    )
  }

As long as your root element has a className tied to it, you’ll be able to target the inner slots with the **:data-[] css selector. Let’s set the card-description opacity to 50%:

<CardLink className="**:data-[slot=card-description]:opacity-50" />

This means “target all the card-description slots inside this <CardLink /> scope”

& [data-slot='card-description'] {
  opacity: 50%;
}

Plays nice with composability

If you paid attention you had noticed I didn’t used the shadcn’s <CardTitle /> and <CardDescription />, this is because those components already have it’s data-slot=[name].

function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="card-title"
      className={cn('leading-none font-semibold', className)}
      {...props}
    />
  )
}
 
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="card-description"
      className={cn('text-muted-foreground text-sm', className)}
      {...props}
    />
  )
}
 

This is cool because it means that if you use your existing UI blocks to create new ones (like our CardLink) these data-names will be inherited. So our final component would end up much compact with no feature loss:

export function CardLink({
  href,
  title,
  description,
  className,
}: {
  href: string
  title: React.ReactNode
  description: React.ReactNode
  className?: string
}) {
  return (
    <Card
      className={cn(
        'hover:bg-accent/50',
        className
      )}
      asChild
    >
      <Link href={href}>
        <CardHeader>
          <CardTitle>
            {title}
            <ArrowRightIcon />
          </CardTitle>
          <CardDescription>{description}</CardDescription>
        </CardHeader>
      </Link>
    </Card>
  )
}

Final Result

<CardLink className="**:data-[slot=card-description]:opacity-50" />