A clickable button component with multiple variants and states.
Import
import { Button } from "heroui-solid";Usage
import { Button } from "heroui-solid"
export function Basic() {
return <Button onClick={() => console.log("Button pressed")}>Click me</Button>
}Variants
import { Button } from "heroui-solid"
export function Variants() {
return (
<div class="flex flex-wrap gap-3">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
<Button variant="danger-soft">Danger Soft</Button>
</div>
)
}With Icons
import { Envelope, Globe, Plus, TrashBin } from "gravity-icons-solid"
import { Button } from "heroui-solid"
export function WithIcons() {
return (
<div class="flex flex-wrap gap-3">
<Button>
<Globe />
Search
</Button>
<Button variant="secondary">
<Plus />
Add Member
</Button>
<Button variant="tertiary">
<Envelope />
Email
</Button>
<Button variant="danger">
<TrashBin />
Delete
</Button>
</div>
)
}Icon Only
import { Ellipsis, Gear, TrashBin } from "gravity-icons-solid"
import { Button } from "heroui-solid"
export function IconOnly() {
return (
<div class="flex gap-3">
<Button isIconOnly variant="tertiary">
<Ellipsis />
</Button>
<Button isIconOnly variant="secondary">
<Gear />
</Button>
<Button isIconOnly variant="danger">
<TrashBin />
</Button>
</div>
)
}Loading
import { Button, Spinner } from "heroui-solid"
export function Loading() {
return (
<Button isPending>
{(state) => (
<>
{state.isPending && <Spinner color="current" size="sm" />}
Uploading...
</>
)}
</Button>
)
}Loading State
import { Paperclip } from "gravity-icons-solid"
import { Button, Spinner } from "heroui-solid"
import { createSignal } from "solid-js"
export function LoadingState() {
const [isLoading, setLoading] = createSignal(false)
const handleClick = () => {
setLoading(true)
setTimeout(() => setLoading(false), 2000)
}
return (
<Button isPending={isLoading()} onClick={handleClick}>
{(state) => (
<>
{state.isPending ? (
<Spinner color="current" size="sm" />
) : (
<Paperclip />
)}
{state.isPending ? "Uploading..." : "Upload File"}
</>
)}
</Button>
)
}Sizes
import { Button } from "heroui-solid"
export function Sizes() {
return (
<div class="flex items-center gap-3">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
)
}Full Width
import { Plus } from "gravity-icons-solid"
import { Button } from "heroui-solid"
export function FullWidth() {
return (
<div class="w-[400px] space-y-3">
<Button fullWidth>Primary Button</Button>
<Button fullWidth>
<Plus />
With Icon
</Button>
</div>
)
}Disabled State
import { Button } from "heroui-solid"
export function Disabled() {
return (
<div class="flex flex-wrap gap-3">
<Button isDisabled>Primary</Button>
<Button isDisabled variant="secondary">
Secondary
</Button>
<Button isDisabled variant="tertiary">
Tertiary
</Button>
<Button isDisabled variant="outline">
Outline
</Button>
<Button isDisabled variant="ghost">
Ghost
</Button>
<Button isDisabled variant="danger">
Danger
</Button>
</div>
)
}Social Buttons
import { Button } from "heroui-solid"
import GoogleIcon from "~icons/devicon/google"
import AppleIcon from "~icons/ion/logo-apple"
import GitHubIcon from "~icons/mdi/github"
export function Social() {
return (
<div class="flex w-full max-w-xs flex-col gap-3">
<Button class="w-full" variant="tertiary">
<GoogleIcon />
Sign in with Google
</Button>
<Button class="w-full" variant="tertiary">
<GitHubIcon />
Sign in with GitHub
</Button>
<Button class="w-full" variant="tertiary">
<AppleIcon />
Sign in with Apple
</Button>
</div>
)
}Custom Element
HeroUI React overrides the rendered element with a render prop. This port uses
Kobalte's polymorphic as prop instead — for example, an anchor that looks like a
button:
import { Button } from "heroui-solid"
export function CustomElement() {
return (
<Button
as="a"
href="https://heroui.com"
rel="noopener noreferrer"
target="_blank"
variant="outline"
>
Visit HeroUI
</Button>
)
}Related Components
- Popover: Displays content in context with a trigger (not ported yet)
- Tooltip: Contextual information on hover or focus (not ported yet)
- Form: Form validation and submission handling (not ported yet)
Styling
Passing Tailwind CSS classes
import { Button } from "heroui-solid";
function CustomButton() {
return (
<Button class="bg-purple-500 text-white hover:bg-purple-600">
Purple Button
</Button>
);
}Customizing the component classes
To customize the Button component classes, you can use the @layer components directive.
Learn more.
@layer components {
.button {
@apply bg-purple-500 text-white hover:bg-purple-600;
}
.button--icon-only {
@apply rounded-lg bg-blue-500;
}
}HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
Adding custom variants
You can extend HeroUI components by wrapping them and adding your own custom variants.
import type { ButtonProps } from "heroui-solid"
import { Button, buttonVariants } from "heroui-solid"
import { splitProps } from "solid-js"
import type { VariantProps } from "tailwind-variants"
import { tv } from "tailwind-variants"
const myButtonVariants = tv({
base: "text-md font-semibold shadow-md text-shadow-lg data-[pending=true]:opacity-40",
defaultVariants: {
radius: "full",
variant: "primary"
},
extend: buttonVariants,
variants: {
radius: {
full: "rounded-full",
lg: "rounded-lg",
md: "rounded-md",
sm: "rounded-sm"
},
size: {
lg: "h-12 px-8",
md: "h-11 px-6",
sm: "h-10 px-4",
xl: "h-13 px-10"
},
variant: {
primary:
"text-white dark:bg-white/10 dark:text-white dark:hover:bg-white/15"
}
}
})
type MyButtonVariants = VariantProps<typeof myButtonVariants>
export type MyButtonProps = Omit<ButtonProps, "class"> &
MyButtonVariants & { class?: string }
function CustomButton(props: MyButtonProps) {
const [local, rest] = splitProps(props, ["class", "radius", "variant"])
return (
<Button
class={myButtonVariants({
class: local.class,
radius: local.radius,
variant: local.variant
})}
{...rest}
/>
)
}
export function CustomVariants() {
return <CustomButton>Custom Button</CustomButton>
}Adding Ripple Effect
HeroUI React adds a ripple through composition — nesting a React-only
<Ripple /> child (e.g. m3-ripple).
This Solid port ships the effect as a CSS flag instead: the --button-ripple
custom property, off by default. Add the ripple class to enable it on a single
button:
import { Button } from "heroui-solid"
export function Ripple() {
return (
<Button class="ripple" variant="secondary">
Click me
</Button>
)
}Because the property inherits, you can enable ripple for every button by
setting it once at the root, then opt individual buttons back out with
no-ripple:
:root {
--button-ripple: 1;
}The ripple expands from the press point and follows Material 3's touch
heuristics: mouse and pen ripple immediately on press, keyboard activation
ripples from the center, and a touch only ripples once it's clearly a tap or
hold — a touch that turns into a scroll never fires it. It uses currentColor
and honors prefers-reduced-motion; tune it with --button-ripple-color and
--button-ripple-opacity.
CSS Classes
The Button component uses these CSS classes (View source styles):
Base & Size Classes
.button- Base button styles.button--sm- Small size variant.button--md- Medium size variant.button--lg- Large size variant
Variant Classes
.button--primary.button--secondary.button--tertiary.button--outline.button--ghost.button--danger.button--danger-soft
Modifier Classes
.button--icon-only.button--icon-only.button--sm.button--icon-only.button--lg
Interactive States
The button supports CSS pseudo-classes and data attributes:
- Hover:
:hover - Active/Pressed:
:active(includes scale transform) - Focus:
:focus-visible(shows focus ring) - Disabled:
:disabledor[aria-disabled="true"](reduced opacity, no pointer events) - Pending:
[data-pending](no pointer events during loading)
API Reference
Button Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "primary" | "secondary" | "tertiary" | "outline" | "ghost" | "danger" | "danger-soft" | "primary" | Visual style variant |
size | "sm" | "md" | "lg" | "md" | Size of the button |
fullWidth | boolean | false | Whether the button should take full width of its container |
isDisabled | boolean | false | Whether the button is disabled |
isPending | boolean | false | Whether the button is in a loading state |
isIconOnly | boolean | false | Whether the button contains only an icon |
onClick | JSX.EventHandlerUnion<HTMLElement, MouseEvent> | - | Handler called when the button is clicked |
children | JSX.Element | (state: { isPending: boolean }) => JSX.Element | - | Button content or render prop |
as | ValidComponent | "button" | Overrides the default DOM element (Kobalte polymorphic as) |
Differences from HeroUI React
- Use
onClick(Solid's native event) instead of React Aria'sonPress. - Polymorphism uses Kobalte's
asprop instead of React'srenderprop override. - The render-prop
stateexposesisPendingonly (noisPressed/isHovered/isFocused) — read fields offstateinside JSX rather than destructuring, so updates stay reactive. - Hover/press styling relies on HeroUI's native
:hover/:activeCSS fallbacks rather than React Aria'sdata-hovered/data-pressedattributes — visuals are identical.
Last updated: 7/19/26, 3:27 AM