Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Pro

Liquid Button for React

A button that fills from the bottom on hover, its label changing color right at the waterline. For React with Base UI, when a submit or a call to action should feel like it's filling up.

Framework
UI library

When the pointer arrives or the button takes keyboard focus, a fill rises from the bottom edge like water in a glass, and drains when it leaves. The label above the waterline keeps its color and the part below it turns to the fill's text color, so it reads all the way through.

Use it on a form's submit or a primary call to action that sits on a quiet page; for a fill that slides in from the side of a pill, use the Pill Button. It starts as an outline button so the empty state is a frame, and it wraps the Button, so every variant and size works. The fill is a second copy of the label on a colored layer, uncovered by a rising clip-path in a CSS transition, with no animation library and no JavaScript. With reduced motion the fill fades in and out instead of rising.

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 { LiquidButton } from "@/components/ui/liquid-button";
<LiquidButton>Subscribe</LiquidButton>

Demos

Duration

duration is how long the fill takes to rise, and to drain, in milliseconds; the default is 500. A short one suits a small button that's hovered often, a long one a large call to action that can take its time.

Preview width

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 send button fills with the foreground color and a delete button with the destructive one. Icons in the label change color with the text.

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 fills the button as hover does, so people who tab see the same cue.

Enter
Activate the button.
Space
Activate the button.

FAQ

How does the fill work?

The button holds a colored layer with its own copy of the label, clipped to nothing. On hover a clip-path uncovers it from the bottom up, so the fill and the label's new color move as one edge.

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.

How do I make the fill rise slower?

Set duration in milliseconds; 500 is the default. The Duration demo shows 250 and 1200.

Why does it start as an outline button?

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

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 rise or drain: it fades in over the whole button on hover and focus, and fades out when they end, in the same duration.

API Reference

LiquidButton

data-slot="liquid-button-fill"

PropType
children?ReactNode

The label, drawn again on the fill in fillForeground.

duration?number

How long the fill takes to rise, and to drain, in milliseconds.

Default
500
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

Forwarded to the part.

CSS variableType
--liquid-button-durationstring

The rise, and the drain (500ms).

--liquid-button-fillstring

The fill's color.

--liquid-button-fill-foregroundstring

The label's color on the fill.

All 35 buttons