Button

Clickable element that triggers an action.

import { Button } from 'ui-lab-components/button';
 
export function Example() {
  return (
    <Button variant="primary" size="md">
      Get started
    </Button>
  );
}

button/00-button-group

An editor mode switcher — labeled Group buttons with active state for moving between write, split, and preview.

Loading interactive preview
"use client";
 
import { useState } from 'react'
import { Button } from 'ui-lab-components/button'
import { Divider } from 'ui-lab-components/divider'
import { Group } from 'ui-lab-components/group'
import { Switch } from 'ui-lab-components/switch'
import { FaCode, FaAt, FaEye, FaImage, FaLink, FaLocationDot, FaPlus, FaPen } from 'react-icons/fa6'
 
export default function Example() {
  const [mode, setMode] = useState("write");
  const [enabled, setEnabled] = useState(false);
 
  return (
    <div className="flex w-130 flex-col gap-4">
      <div className="flex items-center space-x-12 justify-between">
        <div className="h-4 w-32 rounded bg-background-700/50" aria-hidden="true" />
        <div className="space-x-2.5">
          <Group variant="secondary" orientation="horizontal" spacing="none">
            <Group.Button active={mode === "write"} onPress={() => setMode("write")} >
              Write
            </Group.Button>
            <Divider orientation="vertical" />
            <Group.Button active={mode === "preview"} onPress={() => setMode("preview")}>
              Markup
            </Group.Button>
          </Group>
        </div>
      </div>
 
      <div className="flex flex-col gap-2.5 rounded-sm p-3">
        <div className='grid grid-cols-[auto_1fr] gap-2.5'>
          <div className="h-10 w-10 rounded-full border-3 border-background-950 bg-background-800" />
          <div className='space-y-2'>
            <div className="h-3 w-full rounded bg-background-700/40" aria-hidden="true" />
            <div className="h-3 w-5/6 rounded bg-background-700/40" aria-hidden="true" />
            <div className="h-3 w-4/6 rounded bg-background-700/40" aria-hidden="true" />
          </div>
        </div>
        <div className="mt-8 flex items-center justify-between">
          <div className="flex items-center gap-2">
            <Switch aria-label='Draft' size="sm" state={{ checked: enabled }} onChange={setEnabled} />
            <div className={`h-3 w-20 rounded bg-background-700 transition-opacity ${!enabled ? 'opacity-60' : 'opacity-100'}`} aria-hidden="true" />
          </div>
          <div className="flex items-center gap-1">
            <Button aria-label="Mention someone" size="sm" variant="ghost" icon={{ left: <FaAt /> }} />
            <Button aria-label="Add image" size="sm" variant="ghost" icon={{ left: <FaImage /> }} />
            <Button aria-label="Add link" size="sm" variant="ghost" icon={{ left: <FaLink /> }} />
            <Button aria-label="Add location" size="sm" variant="ghost" icon={{ left: <FaLocationDot /> }} />
            <Divider orientation="vertical" className="mx-1" />
            <Button size="sm" variant="secondary">Save draft</Button>
            <Button aria-label="Create draft" variant="secondary" icon={{ left: <FaPlus /> }} />
          </div>
        </div>
      </div>
    </div>
  )
}
 

button/01-button-hero

A compact hero call-to-action — two buttons paired with muted placeholder avatar and text shapes.

Loading interactive preview
import { Button } from 'ui-lab-components/button'
import { FaArrowRight, FaPlay } from 'react-icons/fa6'
 
export default function Example() {
  return (
    <section className="flex w-115 flex-col gap-7 rounded-sm p-5">
      <div className="flex items-center gap-3" aria-hidden="true">
        <div className="flex -space-x-2">
          <div className="h-7 w-7 rounded-full border-2 border-background-950 bg-background-700/60" />
          <div className="h-7 w-7 rounded-full border-2 border-background-950 bg-background-700/45" />
          <div className="h-7 w-7 rounded-full border-2 border-background-950 bg-background-700/30" />
        </div>
        <div className="h-2.5 w-24 rounded bg-background-700/40" />
      </div>
 
      <div className="flex flex-col gap-3" aria-hidden="true">
        <div className="h-5 w-4/5 rounded bg-background-700/55" />
        <div className="h-5 w-3/5 rounded bg-background-700/55" />
        <div className="mt-1 h-2.5 w-full rounded bg-background-700/30" />
        <div className="h-2.5 w-5/6 rounded bg-background-700/30" />
      </div>
 
      <div className="flex items-center gap-2.5">
        <Button icon={{ right: <FaArrowRight /> }}>Get started</Button>
        <Button variant="secondary" icon={{ left: <FaPlay /> }}>Watch demo</Button>
      </div>
    </section>
  )
}
 

