Fixing Shadcn Slot Issues with Multiple Children



This content originally appeared on DEV Community and was authored by Weam Adel

Introduction

Ever tried using a shadcn/ui Button as a link while also including icons or other JSX inside? If so, you may have hit a frustrating issue: the styles break, or worse, your icon ends up outside the clickable area.

In this article, you’ll learn why that happens—and how to fix it.

The Basics: Using asChild

The Button component from Shadcn supports an asChild prop, which swaps the rendered button element for something else (like a Link or a tag). Under the hood, it uses @radix-ui/react-slot’s Slot component to forward props and styles to the element.

Here’s a typical Button implementation:

//components/ui/button.tsx

function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
  }) {
  const Comp = asChild ? Slot : "button";

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  );
}

export { Button, buttonVariants };

We can then use our button as a button, link or anything else:

 <Button className="me-3">Button</Button>

 {/* Or */}

 <Button asChild>
   <Link to="/">Link</Link>
 </Button>

Awesome, but what if you want to add an icon (or any other JSX)?

Simple, you can just import your icon and use it inside your element:

  <Button className="me-3">
    <CheckIcon className="me-2 size-5" /> Button
  </Button>

  {/* Or */}

  <Button asChild>
    <Link to="/">
      <CheckIcon className="me-2 size-5" />
      Link
    </Link>
  </Button>

It works perfectly as expected:

The Problem: Multiple Children Inside the Slot

Now let’s say that you want to move your icon (or any other piece of JSX) inside your component for abstraction and to avoid duplication (all your buttons have a check icon 🤷‍♀)

You might update the Button to look like this:

// components/ui/button.tsx

import { Slot } from "@radix-ui/react-slot";
import CheckIcon from "@/assets/icons/check.svg?react";

function Button({
  className,
  variant,
  size,
  asChild = false,
  children,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
  }) {
  const Comp = asChild ? Slot : "button";

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    >
      <>
        <CheckIcon className="me-2 size-5" />
        {children}
      </>
    </Comp>
  );
}

export { Button, buttonVariants };

Looks innocent, right? But when you render it:

 <Button className="me-3">
    Button
 </Button>

 <Button asChild>
   <Link to="/">Link</Link>
 </Button>

For your surprise, the button works fine, but the link 😭

When you open the DevTools, you will realize that your icon is outside the a element and the styles are not applied at all:

Even wrapping the contents in a div doesn’t help:

<Comp
  data-slot="button"
  className={cn(buttonVariants({ variant, size, className }))}
  {...props}
>
  <div>
    <CheckIcon className="me-2 size-5" />
    {children}
  </div>
</Comp>

Now the structure is worse: styles break, and only the text is clickable.

The Solution: Use Slottable

Despite there can be other workarounds to make this work, but the straight forward solution is to use a Radix Component called Slottable.

When your component (that uses Slot) has multiple children, Slottable ensures that the props are passed to the right component.

Here’s the updated Button component:

import { Slot, Slottable } from "@radix-ui/react-slot";
import CheckIcon from "@/assets/icons/check.svg?react";

function Button({
  className,
  variant,
  size,
  asChild = false,
  children,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
  }) {
  const Comp = asChild ? Slot : "button";

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    >
      <CheckIcon className="me-2 size-5" />
      <Slottable>{children}</Slottable>
    </Comp>
  );
}

Result

Correctly styled elements…

…and the link is fully clickable as the text & icon are inside the a element 👌


This content originally appeared on DEV Community and was authored by Weam Adel