Skip to content
Lenso UI

Button

A clickable button component with multiple variants and states

Usage

import { Button } from '@lenso/ui';
"use client";
import { Button } from "@lenso/ui";
export function Basic() {  return <Button onClick={() => console.log("Button pressed")}>Click me</Button>;}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Examples

Variants

"use client";
import { Button } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ row: { display: "flex", flexWrap: "wrap", gap: 12 } });export function Variants() {  return (    <div {...stylex.props(styles.row)}>      <Button variant="primary">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>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Sizes

"use client";
import { Button } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ row: { display: "flex", alignItems: "center", gap: 12 } });export function Sizes() {  return (    <div {...stylex.props(styles.row)}>      <Button size="sm">Small</Button>      <Button size="md">Medium</Button>      <Button size="lg">Large</Button>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Icons

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Envelope, Globe, Plus, TrashBin } from "@gravity-ui/icons";import { Button } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";export function WithIcons() {  return (    <div {...stylex.props(styles.row)}>      <Button>        <Button.Icon>          <Globe />        </Button.Icon>        Search      </Button>      <Button variant="secondary">        <Button.Icon>          <Plus />        </Button.Icon>        Add Member      </Button>      <Button variant="tertiary">        <Button.Icon>          <Envelope />        </Button.Icon>        Email      </Button>      <Button variant="danger">        <Button.Icon>          <TrashBin />        </Button.Icon>        Delete      </Button>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Icon Only

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Ellipsis, Gear, TrashBin } from "@gravity-ui/icons";import { Button } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";export function IconOnly() {  return (    <div {...stylex.props(styles.row)}>      <Button isIconOnly aria-label="More options" variant="tertiary">        <Button.Icon>          <Ellipsis />        </Button.Icon>      </Button>      <Button isIconOnly aria-label="Settings" variant="secondary">        <Button.Icon>          <Gear />        </Button.Icon>      </Button>      <Button isIconOnly aria-label="Delete" variant="danger">        <Button.Icon>          <TrashBin />        </Button.Icon>      </Button>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Loading

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Button, Spinner } from "@lenso/ui";export function Loading() {  return (    <Button isLoading>      <Spinner color="current" size="sm" />      Uploading...    </Button>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Loading State

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Paperclip } from "@gravity-ui/icons";import { Button, Spinner } from "@lenso/ui";import { useEffect, useRef, useState } from "react";export function LoadingState() {  const [isLoading, setLoading] = useState(false);  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);  useEffect(() => () => clearTimeout(timer.current), []);  return (    <Button      isLoading={isLoading}      onClick={() => {        setLoading(true);        timer.current = setTimeout(() => setLoading(false), 2000);      }}    >      {isLoading ? (        <Spinner color="current" size="sm" />      ) : (        <Button.Icon>          <Paperclip />        </Button.Icon>      )}      {isLoading ? "Uploading..." : "Upload File"}    </Button>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Full Width

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Plus } from "@gravity-ui/icons";import { Button } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";export function FullWidth() {  return (    <div {...stylex.props(styles.full)}>      <Button fullWidth>Primary Button</Button>      <Button fullWidth>        <Button.Icon>          <Plus />        </Button.Icon>        With Icon      </Button>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Disabled State

"use client";
import { Button } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ row: { display: "flex", flexWrap: "wrap", gap: 12 } });export function Disabled() {  return (    <div {...stylex.props(styles.row)}>      <Button disabled>Primary</Button>      <Button disabled variant="secondary">        Secondary      </Button>      <Button disabled variant="tertiary">        Tertiary      </Button>      <Button disabled variant="outline">        Outline      </Button>      <Button disabled variant="ghost">        Ghost      </Button>      <Button disabled variant="danger">        Danger      </Button>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Social Buttons

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Button } from "@lenso/ui";import { Icon } from "@iconify/react";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";export function Social() {  return (    <div {...stylex.props(styles.social)}>      <Button variant="tertiary" fullWidth>        <Button.Icon>          <Icon icon="devicon:google" />        </Button.Icon>        Sign in with Google      </Button>      <Button variant="tertiary" fullWidth>        <Button.Icon>          <Icon icon="mdi:github" />        </Button.Icon>        Sign in with GitHub      </Button>      <Button variant="tertiary" fullWidth>        <Button.Icon>          <Icon icon="ion:logo-apple" />        </Button.Icon>        Sign in with Apple      </Button>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Render Function

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Button } from "@lenso/ui";import { useState } from "react";export function RenderFunction() {  const [pressed, setPressed] = useState(false);  return (    <Button      onPointerDown={() => setPressed(true)}      onPointerUp={() => setPressed(false)}      onPointerCancel={() => setPressed(false)}      onPointerLeave={() => setPressed(false)}      onKeyDown={(event) => {        if (event.key === " " || event.key === "Enter") setPressed(true);      }}      onKeyUp={() => setPressed(false)}      onBlur={() => setPressed(false)}      render={(props) => <button {...props} data-custom={pressed ? "pressed" : "bar"} />}    >      Press me    </Button>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Adding custom variants

You can extend HeroUI components by wrapping them and adding your own custom variants.

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0. Variant composition uses StyleX, not tailwind-variants.import { Button, type ButtonRootProps } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "./source.stylex";const radii = stylex.create({  full: { borderRadius: 9999 },  lg: { borderRadius: 8 },  md: { borderRadius: 6 },  sm: { borderRadius: 2 },});const sizes = stylex.create({  sm: { height: 40, paddingInline: 16 },  md: { height: 44, paddingInline: 24 },  lg: { height: 48, paddingInline: 32 },  xl: { height: 52, paddingInline: 40 },});type CustomButtonProps = Omit<ButtonRootProps, "size"> & {  size?: keyof typeof sizes;  radius?: keyof typeof radii;};function CustomButton({ radius = "full", size = "md", xstyle, ...props }: CustomButtonProps) {  return <Button {...props} xstyle={[styles.custom, radii[radius], sizes[size], xstyle]} />;}export function CustomVariants() {  return <CustomButton>Custom Button</CustomButton>;}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Adding Ripple Effect

The Button component supports ripple effects through composition, allowing you to nest ripple components as children. This example uses m3-ripple.

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Button } from "@lenso/ui";import { Ripple } from "m3-ripple";import "m3-ripple/ripple.css";export function RippleEffect() {  return (    <Button variant="secondary">      <Ripple />      Click me    </Button>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Customization

Tailwind CSS

"use client";// Adapted from HeroUI v3.2.6 (e385ac2), Apache-2.0.import { Button } from "@lenso/ui";import { styles } from "./source.stylex";export function CustomStyles() {  return (    <Button variant="ghost" xstyle={styles.upgrade}>      Upgrade    </Button>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

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;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

The Button component uses these CSS classes (View source styles):

Base & Size Classes [!toc]

  • .button - Base button styles
  • .button--sm - Small size variant
  • .button--md - Medium size variant
  • .button--lg - Large size variant

Variant Classes [!toc]

  • .button--primary
  • .button--secondary
  • .button--tertiary
  • .button--outline
  • .button--ghost
  • .button--danger

Modifier Classes [!toc]

  • .button--icon-only
  • .button--icon-only.button--sm
  • .button--icon-only.button--lg

Interactive States

The button supports both CSS pseudo-classes and data attributes for flexibility:

  • Hover: :hover or [data-hovered="true"]
  • Active/Pressed: :active or [data-pressed="true"] (includes scale transform)
  • Focus: :focus-visible or [data-focus-visible="true"] (shows focus ring)
  • Disabled: :disabled or [aria-disabled="true"] (reduced opacity, no pointer events)
  • Pending: [data-pending] (no pointer events during loading)

API Reference

Button

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost' | 'danger''primary'Visual style variant
size'sm' | 'md' | 'lg''md'Size of the button
fullWidthbooleanfalseWhether the button should take full width of its container
isDisabledbooleanfalseWhether the button is disabled
isPendingbooleanfalseWhether the button is in a loading state
isIconOnlybooleanfalseWhether the button contains only an icon
onPress(e: PressEvent) => void-Handler called when the button is pressed
childrenReact.ReactNode | (values: ButtonRenderProps) => React.ReactNode-Button content or render prop
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, ButtonRenderProps>-Overrides the default DOM element with a custom render function.

Render Props

When using the render prop pattern, these values are provided:

PropTypeDescription
isPendingbooleanWhether the button is in a loading state
isPressedbooleanWhether the button is currently pressed
isHoveredbooleanWhether the button is hovered
isFocusedbooleanWhether the button is focused
isFocusVisiblebooleanWhether the button should show focus indicator
isDisabledbooleanWhether the button is disabled

See upstream Button showcases. Product showcases are not part of the local component runtime.