MainButton
Accent button at the top of a side menu that opens a menu of the things a user can create. It is the portal's "New document / New folder / Upload" button, and it fills the width it is given.
Use this when / not when
- Use for the one creating action of a whole section, at the top of its navigation panel.
- Not for an ordinary action in a form or a dialog —
Buttonhas the sizes, the primary and secondary looks and a loading state, and it is a real<button>. - Not for a menu opened from an arbitrary element:
ContextMenuis the menu itself, and you open it where you like through its ref. - Not on a phone layout. The portal swaps in
MainButtonMobile, a floating disc in the corner. - There is no icon prop and no sizes. The button is the text, an optional arrow, and the accent background.
Import
import { MainButton } from "@onlyoffice/apps-ui-kit/components/main-button";
Also exported from the root barrel @onlyoffice/apps-ui-kit.
Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme: every value the button
draws itself with — background, padding, radius, font size and weight — is defined under the
theme's .light / .dark class, so without it the button is unstyled text. The menu is a
ContextMenu, which needs TranslationProvider from
@onlyoffice/apps-ui-kit/providers/translation for its own labels.
Stories
Default
The button with its menu: click it to open the list of things to create, then change any other prop live in the Controls panel below.
Disabled
MainButton in a disabled state. The button cannot be interacted with and appears with reduced opacity.
Disabled With Dropdown
MainButton with a dropdown menu in a disabled state. Both the button and dropdown are non-interactive.
With Action
For a single action that needs no menu: the button has no arrow, and a click is reported in the Actions panel instead of opening a list (isDropdown={false}, onAction).
With Item Descriptions
For choices that need a word of explanation: click the button and each item shows its label with a description under it, while the menu grows wider than the button to fit the text (description on the items of model).
With Dropdown
MainButton with a full dropdown menu including icons, nested sub-menus, and separators. Click the button to see the dropdown.
Without Arrow
For a button whose label alone says it opens a list: the arrow beside the text is gone, yet a click still opens the same menu (hideArrow).
Right To Left
The button in a right-to-left layout: the text moves to the right edge and the arrow to the left; click it and the menu opens with its icons on the right and the sub-menu chevron on the left. The wrapper carries dir="rtl"; the direction also comes from the theme's interfaceDirection (the Direction toolbar).
Css Customization
Every overridable variable set on a wrapper around one button -- the variables are listed under CSS variables on this page. Click it to see the menu keep the button's new width.
Minimal example
import { MainButton } from "@onlyoffice/apps-ui-kit/components/main-button";
export function CreateNew({ onCreate }: { onCreate: (kind: string) => void }) {
return (
<div style={{ maxWidth: 210 }}>
<MainButton
text="Create new"
model={[
{ key: "doc", label: "New document", onClick: () => onCreate("doc") },
{ key: "sep", isSeparator: true },
{ key: "upload", label: "Upload", onClick: () => onCreate("upload") },
]}
/>
</div>
);
}
Props
Portal-only props
These need DocSpace portal context and do nothing in a standalone app.
| Property | Type | Description |
|---|---|---|
setRefMap? | (key: GuidanceRefKey, ref: RefObject<HTMLDivElement | null>) => void | Registers the button's element in the portal's guidance map, for the onboarding tour. |
Recipes
As a plain action button
isDropdown={false} turns the button into a single action: the arrow disappears, no menu is
rendered, and the click calls onAction. model is still required by the type, and nothing
reads it.
import { MainButton } from "@onlyoffice/apps-ui-kit/components/main-button";
export function InviteButton({ onInvite }: { onInvite: () => void }) {
return (
<div style={{ maxWidth: 210 }}>
<MainButton
text="Invite"
isDropdown={false}
model={[]}
onAction={onInvite}
/>
</div>
);
}
Disabled / read-only
isDisabled fades the button to 60% and drops the click, in both modes: the menu does not
open and onAction is not called.
import { MainButton } from "@onlyoffice/apps-ui-kit/components/main-button";
export function CreateNewLocked({ canCreate }: { canCreate: boolean }) {
return (
<div style={{ maxWidth: 210 }}>
<MainButton
text="Create new"
isDisabled={!canCreate}
model={[{ key: "doc", label: "New document" }]}
/>
</div>
);
}
Items with descriptions
An item carrying description is drawn as two lines, and one such item changes how the whole
menu is sized: instead of being clamped to the button's width it is sized by its content,
with the button's width as a minimum.
import { MainButton } from "@onlyoffice/apps-ui-kit/components/main-button";
export function InviteMenu({ onInvite }: { onInvite: (role: string) => void }) {
return (
<div style={{ maxWidth: 210 }}>
<MainButton
text="Invite"
model={[
{
key: "admin",
label: "Room admin",
description: "Manages the rooms they are assigned to.",
onClick: () => onInvite("admin"),
},
{
key: "user",
label: "User",
description: "Sees only the rooms they were invited to.",
onClick: () => onInvite("user"),
},
]}
/>
</div>
);
}
Behaviour the types don't state
- The button has no width of its own. It is
display: flexwithjustify-content: space-between, so it fills its parent and pushes the arrow to the far edge. Give it a parent with a width; the portal's is 210px. - The menu's width is the button's width, measured with
getBoundingClientRecton mount and on everywindowresize. There is noResizeObserver: a button that changes width without the window changing keeps the previous menu width until the next resize. One item with adescriptionswitches that width to amin-widthand lets the menu grow. modelgoes to the menu unchanged, so an item takes everything aContextMenuitem does: aniconof its own,isSeparatorfor a divider between groups, anditemsfor a sub-menu that an arrow on the item opens to the side.- The menu is a
ContextMenuappended todocument.bodyand positionedfixedat the button's bottom inline-start corner, through a synthetic mouse event built from the button's rect. It is not inside the button's DOM subtree, so a parent'soverflow: hiddendoes not clip it and a parent'sz-indexdoes not raise it. - Opening is not observable. There is no
onOpen,onCloseoropenprop, and the menu's state lives inside the component;openedis declared, never read, and lands on the DOM element as an unknown attribute. onActionis only called whileisDropdownisfalse. In dropdown mode the click toggles the menu andonActionis never reached — a handler passed alongside the defaultisDropdownsilently never runs.hideArrowhides only the arrow. The menu still opens on click; it removes the affordance, not the behaviour.textis the whole label. Children are not rendered, and there is no icon prop. The text is aTextwithdisplay: inlineand no truncation, so a label longer than the button wraps rather than ellipsising.className,idandstyleall land on the button, not on theposition: relativewrapper around it, so none of them can be used to position the button.anchorRefreplaces the button as the element the menu is measured and positioned against — for a button visually nested inside a larger control.
CSS variables
Set them on any ancestor. Each has a theme value behind it; these are the overrides.
The background falls back to the kit's blue when the theme carries no colour scheme, so unlike
FloatingButton this one is never invisible.
Accessibility
- The button is a
<div>with a click handler. Norole, notabIndex, no key handler: it cannot be focused or activated from the keyboard, and a screen reader announces the text as ordinary content. - Nothing announces that the element opens a menu — there is no
aria-haspopuporaria-expanded, and neither can be supplied, because unknown props are not accepted byMainButtonProps. - The disabled state is opacity plus a dropped handler.
aria-disabledis not set, so the difference is invisible to assistive technology. - The menu that opens is a
ContextMenuand has keyboard navigation of its own once it is open — reaching it is the part that does not work.
Test ids
The menu carries context-menu, from ContextMenu. None of them
can be overridden by a prop; the button itself is reached through id.
Related
ContextMenu— the menu this button opens, on its own.Button— the ordinary button, with sizes, variants and a loader.MainButtonMobile— the phone form of the same action.