Stepwise UI

Button

Squircle button in three sizes and five variants. Supports left and right icons, a slide-in hover animation for right-side icons, icon-only mode, full-width layout, and a loading state. For provider sign-in buttons see Social Button, which is built on this. Works with both iconsax-react and @hugeicons/react.

Installation

npx stepwise-ui add button

Variants

import { Button } from '@/components/stepwise/button'

<Button variant="solid">solid</Button>
<Button variant="outline">outline</Button>
<Button variant="ghost">ghost</Button>
<Button variant="soft">soft</Button>
<Button variant="destructive">destructive</Button>

With icons

Pass any icon element to icon and control placement with iconPosition.
import { HugeiconsIcon } from '@hugeicons/react'
import { PlusSignIcon, ArrowRight02Icon, Delete02Icon } from '@hugeicons/core-free-icons'

// icon on the left (default)
<Button icon={<HugeiconsIcon icon={PlusSignIcon} size={16} strokeWidth={2} color="currentColor" />}>
  New item
</Button>

// icon on the right
<Button
  variant="outline"
  icon={<HugeiconsIcon icon={ArrowRight02Icon} size={16} strokeWidth={2} color="currentColor" />}
  iconPosition="right"
>
  Continue
</Button>

// destructive with icon
<Button
  variant="destructive"
  icon={<HugeiconsIcon icon={Delete02Icon} size={16} strokeWidth={2} color="currentColor" />}
>
  Delete
</Button>

Slide icon

slideIcon hides the icon on desktop until hover, sliding in from whichever side iconPosition names. On touch (pointer: coarse) the icon is always visible.

hover to reveal icon

import { HugeiconsIcon } from '@hugeicons/react'
import { ArrowRight02Icon } from '@hugeicons/core-free-icons'

// icon hides on desktop until hover - always visible on touch
<Button
  icon={<HugeiconsIcon icon={ArrowRight02Icon} size={16} strokeWidth={2} color="currentColor" />}
  iconPosition="right"
  slideIcon
>
  Get started
</Button>

Icon only

iconOnly renders a square button (width = height). Always add aria-label. The same four concepts (add, mail, settings, download) are shown side-by-side from both libraries so you can compare the icon styles at a glance.
iconsax-react
@hugeicons/react
import { Add } from 'iconsax-react'
import { HugeiconsIcon } from '@hugeicons/react'
import { PlusSignIcon } from '@hugeicons/core-free-icons'

// iconsax-react - always add aria-label
<Button
  iconOnly
  icon={<Add size={16} variant="Linear" color="currentColor" />}
  aria-label="Add"
/>

// @hugeicons/react - same slot, different import
<Button
  iconOnly
  icon={<HugeiconsIcon icon={PlusSignIcon} size={16} strokeWidth={1.5} color="currentColor" />}
  aria-label="Add"
/>

Full width

fullWidth stretches the button to 100% of its parent container. Useful for forms, cards, and mobile layouts.
// stretches to 100% of the parent container
<Button fullWidth variant="solid">
  Create account
</Button>

<Button
  fullWidth
  variant="outline"
  icon={<ArrowRight size={16} variant="Linear" color="currentColor" />}
  iconPosition="right"
>
  Continue
</Button>

<Button fullWidth variant="ghost">
  Sign in instead
</Button>

Disabled

The disabled prop applies 45% opacity, flattens the elevation, and blocks all pointer events. Works across every variant and size.
// disabled prop applies 45% opacity and blocks all interaction
<Button disabled variant="solid">solid</Button>
<Button disabled variant="outline">outline</Button>
<Button disabled variant="ghost">ghost</Button>
<Button disabled variant="soft">soft</Button>
<Button disabled variant="destructive">destructive</Button>

// works on icon-only too
<Button disabled iconOnly icon={<Add size={16} variant="Linear" color="currentColor" />} aria-label="Add" />

Loading

The label always stays put. With an icon the spinner takes the icon's slot; without one it centres over a faded label, so the button never changes width mid-request and its accessible name survives. Sets aria-busy.
// with an icon - the spinner takes the icon's slot, label never moves
<Button loading icon={<ImportSquare />}>Saving</Button>

// without one - the spinner centres over a faded label, width stays put
<Button loading>Submit</Button>
<Button loading iconOnly icon={<Add />} aria-label="Adding" />

Custom fill

Defaults stay on the design tokens. Pass Tailwind on className to restyle one instance. bg-sky-500 is promoted to both gradient stops so a single colour still covers the fill. from-* / to-* replace the fade. Pair a fill change with text-* so the label contrast holds.
// one colour → both gradient stops (flat fill)
<Button className="bg-sky-500 text-white">flat sky</Button>

// two stops → keep the top→bottom fade
<Button className="from-sky-400 to-sky-700 text-white">sky gradient</Button>

// no fill class → only the text token is replaced
<Button variant="outline" className="text-sky-700">text only</Button>

Sizes

Three sizes with fully-rounded corners (radius = height ÷ 2) and squircle smoothing at 0.6.
<Button size="sm">small</Button>
<Button size="default">default</Button>
<Button size="lg">large</Button>

Props

PropTypeDescription
variant
"solid" | "outline" | "ghost" | "soft" | "destructive"
Visual style. Default: "solid".
size
"sm" | "default" | "lg"
28 / 36 / 40 px. Default: "default" (36 px).
icon
React.ReactNode
Icon element - iconsax-react or @hugeicons/react.
iconPosition
"left" | "right"
Side the icon renders on. Default: "left".
iconOnly
boolean
Square icon-only button. Requires aria-label.
slideIcon
boolean
Icon hides until hover, from either side. Always visible on touch.
fullWidth
boolean
Stretches button to 100% of the parent container.
loading
boolean
Spinner + aria-busy, disables the button. Label and width stay put.
disabled
boolean
45% opacity, flattens elevation, blocks interaction.
noRipple
boolean
Disables the press ripple. Skipped automatically under prefers-reduced-motion.
href
string
Renders an <a> instead of a <button>, styled identically. Ignored while disabled or loading. target="_blank" gets rel="noopener noreferrer" automatically.
className
string
Merged last via cn(). bg-* becomes both gradient stops; from-* / to-* replace the fade; text-* / h-* / px-* override the matching defaults.