Skip to main content

MainButtonMobile

Floating round button in the corner of the screen that opens a full-width sheet of actions. It is the phone form of the portal's "create" button: a plus that becomes a minus while its sheet is open.

MainButtonMobileMainButtonMobile

Use this when / not when​

  • Use for the one creating action of a mobile screen, when there is no room for a button at the top of a panel.
  • Not on desktop — MainButton is the wide accent button that belongs at the top of a side menu. This component does not switch between the two forms; the portal renders one or the other.
  • Not for the progress of a background operation, even though it is built on one: FloatingButton is the disc that shows progress.
  • Not for an ordinary menu anchored to a control — DropDown with DropDownItem gives you the same items without the fixed corner placement.
  • There is no progress bar and no upload section any more. percent and title are declared and ignored, and the ProgressOption type has nothing that accepts it.

Import​

import { MainButtonMobile } from "@onlyoffice/apps-ui-kit/components/main-button-mobile";

Also exported from the root barrel @onlyoffice/apps-ui-kit.

Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme. Everything this component draws — the button's colour, the sheet's background and, under the light theme, the sheet's whole placement — comes from custom properties defined on the theme's .light / .dark class. See the dark-theme note under "Behaviour the types don't state".

Stories​

Default​

The button in the corner of the screen with both groups of items. Tap it to open the menu, tap outside or pick an item to close it, and change any other prop live in the Controls panel below.

DefaultDefault

With Alert​

A badge on the closed button draws attention to something waiting for the user (alert). Click the badge to see onAlertClick in the Actions panel, which is called only while withAlertClick is set; the badge is hidden while the menu is open.

With AlertWith Alert

With Submenu​

Groups related actions under one item without opening a second menu. New form opens its nested items in place under it, already expanded (items, openByDefault); New folder carries a second line under its label (description).

With SubmenuWith Submenu

Without Menu​

For a screen with only one thing to create, the button runs that action directly (withMenu). Click it to see onClick in the Actions panel; no menu opens.

Without MenuWithout Menu

Css Customization​

The variables are listed under CSS variables on this page. One instance, with the alert badge on so the badge variables show. Open the menu to see the item padding and the lower group's background.

Css CustomizationCss Customization

Minimal example​

The component keeps the open state internally; opened seeds it and overrides it whenever the value you pass changes.

import { useState } from "react";
import { MainButtonMobile } from "@onlyoffice/apps-ui-kit/components/main-button-mobile";

export function CreateSheet({
onCreate,
}: {
onCreate: (kind: string) => void;
}) {
const [opened, setOpened] = useState(false);

return (
<div style={{ position: "relative", height: 400 }}>
<MainButtonMobile
opened={opened}
isOpenButton
onClose={() => setOpened(false)}
actionOptions={[
{
key: "doc",
label: "New document",
onClick: () => onCreate("doc"),
},
{
key: "folder",
label: "New folder",
onClick: () => onCreate("folder"),
},
]}
/>
</div>
);
}

Props​

PropertyTypeDescription
actionOptions?ActionOption[]Items of the upper group of the menu. An item with items becomes a submenu.
alert?booleanWhether the alert badge is drawn over the button. It is hidden while the menu is open.
buttonOptions?ButtonOption[]Items of the lower group, drawn on a background of its own. An item with items becomes a submenu.
className?stringApplied to the wrapper that carries the button and the menu.
isOpenButton?booleanWhether onClose is called at all. It then fires on every toggle, including the one that opens the menu.
mainButtonRef?RefObject<HTMLDivElement | null>Ignored. The component uses an internal ref of the same name; use ref for the element.
manualWidth?stringWidth of the menu, as a CSS length.
onAlertClick?() => voidCalled when the alert badge is clicked, and only while withAlertClick is set.
onClick?(e: React.MouseEvent) => voidCalled with the event when the button is clicked, and only while withMenu is false.
onClose?() => voidCalled on every toggle of the menu, and only while isOpenButton is set.
onUploadClick?() => voidIgnored. Nothing reads this prop; the button's own handler is onClick.
opened?booleanWhether the menu is open. It is copied into state, so a click changes it back without telling you.
percent?numberIgnored. Nothing reads this prop; the button draws no progress.
ref?RefObject<MainButtonMobileRef>Handle exposing contains and getButtonElement, for deciding whether a click landed on the button.
sectionWidth?numberIgnored. Nothing reads this prop.
style?CSSPropertiesMerged into the wrapper's inline style, after the z-index the component sets itself.
title?stringIgnored. Nothing reads this prop; the groups have no heading.
withAlertClick?booleanWhether clicking the alert badge calls onAlertClick.
withButton?booleanIgnored. Nothing reads this prop.
withMenu?booleanWhether the button opens the menu. When false it calls onClick and the menu never opens. Default: true.
withoutButton?booleanWhether the lower group takes the plain wrapper background instead of the accent one.