button/03-inline-delete-confirmation

A restrained destructive action — the delete button confirms intent in place, communicates progress, and ends with a recoverable undo state.

Loading interactive preview
"use client";
 
import { useEffect, useState } from 'react'
import { Button } from 'ui-lab-components/button'
import { Flex } from 'ui-lab-components/flex'
import { FaArrowRotateLeft, FaSpinner, FaTrashCan } from 'react-icons/fa6'
 
type DeleteState = 'idle' | 'confirming' | 'deleting' | 'deleted'
 
export default function Example() {
  const [state, setState] = useState<DeleteState>('idle')
 
  useEffect(() => {
    if (state !== 'deleting') return
 
    const timer = window.setTimeout(() => setState('deleted'), 900)
    return () => window.clearTimeout(timer)
  }, [state])
 
  return (
    <Flex
      direction="column"
      gap="lg"
      className="w-120 rounded-sm p-5"
    >
      <Flex align-center gap="md" aria-hidden="true">
        <div className="h-10 w-10 rounded-sm bg-background-700/50" />
        <Flex direction="column" gap="xs">
          <div className="h-3 w-28 rounded bg-background-700/55" />
          <div className="h-2.5 w-18 rounded bg-background-700/30" />
        </Flex>
      </Flex>
 
      <div className="h-px bg-background-700/40" aria-hidden="true" />
 
      <Flex align-center justify-between gap="xl" >
        <Flex direction="column" gap="sm" aria-hidden="true">
          <div className="h-3 w-24 rounded bg-background-700/50" />
          <div className="h-2.5 w-48 rounded bg-background-700/30" />
          <div className="h-2.5 w-36 rounded bg-background-700/30" />
        </Flex>
 
        <Flex className="w-fit" align-center gap="sm" aria-live="polite">
          {state === 'idle' && (
            <Button
              variant="danger"
              icon={{ left: <FaTrashCan /> }}
              onPress={() => setState('confirming')}
            >
              Delete project
            </Button>
          )}
 
          {state === 'confirming' && (
            <>
              <Button variant="ghost" onPress={() => setState('idle')}>
                Cancel
              </Button>
              <Button
                variant="danger"
                icon={{ left: <FaTrashCan /> }}
                onPress={() => setState('deleting')}
              >
                Yes, delete
              </Button>
            </>
          )}
 
          {state === 'deleting' && (
            <Button
              variant="danger"
              icon={{ left: <FaSpinner className="animate-spin" /> }}
              isDisabled
            >
              Deleting…
            </Button>
          )}
 
          {state === 'deleted' && (
            <Button
              variant="secondary"
              icon={{ left: <FaArrowRotateLeft /> }}
              onPress={() => setState('idle')}
            >
              Undo
            </Button>
          )}
        </Flex>
      </Flex>
    </Flex>
  )
}
 

button/04-media-player

A music player card — transport controls and progress bar set against skeleton track art and metadata, with a menu for queue actions.

Loading interactive preview
"use client";
 
import React, { useState } from 'react'
import { Button } from 'ui-lab-components/button'
import { Divider } from 'ui-lab-components/divider'
import { Group } from 'ui-lab-components/group'
import { Menu } from 'ui-lab-components/menu'
import { Progress } from 'ui-lab-components/progress'
import { Slider } from 'ui-lab-components/slider'
import {
  FaBackwardStep, FaForwardStep,
  FaPause, FaPlay, FaRepeat, FaShuffle,
  FaVolumeHigh, FaVolumeLow, FaVolumeOff, FaVolumeXmark,
  FaListUl, FaShareNodes, FaHeartCrack, FaHeadphones,
  FaUpRightAndDownLeftFromCenter,
  FaPlus,
  FaEllipsis,
} from 'react-icons/fa6'
 
export default function Example() {
  const [playing, setPlaying] = useState(false);
  const [shuffle, setShuffle] = useState(false);
  const [repeat, setRepeat] = useState(false);
  const [muted, setMuted] = useState(false);
  const [volume, setVolume] = useState(70);
  const [liked, setLiked] = useState(false);
  const [queue, setQueue] = useState(false);
  const [progress, setProgress] = useState(37);
 
  const volumeIcon = muted ? <FaVolumeXmark /> : volume === 0 ? <FaVolumeOff /> : volume < 50 ? <FaVolumeLow /> : <FaVolumeHigh />;
 
  return (
    <div className="flex w-100 flex-col gap-5 rounded-sm p-2">
      <div className="flex items-center gap-3.5">
        <div className="h-14 w-14 flex-shrink-0 rounded-sm bg-background-700/50" aria-hidden="true" />
        <div className="flex flex-1 flex-col gap-2">
          <div className="h-3 w-36 rounded bg-background-700/50" aria-hidden="true" />
          <div className="h-2.5 w-24 rounded bg-background-700/30" aria-hidden="true" />
        </div>
        <div className='flex space-x-1.5'>
          <Button
            aria-label={liked ? 'Unsave' : 'Save'}
            styles={{ root: liked ? "opacity-30" : "opacity-100" }}
            onPress={() => setLiked(!liked)}
            variant='ghost'
            size="sm"
            icon={<FaPlus />}
          />
          <Menu type="pop-over">
            <Menu.Trigger>
              <Button variant='ghost' icon={{ left: <FaEllipsis /> }} aria-label="Track options">
              </Button>
            </Menu.Trigger>
            <Menu.Content side="bottom" align="end" offset={6}>
              <Menu.Item><FaHeadphones className="mr-2 opacity-60" />Go to artist</Menu.Item>
              <Menu.Item><FaListUl className="mr-2 opacity-60" />Add to playlist</Menu.Item>
              <Menu.Item><FaShareNodes className="mr-2 opacity-60" />Share track</Menu.Item>
              <Menu.Separator />
              <Menu.Item><FaHeartCrack className="mr-2 opacity-60" />Remove from queue</Menu.Item>
            </Menu.Content>
          </Menu>
        </div>
      </div>
 
      <div className="flex items-center mb-4 gap-3">
        <div className="h-2.5 w-7 rounded bg-background-700/30" aria-hidden="true" />
        <Progress show-controls value={progress} onValueChange={setProgress} styles={{ root: 'flex-1', track: 'h-3.5' }} />
        <div className="h-2.5 w-7 rounded bg-background-700/30" aria-hidden="true" />
      </div>
 
      <div className="flex items-center justify-between">
        <Button
          size="sm"
          variant="ghost"
          aria-label={muted ? 'Unmute' : 'Mute'}
          icon={{ left: volumeIcon }}
          styles={{ root: muted ? 'opacity-40' : '' }}
          onPress={() => setMuted(!muted)}
        />
        <div>
          <Slider
            value={volume}
            onValueChange={([v]) => setVolume(v)}
            disabled={muted}
            aria-label="Volume"
            style={{ '--inline-size': '4rem' } as React.CSSProperties}
          />
        </div>
        <div className="flex items-center mx-auto gap-2.5">
          <Button
            size="sm"
            variant="ghost"
            aria-label="Shuffle"
            icon={{ left: <FaShuffle /> }}
            styles={{ root: shuffle ? 'text-foreground-400' : 'opacity-30' }}
            onPress={() => setShuffle(!shuffle)}
          />
          <Group variant='default' orientation="horizontal" spacing="xs">
            <Group.Button size="icon" aria-label="Previous">
              <FaBackwardStep />
            </Group.Button>
            <Divider orientation="vertical" />
            <Group.Button
              size="icon"
              aria-label={playing ? 'Pause' : 'Play'}
              onPress={() => setPlaying(!playing)}
            >
              {playing ? <FaPause /> : <FaPlay />}
            </Group.Button>
            <Divider orientation="vertical" />
            <Group.Button size="icon" aria-label="Next">
              <FaForwardStep />
            </Group.Button>
          </Group>
          <Button
            size="sm"
            variant="ghost"
            aria-label="Repeat"
            icon={{ left: <FaRepeat /> }}
            styles={{ root: repeat ? 'text-foreground-400' : 'opacity-30' }}
            onPress={() => setRepeat(!repeat)}
          />
        </div>
        <div className="flex ml-auto items-center gap-2">
          <Button
            size="sm"
            variant="ghost"
            aria-label="Queue"
            icon={{ left: <FaListUl /> }}
            styles={{ root: queue ? 'text-foreground-400' : 'opacity-30' }}
            onPress={() => setQueue(!queue)}
          />
          <Button
            size="sm"
            variant="ghost"
            aria-label="Expand"
            icon={{ left: <FaUpRightAndDownLeftFromCenter /> }}
            styles={{ root: 'opacity-30' }}
          />
        </div>
      </div>
 
    </div>
  )
}
 

button/04-split-button

A merge split button — the primary action commits to the selected strategy while alternates with descriptions wait behind the menu.

Loading interactive preview
"use client";
 
import { useState } from 'react'
import { Button } from 'ui-lab-components/button'
import { Divider } from 'ui-lab-components/divider'
import { Flex } from 'ui-lab-components/flex'
import { Group } from 'ui-lab-components/group'
import { Select } from 'ui-lab-components/select'
import {
  FaChevronDown, FaCodeMerge, FaCodeCommit, FaLayerGroup,
  FaCircleCheck, FaComment, FaCodeBranch, FaLink,
} from 'react-icons/fa6'
 
const mergeStrategies = [
  {
    value: "merge",
    label: "Merge",
    menuLabel: "Create a merge commit",
    description: "All commits are preserved in history",
    icon: <FaCodeMerge />,
  },
  {
    value: "squash",
    label: "Squash and merge",
    menuLabel: "Squash and merge",
    description: "Combine all commits into one",
    icon: <FaLayerGroup />,
  },
  {
    value: "rebase",
    label: "Rebase and merge",
    menuLabel: "Rebase and merge",
    description: "Replay commits onto the base branch",
    icon: <FaCodeCommit />,
  },
] as const;
 
export default function Example() {
  const [strategy, setStrategy] = useState<string | number | null>(mergeStrategies[0].value);
 
  const selectedStrategy = mergeStrategies.find((item) => item.value === strategy) ?? mergeStrategies[0];
 
  return (
    <div className="flex w-110 flex-col gap-4 rounded-sm p-4">
      <div className="flex items-center gap-3">
        <div className="h-9 w-9 flex-shrink-0 rounded-full border-3 border-background-950 bg-background-800" aria-hidden="true" />
        <div className="flex flex-1 flex-col gap-2">
          <div className="h-3 w-32 rounded bg-background-700/50" aria-hidden="true" />
          <div className="h-2.5 w-20 rounded bg-background-700/30" aria-hidden="true" />
        </div>
      </div>
 
      <div className="flex flex-col gap-2" aria-hidden="true">
        <div className="h-2.5 w-full rounded bg-background-700/30" />
        <div className="h-2.5 w-4/5 rounded bg-background-700/30" />
      </div>
 
      <div className="flex items-center gap-2">
        <FaCircleCheck className="h-3.5 w-3.5 text-foreground-400" aria-hidden="true" />
        <div className="h-2.5 w-36 rounded bg-background-700/30" aria-hidden="true" />
      </div>
 
      <div className="h-px bg-background-700/40" aria-hidden="true" />
 
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-1">
          <Button size="sm" variant="ghost" icon={{ left: <FaComment /> }} />
          <Button size="sm" variant="ghost" icon={{ left: <FaCodeBranch /> }} />
          <Button size="sm" variant="ghost" icon={{ left: <FaLink /> }} />
        </div>
 
        <Flex className="w-fit" align-center gap="md">
          <Group orientation="horizontal">
            <Group.Select selectedKey={strategy} onSelectionChange={setStrategy}>
              <Select.Value>
                {() => (
                  <Button variant='ghost' icon={{ left: selectedStrategy.icon }}>
                    {selectedStrategy.label}
                  </Button>
                )}
              </Select.Value>
              <Divider />
              <Select.Trigger
                chevron={<FaChevronDown className="h-3.5 w-3.5" />}
                aria-label="Choose merge strategy"
              />
              <Select.Content>
                <Select.List>
                  {mergeStrategies.map((item) => (
                    <Select.Item
                      key={item.value}
                      value={item.value}
                      textValue={item.menuLabel}
                      icon={item.icon}
                      description={item.description}
                    >
                      {item.menuLabel}
                    </Select.Item>
                  ))}
                </Select.List>
              </Select.Content>
            </Group.Select>
          </Group>
        </Flex>
      </div>
    </div>
  )
}
 

button/05-viewport-tools

A 3D viewport toolbar — icon-only Group toggle for transform tools beside a render action with a shortcut hint.

Loading interactive preview
"use client";
 
import { useEffect, useState } from 'react'
import { Button } from 'ui-lab-components/button'
import { Divider } from 'ui-lab-components/divider'
import { Group } from 'ui-lab-components/group'
import {
  FaArrowPointer,
  FaArrowsUpDownLeftRight,
  FaCircleCheck,
  FaCube,
  FaExpand,
  FaRotate,
  FaSpinner,
  FaWandMagicSparkles,
} from 'react-icons/fa6'
import { RiCommandFill } from 'react-icons/ri';
 
const tools = [
  { id: 'select', label: 'Select', icon: <FaArrowPointer /> },
  { id: 'move', label: 'Move', icon: <FaArrowsUpDownLeftRight /> },
  { id: 'rotate', label: 'Rotate', icon: <FaRotate /> },
  { id: 'scale', label: 'Scale', icon: <FaExpand /> },
] as const
 
export default function Example() {
  const [activeTool, setActiveTool] = useState<(typeof tools)[number]['id']>('select')
  const [renderState, setRenderState] = useState<'idle' | 'rendering' | 'rendered'>('idle')
 
  useEffect(() => {
    if (renderState !== 'rendering') return
 
    const timer = window.setTimeout(() => setRenderState('rendered'), 1200)
    return () => window.clearTimeout(timer)
  }, [renderState])
 
  const renderAction = {
    idle: {
      label: 'Render',
      icon: <FaWandMagicSparkles />,
      isDisabled: false,
      onPress: () => setRenderState('rendering'),
    },
    rendering: {
      label: 'Rendering…',
      icon: <FaSpinner className="animate-spin" />,
      isDisabled: true,
    },
    rendered: {
      label: 'Rendered',
      icon: <FaCircleCheck className="text-success-500" />,
      isDisabled: false,
      onPress: () => setRenderState('idle'),
    },
  }[renderState]
 
  return (
    <div className="flex w-115 flex-col gap-3 rounded-sm p-3">
      <div className="relative h-48 overflow-hidden rounded-sm border border-background-700/40 bg-background-900/60">
        <div className="absolute inset-0 opacity-30 [background-image:linear-gradient(to_right,var(--background-700)_1px,transparent_1px),linear-gradient(to_bottom,var(--background-700)_1px,transparent_1px)] [background-size:28px_28px]" aria-hidden="true" />
        <div className="absolute left-3 top-3 flex items-center gap-2" aria-hidden="true">
          <FaCube className="h-3.5 w-3.5 text-foreground-400" />
          <div className="h-2.5 w-18 rounded bg-background-700/55" />
        </div>
      </div>
 
      <div className="flex items-center justify-between">
        <Group variant="secondary" orientation="horizontal" spacing="none">
          {tools.map((tool, index) => (
            <div key={tool.id} className="flex items-center">
              {index > 0 && <Divider orientation="vertical" />}
              <Group.Button
                size="icon"
                active={activeTool === tool.id}
                aria-label={tool.label}
                onPress={() => setActiveTool(tool.id)}
              >
                {tool.icon}
              </Group.Button>
            </div>
          ))}
        </Group>
 
        <Button
          variant="secondary"
          icon={{ left: renderAction.icon }}
          hint={renderState === 'idle' ? [<RiCommandFill />, "K"] : undefined}
          isDisabled={renderAction.isDisabled}
          onPress={renderAction.onPress}
        >
          {renderAction.label}
        </Button>
      </div>
    </div>
  )
}
 
UI Lab