Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Pro

Pill Button for React

A fully rounded button that a fill slides into from the side on hover, its label changing color right at the fill's curved edge. For React with Base UI, when a call to action should feel solid and inviting.

Framework
UI library

The Pill Button is round at both ends and starts as an outline. When the pointer arrives or the button takes keyboard focus, a fill slides in from the side the text starts on, its leading edge curved like the pill itself, and slides back out when it leaves. The label turns to the fill's text color exactly where the fill passes, and a soft inset highlight along the fill's bottom edge makes it read as a curved surface.

Use it for the action a page is built around: Get started, Subscribe, Book a demo. For a fill that rises from the bottom of a square button, use the Liquid Button. It wraps the Button, so every variant and size works, with wider padding at each size so the ends have room. The fill is a second copy of the label on a colored layer, uncovered by a rounded clip-path in a CSS transition, with no JavaScript and no animation library. With reduced motion the fill fades in and out instead of sliding.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Usage

import { PillButton } from "@/components/ui/pill-button";
<PillButton size="lg">Get started</PillButton>

Demos

Color

fillColor is the fill and fillForeground the label's color where the fill covers it, each any CSS color; by default they're the primary color and its foreground. Set them together so the covered label stays readable. Here a demo booking fills with the foreground color, and a Cancel plan button with the destructive one, so it never wears the brand color.

Preview width

Duration

duration is how long the fill takes to slide across, and back, in milliseconds; the default is 450. A quick slide suits a small button that's hovered often, a slower one a large hero button. The fill covers any variant at rest, such as secondary here.

Preview width

Accessibility

The button is announced and activated as any Button. The fill's copy of the label is hidden from assistive tech, so the label is read once. Keyboard focus slides the fill in as hover does, so people who tab through the page see the same cue.

Enter
Activate the button.
Space
Activate the button.

FAQ

How does the fill slide in?

The button holds a colored layer with its own copy of the label, clipped to nothing by a clip-path with round corners. On hover the clip opens from the start side to the end, so the fill grows with a curved leading edge, and the label's new color moves with it.

How do I change the fill color?

Pass fillColor, and fillForeground for the label on it, such as var(--foreground) and var(--background). The Color demo shows two.

Why does it start as an outline?

So the empty pill reads as a frame waiting to fill. Pass any other variant to change the look at rest; the fill covers it the same way.

Why is it wider than my other buttons?

It adds padding at the ends at every size, because a fully round end eats into the label's room. Pass px-* in className to match a neighbor exactly.

Which side does the fill come from in a right-to-left layout?

The right, where the text starts. In a left-to-right layout it comes from the left.

Why is my label rendered twice?

The second copy is the one inside the fill, and it's hidden from assistive tech. Keep the label to text and icons: anything with its own state would run twice.

What happens with reduced motion?

The fill doesn't slide: it fades in over the whole pill on hover and focus, and fades out when they end, in the same duration.

API Reference

PillButton

data-slot="pill-button-fill"

PropType
children?ReactNode

The label, drawn again on the fill in fillForeground.

duration?number

How long the fill takes to slide across, and back, in milliseconds.

Default
450
fillColor?string

The fill's color, any CSS color.

Default
"var(--primary)"
fillForeground?string

The label's color where the fill covers it, any CSS color: pick one that reads on fillColor.

Default
"var(--primary-foreground)"
focusableWhenDisabled?boolean

Whether the button should be focusable when disabled.

Default
false
nativeButton?boolean

Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).

Default
true
render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Merged with the part's classes.

CSS variableType
--pill-button-durationstring

The slide across, and back (450ms).

--pill-button-fillstring

The fill's color.

--pill-button-fill-foregroundstring

The label's color on the fill.

All 35 buttons