| name | pimcore-studio-ui-modals |
| description | Using modal dialogs in Pimcore Studio UI - declarative Modal and WindowModal components, plus imperative useFormModal, useAlertModal, useStudioModal hooks |
| metadata | {"audience":"pimcore-developers","focus":"ui-components-modals"} |
What This Skill Covers
How to use modal dialogs in Pimcore Studio UI:
- Modal Component - Declarative modal with open/onCancel props (preferred for complex UIs)
- WindowModal Component - Window-style modals with draggable, resizable behavior
- useFormModal - Imperative confirmation modals, input forms, textarea forms, upload forms
- useAlertModal - Imperative info, error, warning, and success alerts
- useStudioModal - Base modal functionality (works across iframes)
- When to use declarative vs imperative modals
- Modal types and patterns
When to Use This Skill
Use this when:
- Creating modals with complex state management (use
<Modal>)
- Building draggable/resizable window-style modals (use
<WindowModal>)
- Confirming destructive actions (use
useFormModal.confirm())
- Collecting user input via modals
- Showing alerts or informational dialogs
- Creating forms in modal dialogs
- Uploading files via modal
🚨 Import Paths
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.
Choosing the Right Modal Approach
Declarative Components (<Modal>, <WindowModal>)
Use declarative components when:
- Modal has complex state management (forms with validation, multi-step workflows)
- Need full control over modal lifecycle
- Modal content changes based on component state
- Building custom modal layouts or behaviors
- Need to integrate with React hooks (useForm, useState, etc.)
Example use cases:
- Edit forms with RTK Query data
- Multi-step wizards
- Complex configuration dialogs
- Draggable/resizable windows
Imperative Hooks (useFormModal, useAlertModal)
Use imperative hooks when:
- Simple confirmations (delete, yes/no)
- Quick user input (single field, textarea)
- Alert messages (success, error, info)
- Fire-and-forget interactions
Example use cases:
- "Are you sure you want to delete?"
- "Enter a name for this item"
- "Success! Item saved"
Declarative Modal Component
The <Modal> component provides full control over modal behavior using React state.
Basic Modal Usage
import { useState } from 'react'
import { Modal, Button } from '@pimcore/studio-ui-bundle/components'
export const MyComponent = (): React.JSX.Element => {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<Button onClick={() => setIsOpen(true)}>
Open modal
</Button>
<Modal
open={isOpen}
onCancel={() => setIsOpen(false)}
title="My Modal"
footer={[
<Button key="cancel" onClick={() => setIsOpen(false)}>
Cancel
</Button>,
<Button
key="submit"
type="primary"
onClick={() => {
// Handle submit
setIsOpen(false)
}}
>
Submit
</Button>
]}
>
<p>Modal content goes here</p>
</Modal>
</>
)
}
Modal with Form and RTK Query
Common pattern: Edit modal with data fetching and mutation
import { useState, useEffect } from 'react'
import { Modal, Button, FormKit, Form, Input, Skeleton } from '@pimcore/studio-ui-bundle/components'
import { useAssetGetByIdQuery, useAssetUpdateMutation } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
export const EditAssetModal = ({ assetId, onClose }: Props): React.JSX.Element => {
const [isOpen, setIsOpen] = useState(true)
const [form] = Form.useForm()
const { data, isLoading, error: fetchError } = useAssetGetByIdQuery({ id: assetId })
const [updateAsset, { isLoading: isUpdating, data: updateData, error: updateError }] = useAssetUpdateMutation()
useEffect(() => {
if (!isNil(fetchError)) {
trackError(new ApiError(fetchError))
}
}, [fetchError])
useEffect(() => {
if (!isNil(updateError)) {
trackError(new ApiError(updateError))
}
}, [updateError])
useEffect(() => {
if (!isNil(updateData)) {
setIsOpen(false)
onClose()
}
}, [updateData])
useEffect(() => {
if (!isNil(data)) {
form.setFieldsValue({
filename: data.filename,
title: data.metadata?.title ?? ''
})
}
}, [data, form])
const handleSubmit = (values: FormValues): void => {
updateAsset({
id: assetId,
body: {
filename: values.filename,
metadata: { title: values.title }
}
})
}
const handleCancel = (): void => {
setIsOpen(false)
onClose()
}
return (
<Modal
open={isOpen}
onCancel={handleCancel}
title="Edit asset"
footer={[
<Button key="cancel" onClick={handleCancel}>
Cancel
</Button>,
<Button
key="submit"
type="primary"
loading={isUpdating}
onClick={() => form.submit()}
>
Save
</Button>
]}
>
{isLoading ? (
<Skeleton />
) : (
<FormKit formProps={{ form, onFinish: handleSubmit }}>
<Form.Item name="filename" label="Filename" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="title" label="Title">
<Input />
</Form.Item>
</FormKit>
)}
</Modal>
)
}
Modal Props
interface ModalProps {
open: boolean
onCancel?: () => void
onOk?: () => void
title?: React.ReactNode
width?: number | string
footer?: React.ReactNode
closable?: boolean
maskClosable?: boolean
destroyOnClose?: boolean
centered?: boolean
}
WindowModal Component
The <WindowModal> component provides draggable, resizable window-style modals.
Basic WindowModal Usage
import { useState } from 'react'
import { WindowModal, Button } from '@pimcore/studio-ui-bundle/components'
export const MyComponent = (): React.JSX.Element => {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<Button onClick={() => setIsOpen(true)}>
Open window
</Button>
<WindowModal
open={isOpen}
onClose={() => setIsOpen(false)}
title="Draggable Window"
width={800}
height={600}
>
<div>
<h3>Window content</h3>
<p>This modal can be dragged and resized!</p>
</div>
</WindowModal>
</>
)
}
WindowModal with Tabs
Common pattern: Multi-section window
import { useState } from 'react'
import { WindowModal, Tabs } from '@pimcore/studio-ui-bundle/components'
import type { TabsProps } from '@pimcore/studio-ui-bundle/components'
export const SettingsWindow = ({ onClose }: Props): React.JSX.Element => {
const [isOpen, setIsOpen] = useState(true)
const items: TabsProps['items'] = [
{
key: 'general',
label: 'General',
children: <GeneralSettings />
},
{
key: 'advanced',
label: 'Advanced',
children: <AdvancedSettings />
}
]
return (
<WindowModal
open={isOpen}
onClose={() => {
setIsOpen(false)
onClose()
}}
title="Settings"
width={900}
height={700}
resizable
draggable
>
<Tabs items={items} />
</WindowModal>
)
}
WindowModal Props
interface WindowModalProps {
open: boolean
onClose?: () => void
title?: React.ReactNode
width?: number
height?: number
resizable?: boolean
draggable?: boolean
centered?: boolean
destroyOnClose?: boolean
}
Imperative Modals (Hooks)
For simple interactions, use imperative hooks instead of declarative components.
useFormModal Hook
The useFormModal hook provides modal dialogs with form functionality. The most commonly used is the confirm modal for user confirmations.
🚨 MOST IMPORTANT: Confirmation Modal
The confirm() method is the most frequently used modal type! Use it for:
- Confirming destructive actions (delete, remove, discard changes)
- Yes/No questions requiring explicit user confirmation
- Any action that needs user acknowledgment before proceeding
Modal Types Available
interface UseFormModalHookResponse {
confirm: (props) => { destroy: () => void, update: (config) => void }
input: (props) => { destroy: () => void, update: (config) => void }
textarea: (props) => { destroy: () => void, update: (config) => void }
upload: (props) => { destroy: () => void, update: (config) => void }
}
Confirmation Modal (Most Common Use Case)
Basic Confirmation Modal
This is the most common pattern in Pimcore Studio UI:
import { useFormModal } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const MyComponent = () => {
const modal = useFormModal()
const { t } = useTranslation()
const handleDelete = () => {
modal.confirm({
title: t('delete.confirmation.title'),
content: t('delete.confirmation.text'),
okText: t('delete.confirmation.ok'),
cancelText: t('cancel'),
onOk: async () => {
await deleteItem()
}
})
}
return (
<Button
color="danger"
onClick={ handleDelete }
>
{t('delete')}
</Button>
)
}
Confirmation Modal Props
interface ConfirmFormModalProps {
title?: string | React.ReactNode
content?: string | React.ReactNode
okText?: string
cancelText?: string
onOk?: () => void | Promise<void>
onCancel?: () => void
dontAskAgainKey?: string
type?: 'info' | 'success' | 'error' | 'warning' | 'confirm'
icon?: React.ReactNode
okButtonProps?: ButtonProps
cancelButtonProps?: ButtonProps
width?: number | string
}
Real-World Example: Delete Confirmation
import { useFormModal } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const useWidgetEditor = () => {
const modal = useFormModal()
const { t } = useTranslation()
const [deleteWidget] = useWidgetDeleteMutation()
const removeWithConfirmation = (widgetId: string, onFinish?: () => void) => {
modal.confirm({
title: t('element.delete.confirmation.title'),
content: <span>{t('element.delete.confirmation.text')}</span>,
okText: t('element.delete.confirmation.ok'),
onOk: async () => {
await deleteWidget({ widgetId })
onFinish?.()
}
})
}
return { removeWithConfirmation }
}
Confirmation with "Don't Ask Again"
Use dontAskAgainKey to add a "Don't ask again" checkbox that stores the user's preference:
modal.confirm({
title: t('close.confirmation.title'),
content: t('close.confirmation.text'),
okText: t('yes'),
cancelText: t('no'),
dontAskAgainKey: 'close-without-saving-confirmation',
onOk: async () => {
await closeWithoutSaving()
}
})
Common Confirmation Modal Patterns
Pattern: Delete Action
modal.confirm({
title: t('delete.confirmation.title'),
content: t('delete.confirmation.text'),
okText: t('delete'),
okButtonProps: { color: 'danger' },
onOk: async () => {
await deleteItem()
await messageApi.success(t('delete.success'))
}
})
Pattern: Unsaved Changes
modal.confirm({
title: t('unsaved-changes.title'),
content: t('unsaved-changes.text'),
okText: t('discard-changes'),
cancelText: t('keep-editing'),
dontAskAgainKey: 'close-without-saving',
onOk: () => {
closeWithoutSaving()
}
})
Pattern: Publish/Unpublish
modal.confirm({
title: t('publish.confirmation.title'),
content: t('publish.confirmation.text'),
okText: t('publish'),
onOk: async () => {
await publishItem()
await messageApi.success(t('publish.success'))
}
})
Pattern: Generic Confirmation
modal.confirm({
title: t('confirm.title'),
content: t('confirm.text'),
okText: t('yes'),
cancelText: t('no'),
onOk: async () => {
await performAction()
}
})
Input Modal
Displays a modal with a single text input field.
Basic Input Modal
const modal = useFormModal()
const { t } = useTranslation()
const handleRename = () => {
modal.input({
title: t('rename.title'),
label: t('rename.label'),
initialValue: currentName,
okText: t('save'),
cancelText: t('cancel'),
onOk: async (newName: string) => {
await updateName(newName)
}
})
}
Input Modal Props
interface InputFormModalProps {
title?: string
label?: string
initialValue?: string
rule?: Rule
okText?: string
cancelText?: string
onOk?: (value: string) => void | Promise<void>
}
Input Modal with Validation
modal.input({
title: t('create-folder.title'),
label: t('create-folder.label'),
rule: {
required: true,
message: t('create-folder.validation.required')
},
okText: t('create'),
onOk: async (folderName: string) => {
await createFolder(folderName)
}
})
Real-World Example: Rename Widget
const { input } = useFormModal()
const { t } = useTranslation()
const handleRenameWidget = (widgetId: string, currentName: string) => {
input({
title: t('widget.rename.title'),
label: t('widget.rename.label'),
initialValue: currentName,
rule: {
required: true,
message: t('widget.rename.validation.required')
},
onOk: async (newName: string) => {
await updateWidget(widgetId, { name: newName })
}
})
}
Textarea Modal
Displays a modal with a multi-line textarea input.
Basic Textarea Modal
const modal = useFormModal()
const { t } = useTranslation()
const handleEditDescription = () => {
modal.textarea({
title: t('edit-description.title'),
label: t('edit-description.label'),
initialValue: currentDescription,
placeholder: t('edit-description.placeholder'),
okText: t('save'),
onOk: async (newDescription: string) => {
await updateDescription(newDescription)
}
})
}
Textarea Modal Props
interface TextareaFormModalProps {
title?: string
label?: string
initialValue?: string
placeholder?: string
okText?: string
cancelText?: string
onOk?: (value: string) => void | Promise<void>
}
Real-World Example: Edit Notes
const modal = useFormModal()
const { t } = useTranslation()
const handleEditNotes = (currentNotes: string) => {
modal.textarea({
title: t('notes.edit.title'),
label: t('notes.edit.label'),
initialValue: currentNotes,
placeholder: t('notes.edit.placeholder'),
onOk: async (newNotes: string) => {
await saveNotes(newNotes)
await messageApi.success(t('notes.save.success'))
}
})
}
Upload Modal
Displays a modal with a file upload input.
Basic Upload Modal
const modal = useFormModal()
const { t } = useTranslation()
const handleUploadFile = () => {
modal.upload({
title: t('upload.title'),
label: t('upload.label'),
accept: '.csv,.xlsx',
okText: t('upload'),
onOk: async (files: FileList) => {
await uploadFiles(files)
}
})
}
Upload Modal Props
interface UploadFormModalProps {
title?: string
label?: string
accept?: string
rule?: Rule
okText?: string
cancelText?: string
onOk?: (files: FileList) => void | Promise<void>
}
Upload with Validation
modal.upload({
title: t('import.title'),
label: t('import.file-label'),
accept: '.csv',
rule: {
required: true,
message: t('import.validation.file-required')
},
onOk: async (files: FileList) => {
if (files.length > 0) {
await importData(files[0])
await messageApi.success(t('import.success'))
}
}
})
useAlertModal Hook
The useAlertModal hook provides simple alert-style modals for info, success, error, and warning messages.
Alert Types
interface UseAlertModalResponse {
info: (props) => { destroy: () => void, update: (config) => void }
success: (props) => { destroy: () => void, update: (config) => void }
error: (props) => { destroy: () => void, update: (config) => void }
warn: (props) => { destroy: () => void, update: (config) => void }
}
Basic Alert Usage
import { useAlertModal } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const MyComponent = () => {
const modal = useAlertModal()
const { t } = useTranslation()
const showInfo = () => {
modal.info({
title: 'information',
content: t('app.loading-info')
})
}
const showError = () => {
modal.error({
title: 'error',
content: t('app.initialization-failed')
})
}
const showSuccess = () => {
modal.success({
content: t('operation.completed')
})
}
const showWarning = () => {
modal.warn({
content: t('unsaved-changes.warning')
})
}
return (
)
}
Alert Modal Props
interface IAlertModalProps {
title?: string
content: string | React.ReactNode
okText?: string
onOk?: () => void
width?: number | string
}
Real-World Example: App Initialization Error
import { useAlertModal } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const AppLoader = () => {
const modal = useAlertModal()
const { t } = useTranslation()
useEffect(() => {
const initializeApp = async () => {
try {
await loadAppData()
} catch (error) {
modal.error({
title: 'error',
content: t('app.initialization-failed'),
onOk: () => {
window.location.reload()
}
})
}
}
initializeApp()
}, [])
return <div>Loading...</div>
}
useStudioModal Hook
The useStudioModal hook provides the base modal functionality. It works seamlessly across iframe boundaries.
Basic Usage
import { useStudioModal } from '@pimcore/studio-ui-bundle/components'
export const MyComponent = () => {
const { modal, localModal } = useStudioModal()
const showConfirmation = () => {
modal.confirm({
title: 'Confirm Action',
content: 'Are you sure?',
onOk: () => {
}
})
}
return <Button onClick={ showConfirmation }>Show Modal</Button>
}
StudioModal Response
interface StudioModalResponse {
modal: ModalStaticFunctions
localModal: ModalStaticFunctions
}
When to Use
- Use
modal for most cases (works across iframes automatically)
- Use
localModal when you specifically need the modal in the current window only
Modal Control Methods
All modal hooks return methods to control the modal:
Destroy Modal
const modalInstance = modal.confirm({
title: t('confirm.title'),
content: t('confirm.text'),
onOk: handleConfirm
})
modalInstance.destroy()
Update Modal
const modalInstance = modal.confirm({
title: t('processing.title'),
content: t('processing.text'),
okButtonProps: { loading: false }
})
modalInstance.update({
okButtonProps: { loading: true }
})
setTimeout(() => {
modalInstance.update({
content: t('processing.complete'),
okButtonProps: { loading: false }
})
}, 2000)
Common Patterns
Pattern 1: Delete Confirmation
const modal = useFormModal()
const { t } = useTranslation()
const [deleteItem, { isLoading }] = useDeleteMutation()
const handleDelete = (itemId: number) => {
modal.confirm({
title: t('delete.confirmation.title'),
content: t('delete.confirmation.text'),
okText: t('delete'),
okButtonProps: { color: 'danger' },
onOk: async () => {
await deleteItem({ id: itemId })
}
})
}
Pattern 2: Unsaved Changes Warning
const modal = useFormModal()
const { t } = useTranslation()
const handleClose = (hasUnsavedChanges: boolean) => {
if (!hasUnsavedChanges) {
closeEditor()
return
}
modal.confirm({
title: t('unsaved-changes.title'),
content: t('unsaved-changes.text'),
okText: t('discard-changes'),
cancelText: t('keep-editing'),
dontAskAgainKey: 'close-without-saving',
onOk: () => {
closeEditor()
}
})
}
Pattern 3: Create with Name Input
const modal = useFormModal()
const { t } = useTranslation()
const [createItem] = useCreateMutation()
const handleCreate = () => {
modal.input({
title: t('create-item.title'),
label: t('create-item.name-label'),
rule: {
required: true,
message: t('create-item.validation.name-required'),
pattern: /^[a-zA-Z0-9-_]+$/,
message: t('create-item.validation.name-pattern')
},
onOk: async (name: string) => {
await createItem({ name })
await messageApi.success(t('create-item.success'))
}
})
}
Pattern 4: Multi-Step Modal Updates
const modal = useFormModal()
const { t } = useTranslation()
const handleComplexOperation = async () => {
const modalInstance = modal.confirm({
title: t('operation.title'),
content: t('operation.step-1'),
okText: t('continue'),
cancelText: t('cancel'),
onOk: async () => {
modalInstance.update({
content: t('operation.processing'),
okButtonProps: { loading: true },
cancelButtonProps: { disabled: true }
})
try {
await performStep1()
modalInstance.update({
content: t('operation.step-2'),
okButtonProps: { loading: false },
cancelButtonProps: { disabled: false }
})
await performStep2()
modalInstance.destroy()
await messageApi.success(t('operation.success'))
} catch (error) {
modalInstance.destroy()
await messageApi.error(t('operation.failed'))
}
}
})
}
Pattern 5: Error Alert with Retry
const modal = useAlertModal()
const { t } = useTranslation()
const handleLoadDataWithRetry = async () => {
try {
await loadData()
} catch (error) {
modal.error({
title: 'error',
content: t('data.load-failed'),
okText: t('retry'),
onOk: () => {
handleLoadDataWithRetry()
}
})
}
}
When to Use Each Modal Type
⭐ useFormModal.confirm() - MOST COMMON!
This is the most frequently used modal in Pimcore Studio UI!
Use for:
- Confirming destructive actions (delete, remove, discard) - Most common!
- Yes/No questions requiring explicit user confirmation
- State changes that need confirmation (publish, unpublish)
- Unsaved changes warnings before closing/navigating
- Any action that needs explicit user acknowledgment before proceeding
modal.confirm({
title: t('action.confirmation.title'),
content: t('action.confirmation.text'),
okText: t('confirm'),
onOk: async () => {
await performAction()
}
})
useFormModal.input()
Use for:
- Collecting single-line text input (names, titles)
- Rename operations
- Creating items with a name
useFormModal.textarea()
Use for:
- Collecting multi-line text (descriptions, notes, comments)
- Editing longer text content
useFormModal.upload()
Use for:
- File upload operations
- Import/export with file selection
- Attachment uploads
useAlertModal.info()
Use for:
- Informational messages
- Help or guidance
useAlertModal.success()
Use for:
- Success confirmations (less common, prefer toast messages)
- Operation completed notifications
useAlertModal.error()
Use for:
- Critical errors that block workflow
- Initialization failures
- Errors requiring user acknowledgment
useAlertModal.warn()
Use for:
- Non-blocking warnings
- Important notices
Common Mistakes
❌ Not Using Translations
modal.confirm({
title: 'Delete Item',
content: 'Are you sure?'
})
modal.confirm({
title: t('delete.confirmation.title'),
content: t('delete.confirmation.text')
})
❌ Not Handling Async Operations
modal.confirm({
onOk: () => {
deleteItem()
}
})
modal.confirm({
onOk: async () => {
await deleteItem()
await messageApi.success(t('deleted'))
}
})
❌ Using Modal for Simple Notifications
modal.success({
content: t('saved')
})
messageApi.success(t('saved'))
❌ Not Providing Feedback
modal.confirm({
title: t('delete.title'),
content: t('delete.text'),
onOk: async () => {
await deleteItem()
}
})
modal.confirm({
title: t('delete.title'),
content: t('delete.text'),
onOk: async () => {
await deleteItem()
await messageApi.success(t('delete.success'))
}
})
❌ Missing Validation in Input Modals
modal.input({
title: t('create.title'),
onOk: async (name: string) => {
await create(name)
}
})
modal.input({
title: t('create.title'),
rule: {
required: true,
message: t('validation.name-required')
},
onOk: async (name: string) => {
await create(name)
}
})
Quick Reference
Modal Type Selection
| Need | Use |
|---|
| Confirm action | useFormModal().confirm() |
| Get single-line input | useFormModal().input() |
| Get multi-line input | useFormModal().textarea() |
| Upload file | useFormModal().upload() |
| Show info | useAlertModal().info() or toast |
| Show error | useAlertModal().error() for critical |
| Show success | Toast (not modal) |
| Show warning | useAlertModal().warn() or toast |
Common Modal Configurations
modal.confirm({
title: t('delete.title'),
content: t('delete.text'),
okText: t('delete'),
okButtonProps: { color: 'danger' },
onOk: async () => await delete()
})
modal.input({
title: t('create.title'),
label: t('name.label'),
rule: { required: true, message: t('validation.required') },
onOk: async (name) => await create(name)
})
modal.confirm({
title: t('close.title'),
content: t('close.text'),
dontAskAgainKey: 'close-confirmation',
onOk: () => close()
})
Next Steps