一键导入
pimcore-studio-ui-buttons
Using button components in Pimcore Studio UI - Button, IconButton, IconTextButton, DropdownButton, and ButtonGroup
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Using button components in Pimcore Studio UI - Button, IconButton, IconTextButton, DropdownButton, and ButtonGroup
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when creating a new color theme or token set for Pimcore Studio UI (a DynamicTypeThemeAbstract subclass registered in the ThemeRegistry), including choosing an extends chain, filling AntD color tokens, seeding the boot palette, and picking colors that meet WCAG AAA contrast
Building filters in Pimcore Studio UI - the declarative defineFilter framework for filter panels and sidebars (createFiltersStore, FiltersRenderer, useFilterQuery) and custom field-filter dynamic types for per-column listing filters (DynamicTypeFieldFilterAbstract, DynamicTypeFieldFilterRegistry). Use this whenever building, customizing, or extending any kind of filtering, filter panel, filter sidebar, or search/filter UI, or adding a new filter type to a listing or data view - even when the user only says "add filtering", "let users filter by X", "filter this list", or "build a filter sidebar".
Building listings in Pimcore Studio UI using the ListingBuilder decorator pattern - sorting, paging, filtering, inline editing, and custom decorators
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
Pimcore Studio migration and review checklist — 34-point verification checklist, anti-patterns to avoid, and reference base classes for validating Studio code completeness
Pimcore Studio PHP code style rules — strict types, class modifiers, final/readonly patterns, formatting (120 chars), named arguments, imports, PHPDoc, @throws documentation, and constructor promotion
| name | pimcore-studio-ui-buttons |
| description | Using button components in Pimcore Studio UI - Button, IconButton, IconTextButton, DropdownButton, and ButtonGroup |
| metadata | {"audience":"pimcore-developers","focus":"ui-components-buttons"} |
How to use button components in Pimcore Studio UI:
Use this when:
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Follow these capitalization and wording guidelines for all button text:
Buttons and interactive elements use Sentence case - only the first word is capitalized:
// ✅ CORRECT - Sentence case for buttons
<Button>{t('save')}</Button> // "Save"
<Button>{t('save-draft')}</Button> // "Save draft"
<Button>{t('merge-version')}</Button> // "Merge version"
<Button>{t('review-changes')}</Button> // "Review changes"
<Button>{t('export-csv')}</Button> // "Export CSV"
<Button>{t('confirm-delete')}</Button> // "Confirm delete"
// ❌ WRONG - Don't use Title Case for buttons
<Button>{t('save-draft')}</Button> // Not "Save Draft"
<Button>{t('review-changes')}</Button> // Not "Review Changes"
This applies to:
Use verb-first phrasing for all actionable interface elements:
// ✅ CORRECT - Action-oriented (verb-first)
<Button>{t('export-csv')}</Button> // "Export CSV"
<Button>{t('download-file')}</Button> // "Download file"
<Button>{t('create-new')}</Button> // "Create new"
<Button>{t('save-changes')}</Button> // "Save changes"
<IconButton
tooltip={{ title: t('refresh-data') }} // "Refresh data"
/>
// ❌ WRONG - Noun-based, not action-oriented
<Button>{t('csv-export')}</Button> // Not "CSV Export"
<Button>{t('file-download')}</Button> // Not "File Download"
<Button>{t('new-item')}</Button> // Not "New Item"
Match your translation keys to the final output (sentence case, verb-first):
# ✅ CORRECT - Translation keys in studio.en.yaml
toolbar.save: Save
toolbar.save-draft: Save draft
toolbar.export-csv: Export CSV
toolbar.create-new: Create new
modal.confirm-delete: Confirm delete
modal.review-changes: Review changes
# ❌ WRONG - Don't use title case or noun-first
# toolbar.save-draft: Save Draft
# toolbar.csv-export: CSV Export
| Element | Case | Example | Translation Key |
|---|---|---|---|
| Buttons | Sentence case | "Save draft" | save-draft |
| Toolbar actions | Sentence case | "Export CSV" | export-csv |
| Modal buttons | Sentence case | "Confirm delete" | confirm-delete |
| Icon tooltips | Sentence case | "Refresh data" | refresh-data |
Remember:
For navigation labels (Title Case), see the pimcore-studio-ui-navigation skill.
Standard button with text, the most common button type.
type?: 'primary' | 'default' | 'link' | 'text' | 'action'
color?: 'default' | 'primary' | 'secondary' | 'danger'
import { Button } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const MyComponent = (): React.JSX.Element => {
const { t } = useTranslation()
return (
<Button
onClick={ handleSave }
type="primary"
>
{t('toolbar.save')}
</Button>
)
}
interface ButtonProps {
type?: 'primary' | 'default' | 'link' | 'text' | 'action'
color?: 'default' | 'primary' | 'secondary' | 'danger'
loading?: boolean // Shows spinner, locks width
disabled?: boolean
onClick?: () => void
children: React.ReactNode // Button text content
className?: string
htmlType?: 'button' | 'submit' | 'reset'
// ... extends all Ant Design ButtonProps
}
Buttons support loading states with a spinner. The button width is locked during loading to prevent layout shift:
const [updateAsset, { isLoading }] = useAssetUpdateByIdMutation()
return (
<Button
disabled={ isLoading }
loading={ isLoading }
onClick={ handleSave }
type="primary"
>
{t('toolbar.save-and-publish')}
</Button>
)
// File: asset/editor/toolbar/save-button/save-button.tsx
import { Button } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
import { checkElementPermission } from '@pimcore/studio-ui-bundle/modules/element'
export const EditorToolbarSaveButton = (): React.JSX.Element => {
const { t } = useTranslation()
const { asset } = useAssetDraft(id)
const [saveAsset, { isLoading }] = useAssetUpdateByIdMutation()
return (
<>
{checkElementPermission(asset?.permissions, 'publish') && (
<Button
disabled={ isLoading }
loading={ isLoading }
onClick={ onSaveClick }
type="primary"
>
{t(asset?.type === 'folder' ? 'toolbar.save' : 'toolbar.save-and-publish')}
</Button>
)}
</>
)
}
Icon-only buttons for toolbars and compact UIs.
IconButtons MUST always have tooltips for accessibility!
// ❌ DON'T - No tooltip
<IconButton
icon={ { value: 'delete' } }
onClick={ handleDelete }
/>
// ✅ DO - Always include tooltip
<IconButton
icon={ { value: 'delete' } }
onClick={ handleDelete }
tooltip={ { title: t('delete') } }
/>
interface IconButtonProps {
icon: IconProps // Icon configuration (required)
tooltip?: TooltipProps // ALWAYS provide this!
theme?: 'primary' | 'secondary' // Visual theme
variant?: 'minimal' | 'static' // Style variant
size?: 'small' | 'middle' | 'large'
hideShadow?: boolean // Hide box shadow
type?: ButtonProps['type'] // Button type (default: 'link')
onClick?: () => void
disabled?: boolean
loading?: boolean
}
icon={ {
value: 'icon-name', // Icon name from icon library
options: {
width: 16, // Optional size override
height: 16
}
} }
Common icons: 'refresh', 'delete', 'edit', 'plus', 'close', 'save', 'download', 'upload', 'chevron-down', 'chevron-right'
// Primary theme - blue hover
<IconButton
icon={ { value: 'edit' } }
theme="primary"
tooltip={ { title: t('edit') } }
/>
// Secondary theme - gray hover
<IconButton
icon={ { value: 'close' } }
theme="secondary"
tooltip={ { title: t('close') } }
/>
import { IconButton } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const TreeContainer = (): React.JSX.Element => {
const { t } = useTranslation()
const { refetch, isLoading } = useQuery()
return (
<Toolbar>
<IconButton
icon={ { value: 'refresh' } }
loading={ isLoading }
onClick={ async () => await refetch() }
tooltip={ { title: t('refresh') } }
/>
</Toolbar>
)
}
// Small icon button (14px icon)
<IconButton
icon={ { value: 'delete' } }
size="small"
tooltip={ { title: t('delete') } }
/>
// Medium/default (16px icon)
<IconButton
icon={ { value: 'delete' } }
tooltip={ { title: t('delete') } }
/>
// Large (custom size via icon options)
<IconButton
icon={ {
value: 'delete',
options: { width: 24, height: 24 }
} }
tooltip={ { title: t('delete') } }
/>
Buttons that combine an icon with text. Perfect for toolbar actions that need both visual and textual clarity.
interface IconTextButtonProps extends Omit<ButtonProps, 'icon'> {
icon: IconProps // Icon configuration (required)
iconPlacement?: 'left' | 'right' // Icon position (default: 'left')
children: React.ReactNode // Button text
}
import { IconTextButton } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const MyToolbar = (): React.JSX.Element => {
const { t } = useTranslation()
return (
<IconTextButton
icon={ { value: 'plus' } }
onClick={ handleCreate }
type="primary"
>
{t('toolbar.create')}
</IconTextButton>
)
}
// Icon on the left (default)
<IconTextButton
icon={ { value: 'save' } }
>
{t('save')}
</IconTextButton>
// Icon on the right
<IconTextButton
icon={ { value: 'chevron-down' } }
iconPlacement="right"
>
{t('more-options')}
</IconTextButton>
// File: widget-editor/components/tree/tree-container.tsx
<Toolbar justify="space-between">
<IconButton
icon={ { value: 'refresh' } }
onClick={ refetch }
tooltip={ { title: t('refresh') } }
/>
<IconTextButton
icon={ { value: 'new' } }
loading={ isLoading }
onClick={ createWidget }
>
{t('toolbar.new')}
</IconTextButton>
</Toolbar>
// Primary action
<IconTextButton
icon={ { value: 'save' } }
type="primary"
>
{t('save')}
</IconTextButton>
// Link style
<IconTextButton
icon={ { value: 'download' } }
type="link"
>
{t('download')}
</IconTextButton>
// Danger action
<IconTextButton
color="danger"
icon={ { value: 'delete' } }
type="primary"
>
{t('delete')}
</IconTextButton>
Button with a dropdown chevron icon on the right. Commonly used for dropdown menus and "More" actions.
interface DropdownButtonProps extends Omit<IconTextButtonProps, 'icon'> {
icon?: IconProps // Optional custom icon (chevron-down is default)
children: React.ReactNode
}
import { DropdownButton } from '@pimcore/studio-ui-bundle/components'
import { Dropdown } from 'antd'
export const ActionsDropdown = (): React.JSX.Element => {
const { t } = useTranslation()
const items = [
{ key: 'edit', label: t('edit') },
{ key: 'delete', label: t('delete'), danger: true }
]
return (
<Dropdown menu={ { items } }>
<DropdownButton>
{t('more-actions')}
</DropdownButton>
</Dropdown>
)
}
// Override the default chevron-down
<DropdownButton
icon={ { value: 'settings' } }
>
{t('settings')}
</DropdownButton>
import { DropdownButton } from '@pimcore/studio-ui-bundle/components'
import { Dropdown } from 'antd'
import type { MenuProps } from 'antd'
export const BatchActionsDropdown = (): React.JSX.Element => {
const { t } = useTranslation()
const selectedItems = useSelectedItems()
const menuItems: MenuProps['items'] = [
{
key: 'export',
label: t('batch.export'),
icon: <Icon value="download" />
},
{
key: 'delete',
label: t('batch.delete'),
icon: <Icon value="delete" />,
danger: true
}
]
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
switch (key) {
case 'export':
handleExport(selectedItems)
break
case 'delete':
handleDelete(selectedItems)
break
}
}
return (
<Dropdown
disabled={ selectedItems.length === 0 }
menu={ { items: menuItems, onClick: handleMenuClick } }
>
<DropdownButton disabled={ selectedItems.length === 0 }>
{t('batch-actions')} ({selectedItems.length})
</DropdownButton>
</Dropdown>
)
}
Groups multiple related buttons together with consistent spacing.
interface ButtonGroupProps {
items: ReactElement[] // Array of button elements
withSeparator?: boolean // Add visual separator between buttons
noSpacing?: boolean // Use Ant Design's compact group style
}
import { ButtonGroup, Button, IconButton } from '@pimcore/studio-ui-bundle/components'
export const EditorToolbar = (): React.JSX.Element => {
const { t } = useTranslation()
return (
<ButtonGroup
items={ [
<Button
key="save"
onClick={ handleSave }
type="primary"
>
{t('save')}
</Button>,
<Button
key="cancel"
onClick={ handleCancel }
>
{t('cancel')}
</Button>
] }
/>
)
}
// Add visual separators between button groups
<ButtonGroup
items={ [
<IconButton
icon={ { value: 'undo' } }
key="undo"
onClick={ handleUndo }
tooltip={ { title: t('undo') } }
/>,
<IconButton
icon={ { value: 'redo' } }
key="redo"
onClick={ handleRedo }
tooltip={ { title: t('redo') } }
/>
] }
withSeparator
/>
// Use Ant Design's compact button group (no gaps)
<ButtonGroup
items={ [
<IconButton
icon={ { value: 'align-left' } }
key="left"
tooltip={ { title: t('align-left') } }
/>,
<IconButton
icon={ { value: 'align-center' } }
key="center"
tooltip={ { title: t('align-center') } }
/>,
<IconButton
icon={ { value: 'align-right' } }
key="right"
tooltip={ { title: t('align-right') } }
/>
] }
noSpacing
/>
import { ButtonGroup, Button, IconButton } from '@pimcore/studio-ui-bundle/components'
export const FormActions = (): React.JSX.Element => {
const { t } = useTranslation()
// Group 1: Main actions
const mainActions = (
<ButtonGroup
items={ [
<Button
key="save"
loading={ isSaving }
onClick={ handleSave }
type="primary"
>
{t('save')}
</Button>,
<Button
key="cancel"
onClick={ handleCancel }
>
{t('cancel')}
</Button>
] }
/>
)
// Group 2: Secondary actions
const secondaryActions = (
<ButtonGroup
items={ [
<IconButton
icon={ { value: 'delete' } }
key="delete"
onClick={ handleDelete }
tooltip={ { title: t('delete') } }
/>,
<IconButton
icon={ { value: 'duplicate' } }
key="duplicate"
onClick={ handleDuplicate }
tooltip={ { title: t('duplicate') } }
/>
] }
withSeparator
/>
)
return (
<Toolbar justify="space-between">
{mainActions}
{secondaryActions}
</Toolbar>
)
}
<ButtonGroup
items={ [
<Button
disabled={ isLoading }
key="save"
loading={ isLoading }
onClick={ handleSave }
type="primary"
>
{t('save')}
</Button>,
<Button
disabled={ isLoading }
key="cancel"
onClick={ handleCancel }
>
{t('cancel')}
</Button>
] }
/>
<Toolbar>
<IconButton
icon={ { value: 'refresh' } }
loading={ isLoading }
onClick={ refetch }
tooltip={ { title: t('refresh') } }
/>
<IconButton
icon={ { value: 'settings' } }
onClick={ openSettings }
tooltip={ { title: t('settings') } }
/>
<IconTextButton
icon={ { value: 'plus' } }
onClick={ handleCreate }
type="primary"
>
{t('create-new')}
</IconTextButton>
</Toolbar>
// Destructive action with danger color
<Button
color="danger"
onClick={ showDeleteConfirmation }
type="primary"
>
{t('delete')}
</Button>
// Inside modal confirmation
<ButtonGroup
items={ [
<Button
key="confirm"
loading={ isDeleting }
onClick={ handleConfirmDelete }
type="primary"
color="danger"
>
{t('confirm-delete')}
</Button>,
<Button
disabled={ isDeleting }
key="cancel"
onClick={ closeModal }
>
{t('cancel')}
</Button>
] }
/>
import { checkElementPermission } from '@pimcore/studio-ui-bundle/modules/element'
// Only show button if user has permission
{checkElementPermission(element?.permissions, 'publish') && (
<Button
onClick={ handlePublish }
type="primary"
>
{t('publish')}
</Button>
)}
// Disable button if no permission
<Button
disabled={ !checkElementPermission(element?.permissions, 'delete') }
onClick={ handleDelete }
color="danger"
>
{t('delete')}
</Button>
const [saveAsset, { isLoading: isSaving }] = useAssetUpdateByIdMutation()
const { saveSchedules, isLoading: isSavingSchedules } = useSaveSchedules()
const isLoading = isSaving || isSavingSchedules
return (
<Button
disabled={ isLoading }
loading={ isLoading }
onClick={ handleSave }
type="primary"
>
{t('save')}
</Button>
)
<Button> when:<IconButton> when:<IconTextButton> when:<DropdownButton> when:<ButtonGroup> when:// DON'T - No tooltip makes it inaccessible
<IconButton
icon={ { value: 'delete' } }
onClick={ handleDelete }
/>
// DO - Always include tooltip
<IconButton
icon={ { value: 'delete' } }
onClick={ handleDelete }
tooltip={ { title: t('delete') } }
/>
// DON'T - Using primary for cancel
<Button
onClick={ handleCancel }
type="primary"
>
{t('cancel')}
</Button>
// DO - Use default for secondary actions
<Button
onClick={ handleCancel }
>
{t('cancel')}
</Button>
// DON'T - No loading/disabled state
<Button onClick={ handleSave }>
{t('save')}
</Button>
// DO - Show loading and disable during operation
<Button
disabled={ isLoading }
loading={ isLoading }
onClick={ handleSave }
>
{t('save')}
</Button>
// DON'T - Hardcoded text
<Button onClick={ handleSave }>
Save
</Button>
// DO - Always use i18n
<Button onClick={ handleSave }>
{t('save')}
</Button>
// DON'T - No keys for array items
<ButtonGroup
items={ [
<Button onClick={ handleSave }>Save</Button>,
<Button onClick={ handleCancel }>Cancel</Button>
] }
/>
// DO - Always provide keys
<ButtonGroup
items={ [
<Button key="save" onClick={ handleSave }>{t('save')}</Button>,
<Button key="cancel" onClick={ handleCancel }>{t('cancel')}</Button>
] }
/>
| Component | Use Case | Always Include |
|---|---|---|
Button | Text actions, forms, modals | Translation |
IconButton | Compact toolbar actions | Tooltip + Translation |
IconTextButton | Primary toolbar actions | Icon + Translation |
DropdownButton | Menu triggers | Translation |
ButtonGroup | Related action groups | Keys for items |
| Type | Appearance | Use For |
|---|---|---|
primary | Blue filled | Main/primary actions |
default | Gray border | Secondary actions |
link | Text with hover | Tertiary/inline actions |
text | Plain text | Minimal emphasis actions |
| Color | Use For |
|---|---|
primary | Positive/main actions |
secondary | Alternative actions |
danger | Destructive actions (delete, remove) |