actionOptions takes ActionOption and buttonOptions takes ButtonOption. Both carry key, label, an optional icon URL, onClick, isSeparator and nested items; an ActionOption also has action, which is handed back as onClick({ action }), a description drawn under the label, and openByDefault for a submenu that starts open.

Recipes​

Open / close (controlled)​

opened is copied into state on every change, so it opens and closes the sheet — but the button toggles that state on its own as well, and the only way to hear about it is onClose, which needs isOpenButton and fires on opening too.

import { useState } from "react";
import { MainButtonMobile } from "@onlyoffice/apps-ui-kit/components/main-button-mobile";

export function CreateSheetControlled() {
const [opened, setOpened] = useState(false);

return (
<div style={{ position: "relative", height: 400 }}>
<button type="button" onClick={() => setOpened(true)}>
Open the sheet
</button>
<MainButtonMobile
opened={opened}
isOpenButton
onClose={() => setOpened(false)}
actionOptions={[{ key: "doc", label: "New document" }]}
/>
</div>
);
}

As a plain button​

withMenu={false} stops the sheet from opening and routes the click to onClick — the floating plus becomes a single action.

import { MainButtonMobile } from "@onlyoffice/apps-ui-kit/components/main-button-mobile";

export function UploadFab({ onUpload }: { onUpload: () => void }) {
return (
<div style={{ position: "relative", height: 400 }}>
<MainButtonMobile withMenu={false} onClick={onUpload} />
</div>
);
}

Two groups and a submenu​

actionOptions is the upper group and buttonOptions the lower one, which has a background of its own. An item with items becomes a submenu that expands in place rather than opening to the side.

import { MainButtonMobile } from "@onlyoffice/apps-ui-kit/components/main-button-mobile";

export function CreateSheetGrouped({
onCreate,
}: {
onCreate: (action?: string) => void;
}) {
return (
<div style={{ position: "relative", height: 400 }}>
<MainButtonMobile
manualWidth="320px"
actionOptions={[
{
key: "form",
label: "Form",
openByDefault: true,
items: [
{ key: "blank", label: "From blank", action: "form-blank" },
{ key: "file", label: "From a text file", action: "form-file" },
],
onClick: ({ action }) => onCreate(action),
},
{ key: "sep", label: "", isSeparator: true },
{ key: "folder", label: "New folder", action: "folder" },
]}
buttonOptions={[{ key: "upload", label: "Upload from device" }]}
/>
</div>
);
}

The alert badge​

alert puts a badge on the button while the sheet is closed. Clicking it does nothing unless withAlertClick is set as well.

import { MainButtonMobile } from "@onlyoffice/apps-ui-kit/components/main-button-mobile";

export function CreateSheetWithAlert({
onOpenPanel,
}: {
onOpenPanel: () => void;
}) {
return (
<div style={{ position: "relative", height: 400 }}>
<MainButtonMobile
alert
withAlertClick
onAlertClick={onOpenPanel}
actionOptions={[{ key: "doc", label: "New document" }]}
/>
</div>
);
}

