Actions
FAB
Floating action buttons in three sizes, plus the extended FAB.
A floating action button holds the single most important action on a screen. It comes in three sizes; give it a label to make an extended FAB when the icon alone isn't clear enough.
Installation
npx @grenishrai/expressive add fabThis copies fab.tsx into your components folder, along with what it needs from Expressive (utils, ripple, styles), and installs @base-ui/react, class-variance-authority.
Usage
import { Fab } from "@/components/expressive/fab";
<Fab icon={<EditIcon />} aria-label="Compose" />
<Fab icon={<EditIcon />} label="Compose" size="medium" color="tertiary-container" />Props
Fab
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | ReactNode | — | The icon. |
| label | ReactNode | — | Visible label. Makes it an extended FAB. Without it, aria-label is required. |
| size | "default" | "medium" | "large" | "default" | 56, 80 or 96px. |
| color | "primary-container" | "secondary-container" | "tertiary-container" | "primary" | "secondary" | "tertiary" | "surface" | "primary-container" | Container color role. |
| motion | "expressive" | "standard" | "fluid" | "none" | theme default | Motion scheme for this component's transitions. none turns animation off. |
| className | string | — | Extra classes, merged with tailwind-merge. |
Accessibility
- Renders a native button with a required accessible name.
- Use one FAB per screen so its action is unambiguous.
Source
This is the file the CLI writes into your project, shown with the default import paths. It's yours to change.
"use client";
import * as React from "react";
import { Button as BaseButton } from "@base-ui/react/button";
import { type VariantProps, cva } from "class-variance-authority";
import { cn } from "@/lib/expressive/utils";
import type { MotionProp } from "@/lib/expressive/use-motion";
import { useComposedRef, useFluidPress } from "@/lib/expressive/use-fluid";
import { Ripple } from "@/components/expressive/ripple";
/*
* Floating action button in the three M3 Expressive sizes. Pass `label` for an
* extended FAB; icon-only FABs need an accessible name.
*/
export const fabVariants = cva(
[
"state-layer focus-ring relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap",
"shadow-elevation-3 hover:shadow-elevation-4 focus-visible:shadow-elevation-3 active:shadow-elevation-3",
"transition-[border-radius,box-shadow,background-color] spring-spatial-fast",
"disabled:cursor-default disabled:bg-on-surface/(--ex-disabled-container) disabled:text-on-surface/(--ex-disabled-content) disabled:shadow-none",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
],
{
variants: {
color: {
"primary-container": "bg-primary-container text-on-primary-container",
"secondary-container": "bg-secondary-container text-on-secondary-container",
"tertiary-container": "bg-tertiary-container text-on-tertiary-container",
primary: "bg-primary text-on-primary",
secondary: "bg-secondary text-on-secondary",
tertiary: "bg-tertiary text-on-tertiary",
surface: "bg-surface-container-high text-primary",
},
size: {
default: "h-14 min-w-14 rounded-shape-large active:rounded-shape-medium [&_svg]:size-6",
medium: "h-20 min-w-20 rounded-shape-large-increased active:rounded-shape-large [&_svg]:size-7",
large: "h-24 min-w-24 rounded-shape-extra-large active:rounded-shape-large-increased [&_svg]:size-9",
},
extended: { true: "", false: "" },
},
compoundVariants: [
{ extended: true, size: "default", className: "gap-3 px-4 text-title-medium" },
{ extended: true, size: "medium", className: "gap-3 px-6 text-title-large" },
{ extended: true, size: "large", className: "gap-4 px-7 text-headline-small" },
],
defaultVariants: { color: "primary-container", size: "default", extended: false },
},
);
type FabBase = Omit<VariantProps<typeof fabVariants>, "extended"> &
Omit<BaseButton.Props, "className" | "color"> & {
icon?: React.ReactNode;
motion?: MotionProp;
className?: string;
ref?: React.Ref<HTMLElement>;
};
export type FabProps = FabBase &
({ label: React.ReactNode } | { label?: undefined; "aria-label": string } | { label?: undefined; "aria-labelledby": string });
export function Fab({ className, color, size, icon, label, motion, disabled, children, ref, ...props }: FabProps) {
const extended = label !== undefined;
const local = React.useRef<HTMLElement>(null);
useFluidPress(local, motion, !disabled);
return (
<BaseButton
ref={useComposedRef(local, ref)}
data-slot="fab"
data-motion={motion}
disabled={disabled}
className={cn(fabVariants({ color, size, extended }), className)}
{...props}
>
{icon}
{label}
{children}
<Ripple motion={motion} disabled={disabled} />
</BaseButton>
);
}