Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Buttons

Pro

Underline Button for React

A text button whose underline grows from the center on hover, for navigation and quiet calls to action. For React with Base UI, when a link-like action needs a clear hover.

Framework
UI library

The button is just its label until the pointer arrives or it takes keyboard focus; then a line grows under the text, from the middle or from either side, and shrinks back when it leaves. On the current page's link it stays drawn.

Use it for navigation, footer links, page menus, or a secondary action next to a filled one. It starts as a ghost button with the hover background turned off, and it wraps the Button, so variant and size work as usual. The line is a CSS transition on its scale, with no animation library and no JavaScript; with reduced motion it's drawn full width and fades in instead of growing.

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 { UnderlineButton } from "@/components/ui/underline-button";
<UnderlineButton>Read the case study</UnderlineButton>

Demos

Origin

origin sets where the line grows from: center, the default, start or end. Start and end follow the text direction, so in a right-to-left layout the start is on the right.

Preview width

Thickness and Color

thickness is the line's height in pixels, 2 by default, and underlineColor its color, any CSS color, the primary color by default. Here a hairline in the foreground color suits a footer link, and a thick primary line with a slower duration suits a large call to action.

Preview width

Current Page

The line stays drawn on a button with aria-current="page" (or "true"), so a menu marks where people are with no extra prop. Screen readers announce it as the current page too.

Preview width

Accessibility

The underline is decoration and hidden from assistive tech; the label is the button's name. It grows on keyboard focus as well as hover, and the button keeps its usual focus ring. aria-current both draws the line and tells screen readers which item is current.

Enter
Activate the button.
Space
Activate the button.

FAQ

How do I make the underline thicker?

Set thickness in pixels; 2 is the default. Use 1 for a hairline or 3 to 4 for a bolder line. The Thickness and Color demo shows 1 and 4.

How do I change the underline's color?

Pass underlineColor, any CSS color, such as var(--foreground). The default is the primary color, and the label keeps the button's text color.

Can the line grow from one side?

Yes. origin="start" grows it from where the text starts and origin="end" from where it ends, as in the Origin demo.

How do I keep the underline on the current page's link?

Pass aria-current="page". The line stays drawn while it's set, as in the Current Page demo.

How wide is the underline?

As wide as the label, icons included, not the whole button. It sits just under the text.

What happens with reduced motion?

The underline doesn't grow: it fades in at full width on hover and focus, and stays drawn on the current page's link.

API Reference

UnderlineButton

data-slot="underline-button-line"

PropType
children?ReactNode

The label; the line is as wide as it is.

duration?number

How long the underline takes to grow, in milliseconds.

Default
300
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
origin?"center" | "start" | "end"

Where the underline grows from: the middle of the label, or the side the text starts or ends on (the start is the right in a right-to-left layout).

Type
"center" | "start" | "end"
Default
"center"
thickness?number

How thick the underline is, in pixels.

Default
2
underlineColor?string

The underline's color, any CSS color.

Default
"var(--primary)"
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
--underline-button-colorstring

The line's color.

--underline-button-durationstring

How long the line takes to grow (300ms).

--underline-button-thicknessstring

The line's thickness (2px).

All 35 buttons