Behaviour the types don't state​

  • The sheet's placement is declared only under the light theme. position, width, inset-inline-end, bottom and z-index are all read from custom properties that MainButtonMobile.theme.scss defines on .light and never on .dark, so under the dark theme those declarations are invalid and the sheet falls back into normal flow at automatic width. The button itself is unaffected.
  • The button positions itself only as much as FloatingButton does, which is to say it is pinned to the bottom trailing corner of the nearest positioned ancestor. The wrapper takes z-index: 201, raised to 211 while the sheet is open, and the backdrop sits between them at 210.
  • It reads window.innerHeight during the first render, so importing it into a server-rendered tree throws before any effect can run.
  • The sheet's height is measured, not set. Its content is measured with getBoundingClientRect after every open, submenu toggle and window resize, and capped at window.innerHeight - 48. dropdownStyle is applied before that height and cannot change it.
  • manualWidth defaults to 400px, capped by the stylesheet at 100vw - 48px, and replaced at the mobile breakpoint by 100vw - 64px.
  • The browser's Back button closes the sheet. A popstate listener sets the state to closed without calling onClose.
  • A click anywhere outside closes it, through a Backdrop the component renders itself. onClose is only reached when isOpenButton is set.
  • On iOS the component reaches for document.getElementsByClassName("section-scroll")[0] and attaches a scroll listener to it, to swap the sheet's background as the page moves. An iOS page without an element of that class throws on mount.
  • On a device react-device-detect calls mobile, the sheet's content is wrapped in Scrollbar and every item's hover effect is turned off.
  • withoutButton does not hide anything. It swaps the lower group's background for the plain wrapper colour; the button and both groups are drawn either way.
  • Choosing any item closes the sheet, before its handler runs. An ActionOption's handler is called as onClick({ action }), not with the event; a ButtonOption's handler takes no argument at all.
  • ref is an imperative handle, not an element ref: it exposes contains(target) and getButtonElement(). Its type MainButtonMobileRef is not exported from the folder, so it can only be typed by inference.

CSS variables​

Set them on any ancestor. They are what the theme defines; the sheet's geometry is in the same set, which is why overriding it is the workaround for the dark-theme note above.

VariableDefaultEffect
--main-button-mobile-button-colorscheme accentColour of the round button, from --color-scheme-main-accent.
--main-button-mobile-icon-fillwhite / blackFill of the plus and minus.
--main-button-mobile-z-index1010Stacking of the button against its open sheet; below 202 the sheet covers the button.
--main-button-mobile-dropdown-positionfixed (light)Positioning scheme of the sheet.
--main-button-mobile-dropdown-width400px (light)Width of the sheet.
--main-button-mobile-dropdown-right48px (light)Distance from the trailing edge.
--main-button-mobile-dropdown-bottom48px (light)Distance from the bottom.
--main-button-mobile-dropdown-z-index202 (light)Stacking of the sheet.
--main-button-mobile-dropdown-item-padding6px 23pxPadding of one item.
--main-button-mobile-button-options-background-colordark accentBackground of the lower group: the scheme accent mixed with 30% black.
--main-button-mobile-button-wrapper-backgroundtheme greyBackground of the lower group under withoutButton.
--main-button-mobile-badge-size12pxSize of the alert badge.
--main-button-mobile-badge-offset10pxInset of the badge from the button's top trailing corner.

Accessibility​

  • The button is a FloatingButton: a <div> with a click handler, no role, no tabIndex and no key handler, so the sheet cannot be opened from the keyboard.
  • Its aria-label is the icon's name followed by the word "button" — "plus button", or "minus button" while the sheet is open — in English whatever the interface language.
  • The sheet is not a dialog. It is a DropDown, announced as a listbox with each item an option (a separator a separator) — not a menu — with no focus trap and no focus move; Escape does not close it, though the Back button does.
  • The alert badge is an SVG with a click handler and no accessible name.
  • Nothing marks the button as expanded. There is no aria-expanded or aria-haspopup, and unknown props are not accepted, so neither can be supplied from outside.

Test ids​

Elementdata-testid
The wrappermain-button-mobile
The sheetdropdown
The round buttonfloating-button

None of them can be overridden by a prop; the sheet's id is the one DropDown uses for every instance, so a page with more than one is ambiguous.

  • MainButton — the desktop form of the same action.
  • FloatingButton — the disc this one is built on, showing the progress of a background operation.
  • DropDown — the menu without the fixed corner placement.