skills/pimcore-studio-ui-notifications-toasts/SKILL.md
Displaying notification messages and toasts in Pimcore Studio UI using the useMessage hook
npx skillsauth add pimcore/skills pimcore-studio-ui-notifications-toastsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
How to display notification messages (toasts) 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.
The useMessage hook provides a message API for displaying toast notifications.
import { useMessage } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const MyComponent = (): React.JSX.Element => {
const messageApi = useMessage()
const { t } = useTranslation()
const handleSave = async (): Promise<void> => {
try {
await saveData()
await messageApi.success(t('save-success'))
} catch (error) {
await messageApi.error({
content: error.message
})
}
}
return (
<Button onClick={ handleSave }>
{t('save')}
</Button>
)
}
interface MessageInstance {
success: (content: string | ArgsProps, duration?, onClose?) => MessageType
error: (content: string | ArgsProps, duration?, onClose?) => MessageType
warning: (content: string | ArgsProps, duration?, onClose?) => MessageType
info: (content: string | ArgsProps, duration?, onClose?) => MessageType
loading: (content: string | ArgsProps, duration?, onClose?) => MessageType
open: (config: ArgsProps) => MessageType
}
Show success messages after successful operations.
// Simple text message
await messageApi.success(t('save-success'))
// With custom duration (seconds)
await messageApi.success(t('save-success'), 5)
// With callback after close
await messageApi.success(t('save-success'), 3, () => {
console.log('Message closed')
})
messageApi.success({
content: t('operation-completed'),
duration: 3, // seconds (0 = never auto-close)
onClose: () => {
// Callback when message closes
}
})
// File: asset/editor/toolbar/save-button/save-button.tsx
import { useMessage } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const EditorToolbarSaveButton = (): React.JSX.Element => {
const { t } = useTranslation()
const messageApi = useMessage()
const [saveAsset, { isSuccess }] = useAssetUpdateByIdMutation()
useEffect(() => {
const handleSuccessEvent = async (): Promise<void> => {
if (isSuccess) {
await messageApi.success(t('save-success'))
}
}
handleSuccessEvent().catch((error) => {
console.error(error)
})
}, [isSuccess])
return (
<Button
onClick={ handleSave }
type="primary"
>
{t('save')}
</Button>
)
}
Display error messages when operations fail.
// Simple text error
await messageApi.error(t('save-failed'))
// With error details
await messageApi.error({
content: t('save-failed')
})
try {
await saveData()
} catch (error) {
await messageApi.error({
content: error.message || t('unknown-error')
})
}
// File: auth/components/login-form/login-form.tsx
import { useMessage } from '@pimcore/studio-ui-bundle/components'
export const LoginForm = (): React.JSX.Element => {
const messageApi = useMessage()
const handleAuthentication = async (event: React.FormEvent): Promise<void> => {
try {
event.preventDefault()
const response = await login({ credentials: formState })
if (response.error !== undefined) {
await messageApi.error({
content: t('login-failed')
})
} else {
dispatch(setAuthState(true))
}
} catch (e: any) {
await messageApi.error({
content: e.message
})
}
}
return (
<form onSubmit={ handleAuthentication }>
{/* form fields */}
</form>
)
}
// File: data-object/editor/providers/edit-form-provider.tsx
useEffect(() => {
if (isError) {
messageApi.error(t('auto-save-failed'))
}
}, [isError])
Display warning messages for non-critical issues.
messageApi.warning(t('unsaved-changes'))
messageApi.warning({
content: t('operation-may-take-time'),
duration: 5
})
const handleDelete = (): void => {
if (hasRelatedItems) {
messageApi.warning({
content: t('item-has-dependencies'),
duration: 0 // Don't auto-close
})
return
}
performDelete()
}
Display informational messages.
messageApi.info(t('loading-data'))
messageApi.info({
content: t('background-task-started'),
duration: 3
})
The useMessage hook automatically adds custom icons for info messages:
// Automatically includes info-circle icon
messageApi.info(t('processing'))
Display loading messages for ongoing operations.
const hideLoading = messageApi.loading(t('saving'))
// Later, hide the message manually
hideLoading()
// Auto-hide after 3 seconds
messageApi.loading(t('processing'), 3)
const handleExport = async (): Promise<void> => {
const hide = messageApi.loading(t('exporting-data'), 0)
try {
await performExport()
hide()
await messageApi.success(t('export-complete'))
} catch (error) {
hide()
await messageApi.error(t('export-failed'))
}
}
interface ArgsProps {
content: React.ReactNode // Message content
duration?: number // Duration in seconds (0 = manual close)
onClose?: VoidFunction // Callback when closed
icon?: React.ReactNode // Custom icon element
key?: string | number // Unique key for the message
className?: string // Custom CSS class
style?: React.CSSProperties // Custom inline styles
onClick?: (e: React.MouseEvent) => void // Click handler
}
// Show for 5 seconds (default is 3)
messageApi.success(t('saved'), 5)
// Never auto-close (must be closed manually)
messageApi.success({
content: t('important-message'),
duration: 0
})
// Close immediately
messageApi.success(t('quick-message'), 0.5)
// Store reference to close manually
const hide = messageApi.success({
content: t('uploading'),
duration: 0
})
// Later, close the message
setTimeout(() => {
hide()
}, 5000)
const handleMultiStepOperation = async (): Promise<void> => {
// Step 1
const loading = messageApi.loading(t('step-1'))
await performStep1()
loading()
// Step 2
const loading2 = messageApi.loading(t('step-2'))
await performStep2()
loading2()
// Final success
await messageApi.success(t('all-steps-complete'))
}
const [saveData, { isLoading, isSuccess, isError, error }] = useSaveMutation()
const messageApi = useMessage()
const { t } = useTranslation()
useEffect(() => {
if (isSuccess) {
void messageApi.success(t('save-success'))
}
}, [isSuccess])
useEffect(() => {
if (isError) {
void messageApi.error(t('save-failed'))
}
}, [isError])
// File: element/actions/copy-paste/tree-copy-paste-context.tsx
const handleCopy = (element: Element): void => {
setCopiedElement({ element, mode: 'copy' })
void messageApi.success(t('element.tree.copy-success-description', {
type: element.type,
path: element.path
}))
}
const handleCut = (element: Element): void => {
setCopiedElement({ element, mode: 'cut' })
void messageApi.success(t('element.tree.cut-success-description', {
type: element.type,
path: element.path
}))
}
// File: dynamic-types/.../hotspot-image/footer.tsx
const handleClearData = async (): Promise<void> => {
clearHotspots()
await messageApi.success(t('hotspots.data-cleared'))
}
const handleBatchDelete = async (items: Item[]): Promise<void> => {
const loading = messageApi.loading(t('deleting-items', { count: items.length }), 0)
try {
const results = await Promise.allSettled(
items.map(item => deleteItem(item.id))
)
loading()
const succeeded = results.filter(r => r.status === 'fulfilled').length
const failed = results.filter(r => r.status === 'rejected').length
if (failed === 0) {
await messageApi.success(t('batch-delete-success', { count: succeeded }))
} else {
await messageApi.warning(t('batch-delete-partial', {
succeeded,
failed
}))
}
} catch (error) {
loading()
await messageApi.error(t('batch-delete-failed'))
}
}
// File: tags/hooks/use-tag-config.tsx
const handleCreateTag = async (): Promise<void> => {
const result = await createTag(tagData)
if (result.success) {
messageApi.success({
content: t('tag.created', { name: tagData.name }),
duration: 3
})
}
}
const handleUpdateTag = async (): Promise<void> => {
const result = await updateTag(tagData)
if (result.success) {
messageApi.success({
content: t('tag.updated'),
duration: 2
})
}
}
const handleDeleteTag = async (): Promise<void> => {
const result = await deleteTag(tagId)
if (result.success) {
messageApi.success({
content: t('tag.deleted'),
duration: 2
})
}
}
| Type | Use Case | Default Duration | Auto Icon |
|------|----------|------------------|-----------|
| success | Successful operations | 3s | ✓ (checkmark) |
| error | Failed operations | 3s | ✓ (error) |
| warning | Non-critical issues | 3s | ✓ (warning) |
| info | Information | 3s | ✓ (info-circle, custom) |
| loading | Ongoing operations | Manual | ✓ (spinner) |
// DON'T - Hardcoded text
messageApi.success('Saved successfully')
// DO - Use i18n
messageApi.success(t('save-success'))
// DON'T - No error feedback
const handleSave = async (): Promise<void> => {
await saveData()
messageApi.success(t('saved'))
}
// DO - Handle errors
const handleSave = async (): Promise<void> => {
try {
await saveData()
await messageApi.success(t('saved'))
} catch (error) {
await messageApi.error(t('save-failed'))
}
}
// DON'T - Not awaiting (may cause timing issues)
messageApi.success(t('saved'))
doNextThing()
// DO - Await message
await messageApi.success(t('saved'))
doNextThing()
// DON'T - User can't see console
console.log('Data saved')
// DO - Show user-visible message
messageApi.success(t('save-success'))
// DON'T - Spam the user
items.forEach(item => {
messageApi.success(t('item-saved', { name: item.name }))
})
// DO - Single summary message
messageApi.success(t('items-saved', { count: items.length }))
// DON'T - Using error for non-errors
if (items.length === 0) {
messageApi.error(t('no-items')) // Not an error!
}
// DO - Use info or warning
if (items.length === 0) {
messageApi.info(t('no-items'))
}
// For any user action, provide feedback
const handleAction = async (): Promise<void> => {
try {
await performAction()
await messageApi.success(t('action-complete')) // Always confirm
} catch (error) {
await messageApi.error(t('action-failed')) // Always report errors
}
}
// Success: Completed operations
messageApi.success(t('saved'))
// Error: Failed operations
messageApi.error(t('save-failed'))
// Warning: Potential issues
messageApi.warning(t('unsaved-changes'))
// Info: Informational updates
messageApi.info(t('data-loading'))
// Loading: Ongoing operations
messageApi.loading(t('processing'))
// DO - Short and clear
messageApi.success(t('saved'))
// AVOID - Too verbose
messageApi.success(t('your-changes-have-been-successfully-saved-to-the-database'))
// Include dynamic data in messages
messageApi.success(t('item-created', { name: item.name }))
messageApi.warning(t('items-remaining', { count: remaining }))
// Show loading, then result
const handleExport = async (): Promise<void> => {
const hide = messageApi.loading(t('exporting'), 0)
try {
const result = await exportData()
hide()
await messageApi.success(t('export-complete'))
} catch (error) {
hide()
await messageApi.error(t('export-failed'))
}
}
// Simple success
await messageApi.success(t('saved'))
// Simple error
await messageApi.error(t('failed'))
// With duration
messageApi.success(t('saved'), 5)
// Never auto-close
messageApi.warning({
content: t('warning'),
duration: 0
})
// Loading (manual close)
const hide = messageApi.loading(t('loading'), 0)
hide() // Close it later
// With interpolation
messageApi.success(t('created', { name: item.name }))
tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality