npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rezafab/fab-button

v1.6.1

Published

Styled by default. Flexible by design.

Readme

FabButton

Styled by default. Flexible by design.

Documentation: https://documentation-fab-button.vercel.app/

FabButton is a section-based button builder for modern and legacy frontends. It gives you sensible default styling while keeping CSS override simple through CSS variables and data attributes.

Why FabButton

FabButton gives you one section-based API for grouped actions while keeping styling, keyboard behavior, and framework portability consistent.

Latest in v1.6.0

  • Floating button mode with 3x3 screen positioning (floatingPosition)
  • Automatic collapsible floating action menu
  • Attached section panels via per-section panel
  • Per-section panel customization (panelTitle, panelClassName, panelStyle, panelAriaLabel)
  • Built-in section confirmation flow (confirm: true | { title, description })
  • Per-section async feedback states (idle, loading, success, error)
  • Promise-aware async handling with manual asyncState override support
  • asyncFeedbackDuration for per-section auto reset timing
  • Automatic shortcut hint UI from shortcut / shortcutId
  • Responsive overflow mode (More menu) for compact screens
  • Role/permission guard helpers (visibleWhen, disabledWhen)
  • Split-button preset (actionPreset="split")
  • Action analytics hook (onSectionAction) with source metadata

Core Strengths

  • Section-based action composition in one component (left, center, right, or custom keys)
  • Keyboard accessibility (tab/toolbar navigation + built-in shortcut matching)
  • Styling flexibility via CSS variables, data attributes, and runtime CSS mode (manual/library)
  • Native theme support (light, dark, system)
  • Framework portability: React, Vue, Svelte, and Web Component adapters

Feature Support by Adapter

| Feature | React | Vue | Svelte | Web Component | | --- | --- | --- | --- | --- | | Section confirmation modal (confirm) | Yes | Yes | Yes | Yes | | Per-section async state (manual) | Yes | Yes | Yes | Yes (data-async-state) | | Promise auto async handling | Yes | Yes | Yes | No | | Shortcut hint UI + shortcut metadata | Yes | Yes | Yes | Yes | | Responsive overflow mode (More menu) | Yes | Yes | Yes | No | | Floating 3x3 positioning | Yes | Yes | Yes | Yes | | Floating attached section panels | Yes | Yes | Yes (text/number panel content) | No | | Role/permission guards (visibleWhen / disabledWhen) | Yes | Yes | Yes | No | | Split-button preset (actionPreset="split") | Yes | Yes | Yes | No | | Action analytics hook (onSectionAction) | Yes | Yes | Yes | Yes (section-action event) | | CSS mode config (manual/library) + theme config | Yes | Yes | Yes | Yes |

Packages

  • @rezafab/fab-button-core: core types and class/CSS-variable helpers
  • @rezafab/fab-button-styles: default style.css
  • @rezafab/fab-button-theme-tokens: shared theme tokens (tokens.css + JS token object)
  • @rezafab/fab-button-react: React FabButton component
  • @rezafab/fab-button-vue: Vue FabButton component
  • @rezafab/fab-button-svelte: Svelte FabButton component
  • @rezafab/fab-button-element: Custom Element adapter
  • @rezafab/docs: Storybook documentation app

Installation

Install the adapter package that matches your app stack:

pnpm add @rezafab/fab-button-react
pnpm add @rezafab/fab-button-element
pnpm add @rezafab/fab-button-vue
pnpm add @rezafab/fab-button-svelte

Or install the umbrella package and use subpath imports (@rezafab/fab-button/react, /vue, /svelte, /element):

pnpm add @rezafab/fab-button

CSS Mode (Manual vs Library)

FabButton now supports 2 CSS modes:

  • manual (default): uses FabButton's built-in styles (same as current behavior).
  • library: uses class strategies from a UI library (Tailwind/Bootstrap/custom classes).

The main goal is to make integration configurable through one app-level config file, without changing most component code.

1) Create a config file in your app

Example file: src/fab-button.config.ts

import { configureFabButton, createFabButtonConfig } from "@rezafab/fab-button-react"

configureFabButton(
  createFabButtonConfig({
    cssMode: "manual"
  })
)

For Vue/Svelte/Element, import from each package:

  • @rezafab/fab-button-vue
  • @rezafab/fab-button-svelte
  • @rezafab/fab-button-element

2) Import the config once in your app entry

Example src/main.tsx:

import "./fab-button.config"

Switch to Tailwind mode

import { configureFabButton, createFabButtonConfig } from "@rezafab/fab-button-react"

configureFabButton(
  createFabButtonConfig({
    cssMode: "library",
    library: {
      preset: "tailwind"
    }
  })
)

Switch to Bootstrap mode

import { configureFabButton, createFabButtonConfig } from "@rezafab/fab-button-react"

configureFabButton(
  createFabButtonConfig({
    cssMode: "library",
    library: {
      preset: "bootstrap"
    }
  })
)

Custom CSS library mode

import { configureFabButton, createFabButtonConfig } from "@rezafab/fab-button-react"

configureFabButton(
  createFabButtonConfig({
    cssMode: "library",
    library: {
      preset: "custom",
      classes: {
        root: "rk-btn",
        section: "rk-btn__section",
        actionSection: "rk-btn__section--interactive",
        variant: {
          primary: "rk-btn--primary",
          outline: "rk-btn--outline"
        },
        size: {
          sm: "rk-btn--sm",
          md: "rk-btn--md",
          lg: "rk-btn--lg"
        }
      }
    }
  })
)

When cssMode changes, button styles update automatically to the selected mode (manual/library) without rewriting your FabButton component usage.

Native Theme Support (Light / Dark / System)

FabButton supports native theme selection through the theme prop:

  • light
  • dark
  • system (follows prefers-color-scheme)
<FabButton
  theme="dark"
  sections={[
    { key: "left", content: "Dark" },
    { key: "right", content: "Theme" }
  ]}
/>

You can also set it globally via config:

import { configureFabButton } from "@rezafab/fab-button-react"

configureFabButton({
  theme: "system"
})

React Usage

import { FabButton } from "@rezafab/fab-button-react"

export function Example() {
  return (
    <FabButton
      variant="primary"
      sections={[
        { key: "icon", content: "🚀", ariaLabel: "Launch icon" },
        { key: "label", content: "Launch" }
      ]}
    />
  )
}

Styled FabButton Example

import { FabButton } from "@rezafab/fab-button-react"

export function StyledExample() {
  return (
    <FabButton
      variant="primary"
      size="lg"
      shape="pill"
      className="hero-action"
      style={{
        "--fab-button-bg": "linear-gradient(90deg, #0f172a 0%, #1d4ed8 100%)",
        "--fab-button-border": "1px solid #1e40af",
        "--fab-button-gap": "10px",
        "--fab-button-radius": "999px",
        "--fab-button-height": "52px"
      }}
      sections={[
        { key: "label", content: "Start Career Assessment" },
        { key: "badge", content: "Free" },
        { key: "arrow", content: "→", ariaLabel: "Open assessment" }
      ]}
    />
  )
}

Comparison Cases (Without FabButton vs With FabButton)

Why developers use FabButton:

  • Less repeated markup for grouped actions.
  • Better visual consistency across screens and teams.
  • Easier control of grouped behavior (disabled/loading/keyboard navigation) in one place.
  • Simpler style-system switching per project (manual CSS vs Tailwind/Bootstrap/custom).
  • More consistent API across React, Vue, Svelte, and Web Component usage.

Case 1: Utility Action Group (Copy, Share, Save)

Scenario: dashboard card or editor toolbar with three quick utility actions.

Without FabButton (3 separate buttons)

import { useState } from "react"

export function TraditionalActionGroup() {
  const [lastAction, setLastAction] = useState("None")

  return (
    <div>
      <div className="action-row">
        <button type="button" onClick={() => setLastAction("Copy")}>
          Copy
        </button>
        <button type="button" onClick={() => setLastAction("Share")}>
          Share
        </button>
        <button type="button" onClick={() => setLastAction("Save")}>
          Save
        </button>
      </div>
      <p>Last action: {lastAction}</p>
    </div>
  )
}

With FabButton (1 component, 3 sections)

import { FabButton } from "@rezafab/fab-button-react"
import { useState } from "react"

export function FabButtonActionGroup() {
  const [lastAction, setLastAction] = useState("None")

  return (
    <div>
      <FabButton
        sections={[
          { key: "copy", content: "Copy", onClick: () => setLastAction("Copy") },
          { key: "share", content: "Share", onClick: () => setLastAction("Share") },
          { key: "save", content: "Save", onClick: () => setLastAction("Save") }
        ]}
      />
      <p>Last action: {lastAction}</p>
    </div>
  )
}

Both examples produce the same output (Last action: Copy/Share/Save), but the FabButton version is more compact and consistent.

Case 2: Keyboard Accessibility (Previous, Next, Skip)

Scenario: onboarding/tutorial controls that must work well with keyboard users.

Without FabButton (manual keyboard logic)

import { useRef } from "react"

export function TraditionalKeyboardActions() {
  const refs = [useRef<HTMLButtonElement>(null), useRef<HTMLButtonElement>(null), useRef<HTMLButtonElement>(null)]

  const onKeyDown = (index: number, event: React.KeyboardEvent<HTMLButtonElement>) => {
    if (event.key === "ArrowRight") refs[(index + 1) % refs.length].current?.focus()
    if (event.key === "ArrowLeft") refs[(index - 1 + refs.length) % refs.length].current?.focus()
  }

  return (
    <div role="toolbar" aria-label="Tutorial actions">
      <button ref={refs[0]} onKeyDown={(e) => onKeyDown(0, e)}>Previous</button>
      <button ref={refs[1]} onKeyDown={(e) => onKeyDown(1, e)}>Next</button>
      <button ref={refs[2]} onKeyDown={(e) => onKeyDown(2, e)}>Skip</button>
    </div>
  )
}

With FabButton (built-in toolbar keyboard behavior)

import { FabButton } from "@rezafab/fab-button-react"

export function FabButtonKeyboardActions() {
  return (
    <FabButton
      keyboardNavigation="toolbar"
      keyboardOrientation="horizontal"
      sections={[
        { key: "prev", content: "Previous", onClick: () => {} },
        { key: "next", content: "Next", onClick: () => {} },
        { key: "skip", content: "Skip", onClick: () => {} }
      ]}
    />
  )
}

Both versions provide the same actions, but FabButton includes toolbar-style keyboard navigation without writing custom focus logic.

Case 3: Unified Loading/Disabled State (Save, Submit, Publish)

Scenario: form workflow where all related actions should lock during async requests.

Without FabButton (manual state sync across buttons)

import { useState } from "react"

export function TraditionalAsyncActions() {
  const [loading, setLoading] = useState(false)

  const run = async () => {
    setLoading(true)
    await new Promise((r) => setTimeout(r, 700))
    setLoading(false)
  }

  return (
    <div>
      <button disabled={loading} onClick={run}>Save</button>
      <button disabled={loading} onClick={run}>Submit</button>
      <button disabled={loading} onClick={run}>Publish</button>
      {loading && <p>Processing...</p>}
    </div>
  )
}

With FabButton (single loading/disabled source)

import { FabButton } from "@rezafab/fab-button-react"
import { useState } from "react"

export function FabButtonAsyncActions() {
  const [loading, setLoading] = useState(false)

  const run = async () => {
    setLoading(true)
    await new Promise((r) => setTimeout(r, 700))
    setLoading(false)
  }

  return (
    <FabButton
      loading={loading}
      sections={[
        { key: "save", content: "Save", onClick: run },
        { key: "submit", content: "Submit", onClick: run },
        { key: "publish", content: "Publish", onClick: run }
      ]}
    />
  )
}

Both versions expose the same actions, but FabButton centralizes loading/disabled behavior to reduce state inconsistency bugs.

Role-Based Approval Example (Hide + Disable)

Scenario: approval workflow where each role can only act at the right stage.

  • Hide actions when the current user is not authorized for that role.
  • Disable actions when authorized but the workflow prerequisites are not ready.

Without FabButton (manual hide/disable per button)

import { useState } from "react"

type Role = "staff" | "supervisor" | "manager"

export function TraditionalApprovalActions({ currentRole }: { currentRole: Role }) {
  const [loading, setLoading] = useState(false)
  const [staffApproved, setStaffApproved] = useState(false)
  const [supervisorApproved, setSupervisorApproved] = useState(false)
  const [managerApproved, setManagerApproved] = useState(false)

  const canSeeStaff = currentRole === "staff"
  const canSeeSupervisor = currentRole === "supervisor"
  const canSeeManager = currentRole === "manager"

  const canApproveStaff = !staffApproved
  const canApproveSupervisor = staffApproved && !supervisorApproved
  const canApproveManager = supervisorApproved && !managerApproved

  const run = async (cb: () => void) => {
    setLoading(true)
    await new Promise((r) => setTimeout(r, 600))
    cb()
    setLoading(false)
  }

  return (
    <div>
      {canSeeStaff && (
        <button disabled={loading || !canApproveStaff} onClick={() => run(() => setStaffApproved(true))}>
          Staff Approve
        </button>
      )}
      {canSeeSupervisor && (
        <button
          disabled={loading || !canApproveSupervisor}
          onClick={() => run(() => setSupervisorApproved(true))}
        >
          Supervisor Approve
        </button>
      )}
      {canSeeManager && (
        <button disabled={loading || !canApproveManager} onClick={() => run(() => setManagerApproved(true))}>
          Manager Approve
        </button>
      )}
    </div>
  )
}

With FabButton (single grouped control with role logic)

import { FabButton, disabledWhen, visibleWhen } from "@rezafab/fab-button-react"
import { useState } from "react"

type Role = "staff" | "supervisor" | "manager"

export function FabButtonApprovalActions({ currentRole }: { currentRole: Role }) {
  const [loading, setLoading] = useState(false)
  const [staffApproved, setStaffApproved] = useState(false)
  const [supervisorApproved, setSupervisorApproved] = useState(false)
  const [managerApproved, setManagerApproved] = useState(false)

  const canSeeStaff = currentRole === "staff"
  const canSeeSupervisor = currentRole === "supervisor"
  const canSeeManager = currentRole === "manager"

  const canApproveStaff = !staffApproved
  const canApproveSupervisor = staffApproved && !supervisorApproved
  const canApproveManager = supervisorApproved && !managerApproved

  const run = async (cb: () => void) => {
    setLoading(true)
    await new Promise((r) => setTimeout(r, 600))
    cb()
    setLoading(false)
  }

  const sections = [
    {
      key: "staff",
      content: "Staff Approve",
      ...visibleWhen(canSeeStaff),
      ...disabledWhen(!canApproveStaff),
      onClick: () => run(() => setStaffApproved(true))
    },
    {
      key: "supervisor",
      content: "Supervisor Approve",
      ...visibleWhen(canSeeSupervisor),
      ...disabledWhen(!canApproveSupervisor),
      onClick: () => run(() => setSupervisorApproved(true))
    },
    {
      key: "manager",
      content: "Manager Approve",
      ...visibleWhen(canSeeManager),
      ...disabledWhen(!canApproveManager),
      onClick: () => run(() => setManagerApproved(true))
    }
  ]

  return <FabButton loading={loading} sections={sections} />
}

Both approaches can enforce the same rules. FabButton keeps role-based actions in one control while preserving clear hide/disable logic.

Case 4: Style Strategy Switching (Project-Level Theme Direction)

Scenario: a project switches design direction from built-in CSS to utility classes.

Without FabButton (manual refactor in component usage)

export function TraditionalStyledActions({ useUtilityClasses }: { useUtilityClasses: boolean }) {
  return (
    <div className={useUtilityClasses ? "flex gap-2 rounded-lg border p-1" : "action-row"}>
      <button className={useUtilityClasses ? "px-3 py-2 rounded-md hover:bg-black/10" : "btn"}>
        Copy
      </button>
      <button className={useUtilityClasses ? "px-3 py-2 rounded-md hover:bg-black/10" : "btn"}>
        Share
      </button>
      <button className={useUtilityClasses ? "px-3 py-2 rounded-md hover:bg-black/10" : "btn"}>
        Save
      </button>
    </div>
  )
}

With FabButton (switch mode in one config file)

// src/fab-button.config.ts
import { configureFabButton, createFabButtonConfig } from "@rezafab/fab-button-react"

configureFabButton(
  createFabButtonConfig({
    cssMode: "library",
    library: {
      preset: "tailwind"
    }
  })
)
// component usage stays the same
<FabButton
  sections={[
    { key: "copy", content: "Copy", onClick: () => {} },
    { key: "share", content: "Share", onClick: () => {} },
    { key: "save", content: "Save", onClick: () => {} }
  ]}
/>

Both approaches can reach the same UI direction, but FabButton centralizes style-mode switching at config level.

Case 5: Multi-Framework Product (React + Vue + Svelte)

Scenario: a team ships the same action pattern to multiple frontend stacks.

Without FabButton (separate implementations per framework)

// React: app-specific component + behavior
export function ReactActionGroup() {
  return (
    <div>
      <button>Copy</button>
      <button>Share</button>
      <button>Save</button>
    </div>
  )
}
// Vue: rebuild similar template/behavior
// Svelte: rebuild similar template/behavior
// Web Component: rebuild similar behavior again

With FabButton (same concept across frameworks)

// React
import { FabButton } from "@rezafab/fab-button-react"
// Vue
import { FabButton } from "@rezafab/fab-button-vue"
// Svelte
import { FabButton } from "@rezafab/fab-button-svelte"
// Element
import "@rezafab/fab-button-element"

All adapters share the same section-based mental model, so teams can keep behavior and UX aligned across stacks.

Section Action Usage

If any section has onClick, FabButton uses a non-button group root and renders each section as its own <button>.

<FabButton
  sections={[
    { key: "copy", content: "Copy", onClick: () => console.log("copy") },
    { key: "share", content: "Share", onClick: () => console.log("share") }
  ]}
/>

Built-in Section Confirmation

Use confirm on a section to require user confirmation before onClick runs. Supported in React, Vue, Svelte, and Web Component adapters.

  • confirm: true uses the built-in FabButton confirm modal with default text.
  • confirm: { title, description } uses the same modal with custom message text.
<FabButton
  sections={[
    { key: "archive", content: "Archive", onClick: () => console.log("archive") },
    {
      key: "delete",
      content: "Delete",
      confirm: {
        title: "Delete this item?",
        description: "This action cannot be undone."
      },
      onClick: () => console.log("delete")
    }
  ]}
/>

For Web Component sections, use data attributes:

<fab-button keyboard-navigation="toolbar">
  <button data-section="archive">Archive</button>
  <button
    data-section="delete"
    data-confirm="true"
    data-confirm-title="Delete this item?"
    data-confirm-description="This action cannot be undone."
  >
    Delete
  </button>
</fab-button>

Per-Section Async State

Each section can expose async feedback state:

  • idle
  • loading
  • success
  • error

Two usage modes:

  • Auto mode (React/Vue/Svelte): return a Promise from section onClick, and FabButton will automatically show loading then success/error.
  • Manual mode (all adapters): set asyncState directly ("idle" | "loading" | "success" | "error") from your own app state.
<FabButton
  sections={[
    {
      key: "save",
      content: "Save",
      onClick: async () => {
        await new Promise((resolve) => setTimeout(resolve, 900))
      }
    },
    {
      key: "publish",
      content: "Publish",
      onClick: async () => {
        await new Promise((_, reject) => setTimeout(() => reject(new Error("Failed")), 900))
      }
    },
    {
      key: "sync",
      content: "Sync",
      asyncState: "loading"
    }
  ]}
/>

Optional reset timing for auto mode:

  • asyncFeedbackDuration (milliseconds) on each section.
  • Default auto reset timing is 1600ms when asyncFeedbackDuration is not set.

Web Component manual state can use:

  • data-async-state="idle"
  • data-async-state="loading"
  • data-async-state="success"
  • data-async-state="error"

Layout Examples

Horizontal layout (default)

<FabButton
  layout="flex"
  sections={[
    { key: "copy", content: "Copy", onClick: () => {} },
    { key: "share", content: "Share", onClick: () => {} },
    { key: "save", content: "Save", onClick: () => {} }
  ]}
/>

Vertical layout

<FabButton
  layout="flex"
  style={{ flexDirection: "column", alignItems: "stretch" }}
  sections={[
    { key: "overview", content: "Overview", onClick: () => {} },
    { key: "details", content: "Details", onClick: () => {} },
    { key: "history", content: "History", onClick: () => {} }
  ]}
/>

Grid layout

<FabButton
  layout="grid"
  columns="repeat(2, minmax(84px, 1fr))"
  rows="repeat(2, minmax(42px, auto))"
  sections={[
    { key: "up", content: "Up", onClick: () => {} },
    { key: "left", content: "Left", onClick: () => {} },
    { key: "right", content: "Right", onClick: () => {} },
    { key: "down", content: "Down", onClick: () => {} }
  ]}
/>

Responsive Overflow Mode (More Menu)

Use overflow mode to keep the button compact on small screens. Supported in React, Vue, and Svelte adapters.

<FabButton
  overflowMode="more"
  overflowBreakpoint={768}
  overflowVisibleCount={2}
  overflowMenuLabel="More"
  sections={[
    { key: "copy", content: "Copy", onClick: () => {} },
    { key: "share", content: "Share", onClick: () => {} },
    { key: "save", content: "Save", onClick: () => {} },
    { key: "archive", content: "Archive", onClick: () => {} }
  ]}
/>

Behavior:

  • overflowMode="none": default, all sections rendered inline.
  • overflowMode="more": when viewport width is <= overflowBreakpoint, only the first overflowVisibleCount sections stay inline.
  • Remaining sections move into a built-in More dropdown.

Props:

  • overflowBreakpoint default: 768
  • overflowVisibleCount default: 2
  • overflowMenuLabel default: "More"

Web Component adapter does not provide a built-in overflow menu.

Split Button Preset (Primary + Dropdown)

Use split preset when you want the first action to stay as the primary button and the remaining actions to move into a dropdown. Supported in React, Vue, and Svelte adapters.

<FabButton
  actionPreset="split"
  splitButtonTriggerSide="right"
  sections={[
    { key: "save", content: "Save", onClick: () => {} },
    { key: "publish", content: "Publish", onClick: () => {} },
    { key: "archive", content: "Archive", onClick: () => {} }
  ]}
/>

Behavior:

  • First section is rendered as the primary action.
  • Remaining sections are rendered inside the split dropdown menu trigger (default: down symbol \u25BE).
  • The last action selected from dropdown becomes the next primary action.
  • actionPreset="split" has priority over responsive overflowMode.
  • For multiple split buttons (each with its own dropdown), compose multiple FabButton instances side-by-side.

Props:

  • actionPreset default: "default" ("split" to enable split-button preset)
  • splitButtonMenuLabel default: "\u25BE"
  • splitButtonTriggerSide default: "right" ("left" or "right")

Web Component adapter does not provide a built-in split-button preset.

Floating Button + Attached Panels

Use floating mode when the action group should stay attached to the viewport. In React, Vue, and Svelte, floating mode automatically collapses action sections into a toggle menu.

<FabButton
  floating
  floatingPosition="bottom-right"
  floatingOffset="24px"
  floatingMenuLabel="Actions"
  sections={[
    {
      key: "compose",
      content: "Compose",
      panelTitle: "Compose",
      panel: ({ close }) => (
        <form
          onSubmit={(event) => {
            event.preventDefault()
            close()
          }}
        >
          <input aria-label="Subject" placeholder="Subject" />
          <textarea aria-label="Message" placeholder="Write a message" />
          <button type="submit">Save Draft</button>
        </form>
      )
    },
    {
      key: "upload",
      content: "Upload",
      panelTitle: "Upload",
      panel: <div>Upload queue content can live here.</div>
    },
    { key: "share", content: "Share", onClick: () => {} }
  ]}
/>

Positions:

  • top-left, top-center, top-right
  • center-left, center, center-right
  • bottom-left, bottom-center, bottom-right

Panel customization:

  • panel: attached view content for that section.
  • panelTitle: optional title rendered in the attached panel header.
  • panelClassName: custom class for the attached panel.
  • panelStyle: inline style for that section panel.
  • panelAriaLabel: custom accessible label for the panel.

Web Component adapter supports floating positioning via floating, floating-position, and floating-offset, but does not render the built-in collapsible menu or attached panels.

Custom CSS Override

.custom-action {
  --fab-button-bg: #fff7ed;
  --fab-button-color: #7c2d12;
  --fab-button-border: 1px solid #fdba74;
  --fab-button-radius: 999px;
  --fab-button-gap: 8px;
}

Theme Tokens Package

Use the dedicated tokens package when you want one shared source for colors, spacing, radius, and interaction tokens.

pnpm add @rezafab/fab-button-theme-tokens
import "@rezafab/fab-button-theme-tokens/tokens.css"
import { fabButtonThemeTokens } from "@rezafab/fab-button-theme-tokens"

tokens.css exposes CSS variables that the default FabButton styles consume.

Legacy CSS Integration

FabButton avoids global selectors and relies on local classes (.fab-button, .fab-button__section) plus data attributes, so it can coexist with old CSS stacks with low collision risk.

Unstyled Mode

Use unstyled to skip default classes and fully own the rendering styles from your application.

<FabButton
  unstyled
  className="my-button"
  sections={[
    { key: "left", content: "Plain" },
    { key: "right", content: "Styled by app" }
  ]}
/>

Accessibility and Keyboard

For section-action buttons, you can keep default tab navigation or switch to toolbar-style keyboard navigation.

<FabButton
  keyboardNavigation="toolbar"
  keyboardOrientation="horizontal"
  loopNavigation
  sections={[
    { key: "copy", content: "Copy", onClick: () => {} },
    { key: "share", content: "Share", onClick: () => {} },
    { key: "save", content: "Save", onClick: () => {} }
  ]}
/>
  • keyboardNavigation="tab": each section is in normal tab order (default)
  • keyboardNavigation="toolbar": one tab stop + arrow key navigation (Home/End supported)
  • keyboardOrientation: horizontal, vertical, or both
  • loopNavigation: wrap focus from last to first and vice versa (default true)

Keyboard Shortcut Integration Example

You can map keyboard shortcuts directly in each section without writing custom window listeners.

import { useState } from "react"
import { FabButton } from "@rezafab/fab-button-react"

export function KeyboardShortcutActions() {
  const [lastAction, setLastAction] = useState("None")

  return (
    <div>
      <FabButton
        keyboardNavigation="toolbar"
        sections={[
          { key: "copy", shortcut: "1", content: "Copy", onClick: () => setLastAction("Copy") },
          { key: "share", shortcutId: [16, 95], content: "Share", onClick: () => setLastAction("Share") },
          { key: "save", shortcutId: 17, content: "Save", onClick: () => setLastAction("Save") }
        ]}
      />
      <p>Last action: {lastAction}</p>
    </div>
  )
}

Simple rule:

  • Use shortcut for key-based tokens (for example: "1", "c", "Digit1", "code:Digit1", "key:Enter").
  • Use shortcutId for keyboard-map ID based tokens (for example: 16 or [16, 95]).

Reference:

  • shortcutId: 16 = Digit2 (top number row key 2)
  • shortcutId: 95 = Numpad2

All shortcuts trigger the same section click path, so behavior stays consistent with mouse interaction.

Action Analytics Hook

Use onSectionAction to track action source metadata:

  • click: pointer/touch interaction
  • shortcut: global shortcut trigger (shortcut / shortcutId)
  • keyboard-nav: keyboard activation from section focus (Enter / Space)
import { FabButton } from "@rezafab/fab-button-react"

<FabButton
  keyboardNavigation="toolbar"
  sections={[
    { key: "copy", shortcut: "1", content: "Copy", onClick: () => {} },
    { key: "share", shortcutId: 16, content: "Share", onClick: () => {} },
    { key: "save", content: "Save", onClick: () => {} }
  ]}
  onSectionAction={(meta) => {
    // meta: { key, index, source }
    console.log(meta.key, meta.index, meta.source)
  }}
/>

For Web Component, listen to native section-action event:

<fab-button id="analytics-button" keyboard-navigation="toolbar">
  <button data-section="copy" data-shortcut="1">Copy</button>
  <button data-section="share" data-shortcut-id="16">Share</button>
</fab-button>
<script>
  const button = document.getElementById("analytics-button")
  button?.addEventListener("section-action", (event) => {
    // event.detail = { key, index, source }
    console.log(event.detail)
  })
</script>

Automatic Shortcut Hint UI

Shortcut hints are rendered automatically from shortcut / shortcutId on each section.

<FabButton
  sections={[
    { key: "copy", shortcut: "1", content: "Copy", onClick: () => {} },
    { key: "share", shortcutId: [16, 95], content: "Share", onClick: () => {} }
  ]}
/>

Result in default styled mode:

  • Copy shows badge 1
  • Share shows badge 2 / Num2

For React/Vue/Svelte unstyled mode, the visual badge is skipped. Metadata remains available via:

  • data-shortcut
  • data-shortcut-id

data-shortcut-hint is emitted when shortcut hint rendering is enabled (default styled mode).

Full Keyboard Shortcut ID Map

FabButton exports the complete map:

import {
  FAB_BUTTON_SHORTCUT_ID_TO_CODE,
  FAB_BUTTON_SHORTCUT_CODE_TO_ID
} from "@rezafab/fab-button"

Visual map is available in Storybook via story:

  • FabButton/Examples -> FullKeyboardShortcutIdMap

100% Full-Size Keyboard (104 Keys)

| ID | Code | ID | Code | ID | Code | | --- | --- | --- | --- | --- | --- | | 1 | Escape | 2 | F1 | 3 | F2 | | 4 | F3 | 5 | F4 | 6 | F5 | | 7 | F6 | 8 | F7 | 9 | F8 | | 10 | F9 | 11 | F10 | 12 | F11 | | 13 | F12 | 14 | Backquote | 15 | Digit1 | | 16 | Digit2 | 17 | Digit3 | 18 | Digit4 | | 19 | Digit5 | 20 | Digit6 | 21 | Digit7 | | 22 | Digit8 | 23 | Digit9 | 24 | Digit0 | | 25 | Minus | 26 | Equal | 27 | Backspace | | 28 | Tab | 29 | KeyQ | 30 | KeyW | | 31 | KeyE | 32 | KeyR | 33 | KeyT | | 34 | KeyY | 35 | KeyU | 36 | KeyI | | 37 | KeyO | 38 | KeyP | 39 | BracketLeft | | 40 | BracketRight | 41 | Backslash | 42 | CapsLock | | 43 | KeyA | 44 | KeyS | 45 | KeyD | | 46 | KeyF | 47 | KeyG | 48 | KeyH | | 49 | KeyJ | 50 | KeyK | 51 | KeyL | | 52 | Semicolon | 53 | Quote | 54 | Enter | | 55 | ShiftLeft | 56 | KeyZ | 57 | KeyX | | 58 | KeyC | 59 | KeyV | 60 | KeyB | | 61 | KeyN | 62 | KeyM | 63 | Comma | | 64 | Period | 65 | Slash | 66 | ShiftRight | | 67 | ControlLeft | 68 | MetaLeft | 69 | AltLeft | | 70 | Space | 71 | AltRight | 72 | MetaRight | | 73 | ContextMenu | 74 | ControlRight | 75 | PrintScreen | | 76 | ScrollLock | 77 | Pause | 78 | Insert | | 79 | Home | 80 | PageUp | 81 | Delete | | 82 | End | 83 | PageDown | 84 | ArrowUp | | 85 | ArrowLeft | 86 | ArrowDown | 87 | ArrowRight | | 88 | NumLock | 89 | NumpadDivide | 90 | NumpadMultiply | | 91 | NumpadSubtract | 92 | NumpadAdd | 93 | NumpadEnter | | 94 | Numpad1 | 95 | Numpad2 | 96 | Numpad3 | | 97 | Numpad4 | 98 | Numpad5 | 99 | Numpad6 | | 100 | Numpad7 | 101 | Numpad8 | 102 | Numpad9 | | 103 | Numpad0 | 104 | NumpadDecimal | | |

Additional Keys (Non 100%)

| ID | Code | ID | Code | ID | Code | | --- | --- | --- | --- | --- | --- | | 105 | IntlBackslash | 106 | IntlRo | 107 | IntlYen | | 108 | Convert | 109 | NonConvert | 110 | KanaMode | | 111 | Lang1 | 112 | Lang2 | 113 | F13 | | 114 | F14 | 115 | F15 | 116 | F16 | | 117 | F17 | 118 | F18 | 119 | F19 | | 120 | F20 | 121 | F21 | 122 | F22 | | 123 | F23 | 124 | F24 | 125 | NumpadEqual | | 126 | NumpadComma | 127 | NumpadParenLeft | 128 | NumpadParenRight | | 129 | Lang3 | 130 | Lang4 | 131 | Lang5 | | 132 | Fn | 133 | VolumeMute | 134 | VolumeDown | | 135 | VolumeUp | 136 | MediaTrackNext | 137 | MediaTrackPrevious | | 138 | MediaStop | 139 | MediaPlayPause | 140 | LaunchMail | | 141 | LaunchApp1 | 142 | LaunchApp2 | 143 | BrowserSearch | | 144 | BrowserHome | 145 | BrowserBack | 146 | BrowserForward | | 147 | BrowserRefresh | 148 | BrowserStop | 149 | BrowserFavorites |

To generate all 1-149 keys automatically from source map:

import { FAB_BUTTON_SHORTCUT_ID_TO_CODE } from "@rezafab/fab-button"

const allKeys = Object.entries(FAB_BUTTON_SHORTCUT_ID_TO_CODE).map(([id, code]) => ({
  id: Number(id),
  code
}))

Development

pnpm install
pnpm build
pnpm storybook

Non-Bundler Examples

FabButton now includes plain HTML + CDN examples for non-bundler environments (Web Component adapter):

Roadmap

  • Interactive playground + code generator for React, Vue, Svelte, and Element usage