Usage
SplitButton pairs one primary action with a separate button that opens related actions. Use it when one action is the common choice and the other actions are closely related alternatives. The primary button runs independently from the menu button. The examples use report export and project sharing to show practical actions, then show primary-content choices and disabled states. The size, visual variant, and menu-width comparisons keep every other component input constant.
Import SplitButton from @udixio/ui-react, @udixio/ui-angular, or @udixio/ui-svelte. The three adapters share the same actions data model.
Export a report
Download PDF is the common action. The menu offers CSV, Excel, and a shareable report link. The live status shows which action was requested; replace these callbacks with the application’s export and clipboard logic.
Choose an export format
import { useState } from 'react';
import { iDescription } from '@udixio/icons-rounded-400/description';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { iTableView } from '@udixio/icons-rounded-400/table_view';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV', icon: iTableView },
{ id: 'excel', label: 'Download as Excel workbook', icon: iDescription },
{ id: 'link', label: 'Copy report link', icon: iLink },
];
export default function SplitButtonBasicReact() {
const [message, setMessage] = useState('Choose an export format');
return (
<div className="flex flex-col items-center gap-4">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export options"
actions={actions}
variant="filled"
size="medium"
onPrimaryAction={() => setMessage('PDF export requested')}
onActionSelect={(action) => setMessage(`${action.label} selected`)}
/>
<p className="text-body-medium" aria-live="polite">
{message}
</p>
</div>
);
}Choose an export format
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { iDescription } from '@udixio/icons-rounded-400/description';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { iTableView } from '@udixio/icons-rounded-400/table_view';
import {
SplitButton,
type SplitButtonAction,
type SplitButtonActionSelectEvent,
} from '@udixio/ui-angular';
@Component({
selector: 'docs-split-button-basic-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-center gap-4">
<udx-split-button
label="Download PDF"
[icon]="pdfIcon"
menuLabel="More report export options"
[actions]="actions"
variant="filled"
size="medium"
(primaryAction)="message.set('PDF export requested')"
(actionSelect)="selectAction($event)"
/>
<p class="text-body-medium" aria-live="polite">{{ message() }}</p>
</div>
`,
})
export class SplitButtonBasicAngular {
protected readonly pdfIcon = iPictureAsPdf;
protected readonly actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV', icon: iTableView },
{ id: 'excel', label: 'Download as Excel workbook', icon: iDescription },
{ id: 'link', label: 'Copy report link', icon: iLink },
];
protected readonly message = signal('Choose an export format');
protected selectAction(event: SplitButtonActionSelectEvent): void {
this.message.set(`${event.action.label} selected`);
}
}Choose an export format
<script lang="ts">
import type { SplitButtonAction } from '@udixio/core';
import { iDescription } from '@udixio/icons-rounded-400/description';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { iTableView } from '@udixio/icons-rounded-400/table_view';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV', icon: iTableView },
{ id: 'excel', label: 'Download as Excel workbook', icon: iDescription },
{ id: 'link', label: 'Copy report link', icon: iLink },
];
let message = $state('Choose an export format');
</script>
<div class="flex flex-col items-center gap-4">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export options"
{actions}
variant="filled"
size="medium"
onPrimaryAction={() => (message = 'PDF export requested')}
onActionSelect={(action) => (message = `${action.label} selected`)}
/>
<p class="text-body-medium" aria-live="polite">{message}</p>
</div>Primary-button content
The Material 3 specs allow the leading button to contain an icon, label text, or both. The guidelines recommend a brief one- or two-word label with a matching icon. These three examples hold the action, size, and variant constant while comparing icon plus label, label only, and icon only. The icon-only case supplies accessibleLabel.
Choose an export format
import { useState } from 'react';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
export default function SplitButtonContentReact() {
const [message, setMessage] = useState('Choose an export format');
const onPrimaryAction = () => setMessage('PDF export requested');
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-wrap items-end justify-center gap-5">
<figure className="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export formats"
actions={actions}
variant="filled"
size="medium"
onPrimaryAction={onPrimaryAction}
onActionSelect={(action) => setMessage(`${action.label} selected`)}
/>
<figcaption className="text-label-small">Icon and label</figcaption>
</figure>
<figure className="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
menuLabel="More report export formats"
actions={actions}
variant="filled"
size="medium"
onPrimaryAction={onPrimaryAction}
onActionSelect={(action) => setMessage(`${action.label} selected`)}
/>
<figcaption className="text-label-small">Label only</figcaption>
</figure>
<figure className="flex flex-col items-center gap-2">
<SplitButton
icon={iPictureAsPdf}
accessibleLabel="Download PDF"
menuLabel="More report export formats"
actions={actions}
variant="filled"
size="medium"
onPrimaryAction={onPrimaryAction}
onActionSelect={(action) => setMessage(`${action.label} selected`)}
/>
<figcaption className="text-label-small">
Icon only, accessible name “Download PDF”
</figcaption>
</figure>
</div>
<p className="text-body-medium" aria-live="polite">
{message}
</p>
</div>
);
}Choose an export format
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import {
SplitButton,
type SplitButtonAction,
type SplitButtonActionSelectEvent,
} from '@udixio/ui-angular';
@Component({
selector: 'docs-split-button-content-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-center gap-4">
<div class="flex flex-wrap items-end justify-center gap-5">
<figure class="flex flex-col items-center gap-2">
<udx-split-button
label="Download PDF"
[icon]="pdfIcon"
menuLabel="More report export formats"
[actions]="actions"
variant="filled"
size="medium"
(primaryAction)="requestPdf()"
(actionSelect)="selectAction($event)"
/>
<figcaption class="text-label-small">Icon and label</figcaption>
</figure>
<figure class="flex flex-col items-center gap-2">
<udx-split-button
label="Download PDF"
menuLabel="More report export formats"
[actions]="actions"
variant="filled"
size="medium"
(primaryAction)="requestPdf()"
(actionSelect)="selectAction($event)"
/>
<figcaption class="text-label-small">Label only</figcaption>
</figure>
<figure class="flex flex-col items-center gap-2">
<udx-split-button
[icon]="pdfIcon"
accessibleLabel="Download PDF"
menuLabel="More report export formats"
[actions]="actions"
variant="filled"
size="medium"
(primaryAction)="requestPdf()"
(actionSelect)="selectAction($event)"
/>
<figcaption class="text-label-small">
Icon only, accessible name “Download PDF”
</figcaption>
</figure>
</div>
<p class="text-body-medium" aria-live="polite">{{ message() }}</p>
</div>
`,
})
export class SplitButtonContentAngular {
protected readonly pdfIcon = iPictureAsPdf;
protected readonly actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
protected readonly message = signal('Choose an export format');
protected requestPdf(): void {
this.message.set('PDF export requested');
}
protected selectAction(event: SplitButtonActionSelectEvent): void {
this.message.set(`${event.action.label} selected`);
}
}Choose an export format
<script lang="ts">
import type { SplitButtonAction } from '@udixio/core';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
let message = $state('Choose an export format');
const requestPdf = () => (message = 'PDF export requested');
</script>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-wrap items-end justify-center gap-5">
<figure class="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export formats"
{actions}
variant="filled"
size="medium"
onPrimaryAction={requestPdf}
onActionSelect={(action) => (message = `${action.label} selected`)}
/>
<figcaption class="text-label-small">Icon and label</figcaption>
</figure>
<figure class="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
menuLabel="More report export formats"
{actions}
variant="filled"
size="medium"
onPrimaryAction={requestPdf}
onActionSelect={(action) => (message = `${action.label} selected`)}
/>
<figcaption class="text-label-small">Label only</figcaption>
</figure>
<figure class="flex flex-col items-center gap-2">
<SplitButton
icon={iPictureAsPdf}
accessibleLabel="Download PDF"
menuLabel="More report export formats"
{actions}
variant="filled"
size="medium"
onPrimaryAction={requestPdf}
onActionSelect={(action) => (message = `${action.label} selected`)}
/>
<figcaption class="text-label-small">
Icon only, accessible name “Download PDF”
</figcaption>
</figure>
</div>
<p class="text-body-medium" aria-live="polite">{message}</p>
</div>Compact project sharing
An icon-only Share action fits a compact project toolbar. Its accessible name is Share; the menu keeps the related actions visible and named. This uses the spec-supported icon-only form for a space-constrained toolbar; where space permits, prefer the brief visible label and matching icon recommended by the guidelines.
Project sharing
Open the sharing dialog or choose a related action.
import { useState } from 'react';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iMail } from '@udixio/icons-rounded-400/mail';
import { iManageAccounts } from '@udixio/icons-rounded-400/manage_accounts';
import { iShare } from '@udixio/icons-rounded-400/share';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
const actions: SplitButtonAction[] = [
{ id: 'copy-link', label: 'Copy project link', icon: iLink },
{ id: 'invite', label: 'Invite a teammate', icon: iMail },
{ id: 'manage', label: 'Manage project access', icon: iManageAccounts },
];
export default function SplitButtonIconOnlyReact() {
const [message, setMessage] = useState('');
return (
<div className="flex flex-col items-center gap-3">
<p className="text-body-medium">Project sharing</p>
<SplitButton
icon={iShare}
accessibleLabel="Share"
menuLabel="More project sharing options"
actions={actions}
variant="tonal"
size="medium"
onPrimaryAction={() => setMessage('Share action requested')}
onActionSelect={(action) => setMessage(`${action.label} selected`)}
/>
<p className="text-body-medium" aria-live="polite">
{message || 'Open the sharing dialog or choose a related action.'}
</p>
</div>
);
}Project sharing
Open the sharing dialog or choose a related action.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iMail } from '@udixio/icons-rounded-400/mail';
import { iManageAccounts } from '@udixio/icons-rounded-400/manage_accounts';
import { iShare } from '@udixio/icons-rounded-400/share';
import {
SplitButton,
type SplitButtonAction,
type SplitButtonActionSelectEvent,
} from '@udixio/ui-angular';
@Component({
selector: 'docs-split-button-icon-only-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-center gap-3">
<p class="text-body-medium">Project sharing</p>
<udx-split-button
[icon]="shareIcon"
accessibleLabel="Share"
menuLabel="More project sharing options"
[actions]="actions"
variant="tonal"
size="medium"
(primaryAction)="message.set('Share action requested')"
(actionSelect)="selectAction($event)"
/>
<p class="text-body-medium" aria-live="polite">
{{ message() || 'Open the sharing dialog or choose a related action.' }}
</p>
</div>
`,
})
export class SplitButtonIconOnlyAngular {
protected readonly shareIcon = iShare;
protected readonly actions: SplitButtonAction[] = [
{ id: 'copy-link', label: 'Copy project link', icon: iLink },
{ id: 'invite', label: 'Invite a teammate', icon: iMail },
{ id: 'manage', label: 'Manage project access', icon: iManageAccounts },
];
protected readonly message = signal('');
protected selectAction(event: SplitButtonActionSelectEvent): void {
this.message.set(`${event.action.label} selected`);
}
}Project sharing
Open the sharing dialog or choose a related action.
<script lang="ts">
import type { SplitButtonAction } from '@udixio/core';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iMail } from '@udixio/icons-rounded-400/mail';
import { iManageAccounts } from '@udixio/icons-rounded-400/manage_accounts';
import { iShare } from '@udixio/icons-rounded-400/share';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'copy-link', label: 'Copy project link', icon: iLink },
{ id: 'invite', label: 'Invite a teammate', icon: iMail },
{ id: 'manage', label: 'Manage project access', icon: iManageAccounts },
];
let message = $state('');
</script>
<div class="flex flex-col items-center gap-3">
<p class="text-body-medium">Project sharing</p>
<SplitButton
icon={iShare}
accessibleLabel="Share"
menuLabel="More project sharing options"
{actions}
variant="tonal"
size="medium"
onPrimaryAction={() => (message = 'Share action requested')}
onActionSelect={(action) => (message = `${action.label} selected`)}
/>
<p class="text-body-medium" aria-live="polite">
{message || 'Open the sharing dialog or choose a related action.'}
</p>
</div>Sizes
All five sizes use the same PDF-export action, filled variant, and default menu-button width. Only size changes. The default menu width scales with the SplitButton size.
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
import type { SplitButtonSize } from '@udixio/core';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
const sizes: SplitButtonSize[] = [
'xSmall',
'small',
'medium',
'large',
'xLarge',
];
export default function SplitButtonSizesReact() {
return (
<div className="flex flex-wrap items-end justify-center gap-3">
{sizes.map((size) => (
<figure key={size} className="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export formats"
actions={actions}
variant="filled"
size={size}
/>
<figcaption className="text-label-small">{size}</figcaption>
</figure>
))}
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-angular';
import type { SplitButtonSize } from '@udixio/core';
@Component({
selector: 'docs-split-button-sizes-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end justify-center gap-3">
@for (size of sizes; track size) {
<figure class="flex flex-col items-center gap-2">
<udx-split-button
label="Download PDF"
[icon]="pdfIcon"
menuLabel="More report export formats"
[actions]="actions"
variant="filled"
[size]="size"
/>
<figcaption class="text-label-small">{{ size }}</figcaption>
</figure>
}
</div>
`,
})
export class SplitButtonSizesAngular {
protected readonly pdfIcon = iPictureAsPdf;
protected readonly actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
protected readonly sizes: SplitButtonSize[] = [
'xSmall',
'small',
'medium',
'large',
'xLarge',
];
}<script lang="ts">
import type { SplitButtonAction, SplitButtonSize } from '@udixio/core';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
const sizes: SplitButtonSize[] = [
'xSmall',
'small',
'medium',
'large',
'xLarge',
];
</script>
<div class="flex flex-wrap items-end justify-center gap-3">
{#each sizes as size (size)}
<figure class="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export formats"
{actions}
variant="filled"
{size}
/>
<figcaption class="text-label-small">{size}</figcaption>
</figure>
{/each}
</div>Visual variants
The same medium PDF-export action is shown with each visual variant. Only variant changes; size, content, menu label, and actions stay fixed.
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
import type { SplitButtonVariant } from '@udixio/core';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
const variants: SplitButtonVariant[] = [
'elevated',
'filled',
'tonal',
'outlined',
];
export default function SplitButtonVariantsReact() {
return (
<div className="flex flex-wrap items-end justify-center gap-3">
{variants.map((variant) => (
<figure key={variant} className="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export formats"
actions={actions}
size="medium"
variant={variant}
/>
<figcaption className="text-label-small">{variant}</figcaption>
</figure>
))}
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-angular';
import type { SplitButtonVariant } from '@udixio/core';
@Component({
selector: 'docs-split-button-variants-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end justify-center gap-3">
@for (variant of variants; track variant) {
<figure class="flex flex-col items-center gap-2">
<udx-split-button
label="Download PDF"
[icon]="pdfIcon"
menuLabel="More report export formats"
[actions]="actions"
size="medium"
[variant]="variant"
/>
<figcaption class="text-label-small">{{ variant }}</figcaption>
</figure>
}
</div>
`,
})
export class SplitButtonVariantsAngular {
protected readonly pdfIcon = iPictureAsPdf;
protected readonly actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
protected readonly variants: SplitButtonVariant[] = [
'elevated',
'filled',
'tonal',
'outlined',
];
}<script lang="ts">
import type { SplitButtonAction, SplitButtonVariant } from '@udixio/core';
import { iPictureAsPdf } from '@udixio/icons-rounded-400/picture_as_pdf';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
const variants: SplitButtonVariant[] = [
'elevated',
'filled',
'tonal',
'outlined',
];
</script>
<div class="flex flex-wrap items-end justify-center gap-3">
{#each variants as variant (variant)}
<figure class="flex flex-col items-center gap-2">
<SplitButton
label="Download PDF"
icon={iPictureAsPdf}
menuLabel="More report export formats"
{actions}
size="medium"
{variant}
/>
<figcaption class="text-label-small">{variant}</figcaption>
</figure>
{/each}
</div>Menu-button width
The trailing IconButton supports narrow, default, and wide widths through menuButtonProps.width. The same medium, filled report-download action is used for each width; only menuButtonProps.width changes.
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
import type { IconButtonWidth } from '@udixio/core';
import { iDownload } from '@udixio/icons-rounded-400/download';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
const widths: IconButtonWidth[] = ['narrow', 'default', 'wide'];
export default function SplitButtonWidthReact() {
return (
<div className="flex flex-wrap items-end justify-center gap-3">
{widths.map((width) => (
<figure key={width} className="flex flex-col items-center gap-2">
<SplitButton
label="Download report"
icon={iDownload}
menuLabel="More report download formats"
actions={actions}
size="medium"
variant="filled"
menuButtonProps={{ width }}
/>
<figcaption className="text-label-small">{width}</figcaption>
</figure>
))}
</div>
);
}import { ChangeDetectionStrategy, Component } from '@angular/core';
import { iDownload } from '@udixio/icons-rounded-400/download';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-angular';
import type { IconButtonWidth } from '@udixio/core';
@Component({
selector: 'docs-split-button-width-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-wrap items-end justify-center gap-3">
@for (width of widths; track width) {
<figure class="flex flex-col items-center gap-2">
<udx-split-button
label="Download report"
[icon]="downloadIcon"
menuLabel="More report download formats"
[actions]="actions"
size="medium"
variant="filled"
[menuButtonProps]="{ width: width }"
/>
<figcaption class="text-label-small">{{ width }}</figcaption>
</figure>
}
</div>
`,
})
export class SplitButtonWidthAngular {
protected readonly actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
protected readonly downloadIcon = iDownload;
protected readonly widths: IconButtonWidth[] = ['narrow', 'default', 'wide'];
}<script lang="ts">
import type { IconButtonWidth, SplitButtonAction } from '@udixio/core';
import { iDownload } from '@udixio/icons-rounded-400/download';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
const widths: IconButtonWidth[] = ['narrow', 'default', 'wide'];
</script>
<div class="flex flex-wrap items-end justify-center gap-3">
{#each widths as width (width)}
<figure class="flex flex-col items-center gap-2">
<SplitButton
label="Download report"
icon={iDownload}
menuLabel="More report download formats"
{actions}
size="medium"
variant="filled"
menuButtonProps={{ width }}
/>
<figcaption class="text-label-small">{width}</figcaption>
</figure>
{/each}
</div>Disabled SplitButton
When no report is selected, disable the whole control. This blocks the primary action, menu trigger, and every related export action.
Choose a report to enable export.
import { iDownload } from '@udixio/icons-rounded-400/download';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
export default function SplitButtonDisabledReact() {
return (
<div className="flex flex-col items-center justify-center gap-3">
<p className="text-body-medium">Choose a report to enable export.</p>
<SplitButton
label="Export report"
icon={iDownload}
menuLabel="More report export formats"
actions={actions}
variant="filled"
size="medium"
disabled
/>
</div>
);
}Choose a report to enable export.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { iDownload } from '@udixio/icons-rounded-400/download';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-angular';
@Component({
selector: 'docs-split-button-disabled-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-center justify-center gap-3">
<p class="text-body-medium">Choose a report to enable export.</p>
<udx-split-button
label="Export report"
[icon]="downloadIcon"
menuLabel="More report export formats"
[actions]="actions"
variant="filled"
size="medium"
disabled
/>
</div>
`,
})
export class SplitButtonDisabledAngular {
protected readonly downloadIcon = iDownload;
protected readonly actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
}Choose a report to enable export.
<script lang="ts">
import type { SplitButtonAction } from '@udixio/core';
import { iDownload } from '@udixio/icons-rounded-400/download';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'csv', label: 'Download as CSV' },
{ id: 'excel', label: 'Download as Excel workbook' },
];
</script>
<div class="flex flex-col items-center justify-center gap-3">
<p class="text-body-medium">Choose a report to enable export.</p>
<SplitButton
label="Export report"
icon={iDownload}
menuLabel="More report export formats"
{actions}
variant="filled"
size="medium"
disabled
/>
</div>One unavailable menu action
Here, workspace policy disables public-link sharing while email invites and access management remain available. Set disabled on that action record only.
Public links are disabled by this workspace’s administrator.
Choose how to share the project
import { useState } from 'react';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iMail } from '@udixio/icons-rounded-400/mail';
import { iManageAccounts } from '@udixio/icons-rounded-400/manage_accounts';
import { iShare } from '@udixio/icons-rounded-400/share';
import { SplitButton, type SplitButtonAction } from '@udixio/ui-react';
const actions: SplitButtonAction[] = [
{ id: 'copy-link', label: 'Copy public link', icon: iLink, disabled: true },
{ id: 'invite', label: 'Invite by email', icon: iMail },
{ id: 'manage', label: 'Manage access', icon: iManageAccounts },
];
export default function SplitButtonDisabledActionReact() {
const [message, setMessage] = useState('Choose how to share the project');
return (
<div className="flex flex-col items-center gap-3">
<p className="text-body-medium">
Public links are disabled by this workspace’s administrator.
</p>
<SplitButton
label="Share project"
icon={iShare}
menuLabel="More project sharing options"
actions={actions}
variant="filled"
size="medium"
onPrimaryAction={() => setMessage('Share dialog requested')}
onActionSelect={(action) => setMessage(`${action.label} selected`)}
/>
<p className="text-body-medium" aria-live="polite">
{message}
</p>
</div>
);
}Public links are disabled by this workspace’s administrator.
Choose how to share the project
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iMail } from '@udixio/icons-rounded-400/mail';
import { iManageAccounts } from '@udixio/icons-rounded-400/manage_accounts';
import { iShare } from '@udixio/icons-rounded-400/share';
import {
SplitButton,
type SplitButtonAction,
type SplitButtonActionSelectEvent,
} from '@udixio/ui-angular';
@Component({
selector: 'docs-split-button-disabled-action-angular',
standalone: true,
imports: [SplitButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-center gap-3">
<p class="text-body-medium">
Public links are disabled by this workspace’s administrator.
</p>
<udx-split-button
label="Share project"
[icon]="shareIcon"
menuLabel="More project sharing options"
[actions]="actions"
variant="filled"
size="medium"
(primaryAction)="message.set('Share dialog requested')"
(actionSelect)="selectAction($event)"
/>
<p class="text-body-medium" aria-live="polite">{{ message() }}</p>
</div>
`,
})
export class SplitButtonDisabledActionAngular {
protected readonly shareIcon = iShare;
protected readonly actions: SplitButtonAction[] = [
{ id: 'copy-link', label: 'Copy public link', icon: iLink, disabled: true },
{ id: 'invite', label: 'Invite by email', icon: iMail },
{ id: 'manage', label: 'Manage access', icon: iManageAccounts },
];
protected readonly message = signal('Choose how to share the project');
protected selectAction(event: SplitButtonActionSelectEvent): void {
this.message.set(`${event.action.label} selected`);
}
}Public links are disabled by this workspace’s administrator.
Choose how to share the project
<script lang="ts">
import type { SplitButtonAction } from '@udixio/core';
import { iLink } from '@udixio/icons-rounded-400/link';
import { iMail } from '@udixio/icons-rounded-400/mail';
import { iManageAccounts } from '@udixio/icons-rounded-400/manage_accounts';
import { iShare } from '@udixio/icons-rounded-400/share';
import { SplitButton } from '@udixio/ui-svelte';
const actions: SplitButtonAction[] = [
{ id: 'copy-link', label: 'Copy public link', icon: iLink, disabled: true },
{ id: 'invite', label: 'Invite by email', icon: iMail },
{ id: 'manage', label: 'Manage access', icon: iManageAccounts },
];
let message = $state('Choose how to share the project');
</script>
<div class="flex flex-col items-center gap-3">
<p class="text-body-medium">
Public links are disabled by this workspace’s administrator.
</p>
<SplitButton
label="Share project"
icon={iShare}
menuLabel="More project sharing options"
{actions}
variant="filled"
size="medium"
onPrimaryAction={() => (message = 'Share dialog requested')}
onActionSelect={(action) => (message = `${action.label} selected`)}
/>
<p class="text-body-medium" aria-live="polite">{message}</p>
</div>Anatomy and variants
Material 3 defines two adjacent buttons: the leading button runs the primary action and can show a label, an icon, or both; the trailing button always shows the menu icon and opens related options. This implementation composes the shared Button and IconButton primitives and supports the five sizes xSmall, small, medium, large, and xLarge, plus the elevated, filled, tonal, and outlined visual treatments. The default is small and filled. menuButtonProps forwards supported IconButton options such as width, while SplitButton supplies and owns the trailing menu icon and accessible label. primaryButtonProps forwards additional Button options to the leading part.
Actions and state
actions is a non-empty array of { id, label, icon?, href?, disabled? } records. Each action needs a unique, non-empty id and visible label. onPrimaryAction runs the main action. Selecting a menu item emits onActionSelect(action, index) in React and Svelte, or actionSelect with { action, index } in Angular; the menu then closes. Action links keep native link semantics.
Use open with onOpenChange in React, open with openChange in Angular, or bind:open in Svelte for controlled visibility. defaultOpen initializes uncontrolled state. The visible primary label names the primary button; an icon-only primary action also needs accessibleLabel. menuLabel is required and names both the trailing menu button and the popup.
Accessibility and motion
Both parts are native buttons with their own keyboard focus. The trailing button exposes aria-haspopup="menu", aria-expanded, and aria-controls. Enter and Space open the menu and focus its first available action. Arrow Down opens at the first action and Arrow Up at the last; inside the menu, the shared Menu implementation handles arrow navigation, Home, End, and type-ahead. Escape and outside press close the menu; Escape restores focus to its trigger. Each part retains a 48px minimum touch target.
Each button morphs only its own inner corners when that button is hovered, focused, or pressed. Opening the menu turns the trailing chevron and keeps only the trailing button in its selected state: its inner corners stay expanded and its state layer shows the selected overlay. The primary button keeps its own shape. The shared controller honors prefers-reduced-motion by applying the final state immediately. The popup aligns to the inline end of the split button and uses the Menu component’s scrollable surface.
Design decisions and limits
| Classification | Decision |
|---|---|
| Material 3 guidance | Two independent button parts; the trailing part always shows a menu icon and opens related actions; leading content may be a label, icon, or both; guidelines recommend a brief label with a matching icon; five sizes and four visual styles. |
| WAI-ARIA guidance | Use a native menu button with aria-haspopup="menu", aria-expanded, a labelled menu, and the menu-button keyboard pattern. |
| Udixio API choice | Use a shared action-record array, following FabMenu; use menuLabel for the trigger and popup; control visibility with open/defaultOpen. |
| Unsupported | Text treatment, nested menus, and rich framework-specific menu content are not included. |
The primary action is a callback, so use a separate Link for primary navigation. The popup is positioned relative to the component and does not portal or flip based on viewport space; keep it out of clipping containers.