Button

( Action / Form / Interactive )

A soft-canvas button with slowly floating blurred blobs behind a crisp white border.

Live preview and source code for Button

An animated call-to-action button with four blurred blobs drifting slowly behind a white canvas and a crisp 1px border. The label, the icon and the colours are all props, so there are no children to pass and nothing to style before it looks finished. It is a motion.button, so it keeps every native button behaviour: focus, disabled, form submission and event handlers.

Installation

Copy the source from the Code tab above into your project as Button.tsx. The styles are inline and the spinner keyframe travels with the file, so there is no stylesheet to import and no Tailwind config to touch.

It imports framer-motion for the drifting blobs and the press animation, and lucide-react for the default icon and the loading spinner:

Terminal
npm install framer-motion lucide-react

The file is marked "use client", which it needs: the animation runs in the browser. In an app router project it can still be imported from a server component.

Usage

Every prop is optional, and the label is a prop rather than a child:

Example.tsx
import Button from "./components/Button";

export default function Example() {
    return <Button label="Start building" size="lg" blobColor="#c4b5fd" />;
}

When to reach for it

The primary action in a hero or a pricing card, where one button is meant to draw the eye and there is room for it to breathe. The soft blob background is built for a light, low-contrast page rather than a dense toolbar.

A single accented action in an otherwise quiet layout: "Start building", "Upgrade", "Generate". Set blobColor to the accent you already use and the button picks up the page palette without any other change.

An async action that needs feedback. loading swaps the icon for a spinner and disables the button in one prop, so there is no separate disabled state to manage.

When not to

Not in a dense interface. Every instance runs four infinite blob animations, so a toolbar or a table with a button per row pays for motion nobody is looking at. A plain button is the right call there.

Not for navigation. This renders a <button>. A control that changes the URL should be an <a> so it can be opened in a new tab, and no prop here changes the element.

Not when the label is long or structured. The label is a single string with white-space: nowrap, so it will push the button wider rather than wrap. Text plus a badge, or two lines of copy, needs a different control.

Accessibility

Focus, activation by Enter and Space, and removal from the tab order when disabled all come from the native <button> underneath. loading sets both the disabled attribute and aria-busy, but the label does not change, so if the wait is long, announce the result from a live region rather than relying on the spinner.

The motion is not gated on prefers-reduced-motion. The blobs drift on a loop for as long as the button is mounted, and the hover and tap scaling are always on. If you support a reduced-motion setting, read it in the copied file and drop the animate values, since there is no prop that turns the animation off.

The label colour is fixed at #3f3f46. canvasColor changes the background underneath it, so a dark canvas will fail contrast until you change the text colour in the source too.

Props

Every prop the Button component accepts, with its type and default value.

Props accepted by the Button component
PropTypeDefaultDescription
labelstringButtonText label rendered inside the button.
iconLucideIcon | nullSparkleslucide-react icon component. Pass icon={null} to hide it.
iconPosition"left" | "right"leftWhere the icon sits relative to the label.
size"sm" | "md" | "lg"lgVisual size of the button.
loadingbooleanfalseShows a spinner and disables interaction.
fullWidthbooleanfalseStretches the button to fill its container.
blobColorstring#7dd3fcColour used for all the floating blurred blobs on the canvas.
canvasColorstring#ffffffBase canvas colour the blobs float on top of.

Notes

The component renders a <style> block defining a global spin keyframe. If your app already defines spin, one will win over the other.

Props spread onto the button after type="button", so passing type="submit" works and makes it submit the surrounding form.

style is applied after the component's own inline styles, so anything you pass through it wins, including borderRadius and boxShadow.

icon={null} removes the icon entirely. Leaving it unset gives you the Sparkles default, which is rarely what a real call to action wants.