04 / Controls
Shift Button
A call to action that trades its leading icon for a directional cue when someone approaches it.
"use client"
import { Apple } from "lucide-react"
import { ShiftButton } from "mischief-ui/shift-button"
export function ShiftButtonDemo() {
return (
<ShiftButton leadingIcon={<Apple aria-hidden="true" />}>
Download for Mac
</ShiftButton>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/shift-buttonimport { ShiftButton } from "mischief-ui/shift-button"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Button } from "@base-ui/react/button"import { ArrowRight } from "lucide-react"import { cn } from "@/lib/utils" export interface ShiftButtonProps extends Omit< React.ComponentPropsWithoutRef<typeof Button>, "children"> { children: React.ReactNode leadingIcon: React.ReactNode trailingIcon?: React.ReactNodeUsage
export function DownloadButton() {
return (
<ShiftButton
render={<a href="/download" />}
leadingIcon={<Apple aria-hidden="true" />}
>
Download for Mac
</ShiftButton>
)
}The shift
On hover the leading icon slides out to the left and fades, while the trailing icon slides in from the right to take its place. The grid keeps a fixed column for each, so the label never moves and the button never changes width -- the motion happens inside a stable shape.
Under reduced motion the trailing icon is not shown at all and the leading icon stays exactly where it is. The button is then simply a button with an icon, which is the point: the shift is decoration, and nothing is communicated by it alone.
Give trailingIcon only when it says something -- an arrow for navigation, a check for a completed action. Leaving it out is fine, and the leading icon then stays put for everyone.
What it needs installed
Base UI supplies the button, which is why this component is imported from its own entry rather than the package root, and why @base-ui/react has to be installed alongside.
npm install mischief-ui @base-ui/reactAPI
childrenReactNodeThe button or link label.leadingIconReactNodeThe icon visible at rest.trailingIconReactNodeThe arriving icon. Defaults to an arrow.renderReactElementRenders another element, such as a link.classNamestringClasses for the root element.Accessibility
Base UI preserves native button behavior and supports rendering a real link for navigation. The label never disappears, focus remains visible, and reduced motion keeps both the leading icon and text still.