skills/pimcore-studio-ui-widgets/SKILL.md
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
npx skillsauth add pimcore/skills pimcore-studio-ui-widgetsInstall 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.
Working with the Pimcore Studio UI widget system: registering widgets, opening them in layout areas (main/left/right/bottom), configuring tabs via WidgetManagerTabConfig, and connecting widgets to main navigation.
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
Examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Widgets must be registered before they can be opened. Registration happens during module initialization via the WidgetRegistry service.
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
import { container, serviceIds } from '@pimcore/studio-ui-bundle/app'
import { MyWidgetComponent } from './my-widget-component'
const widgetRegistryService = container.get<WidgetRegistry>(serviceIds.widgetManager)
widgetRegistryService.registerWidget({
name: 'my-widget',
component: MyWidgetComponent
})
name is the identifier used later in WidgetManagerTabConfig.componentcomponent must be a React component (named export, not default)onInit before any code tries to open the widgetinterface Widget {
name: string
component: ComponentType
titleComponent?: ComponentType
contentTitleComponent?: ComponentType
isModified?: (tabNode: TabNode) => boolean
getContextProvider?: (context, children) => React.JSX.Element
defaultGlobalContext?: boolean
transformConfig?: (config) => config
isVisible?: (widget: WidgetConfig) => boolean
}
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| name | string | Yes | Unique identifier for the widget |
| component | ComponentType | Yes | React component rendered as the widget body |
| titleComponent | ComponentType | No | Custom component for the tab title |
| contentTitleComponent | ComponentType | No | Custom component for the content area title bar |
| isModified | (tabNode) => boolean | No | Whether the widget has unsaved changes (shows tab indicator) |
| getContextProvider | (context, children) => JSX.Element | No | Wraps the widget in a context provider |
| defaultGlobalContext | boolean | No | Whether to use global context by default |
| transformConfig | (config) => config | No | Transform the widget config before rendering |
| isVisible | (widget) => boolean | No | Dynamic visibility of the widget |
Most widgets only need name and component.
Pimcore Studio UI uses a flexible layout with four widget areas:
┌──────────────────────────────────────────────────────┐
│ Toolbar / Navigation │
├──────────┬───────────────────────────┬───────────────┤
│ │ │ │
│ LEFT │ MAIN │ RIGHT │
│ │ (center) │ │
│ ├───────────────────────────┤ │
│ │ BOTTOM │ │
├──────────┴───────────────────────────┴───────────────┤
│ Status Bar │
└──────────────────────────────────────────────────────┘
| Area | Purpose | Typical Use | |------|---------|-------------| | main | Center content area | Editors, listings, dashboards | | left | Left sidebar | Tree navigation, filters | | right | Right sidebar | Properties, details panels | | bottom | Bottom panel | Logs, console output, search results |
Feature widgets usually open in main. Tree views and navigation panels typically go in left.
import { useWidgetManager, type WidgetManagerTabConfig } from '@pimcore/studio-ui-bundle/modules/widget-manager'
const widgetManager = useWidgetManager()
| Method | Description |
|--------|-------------|
| openMainWidget(config) | Open widget in center area |
| openLeftWidget(config) | Open widget in left sidebar |
| openRightWidget(config) | Open widget in right sidebar |
| openBottomWidget(config) | Open widget in bottom panel |
| closeWidget(id) | Close a widget by its ID |
| switchToWidget(id) | Focus an already-open widget |
All four open*Widget methods take the same WidgetManagerTabConfig shape — pick the one matching the target area.
export const MyComponent = (): React.JSX.Element => {
const widgetManager = useWidgetManager()
const openMyFeature = (): void => {
const tabConfig: WidgetManagerTabConfig = {
name: 'My Feature',
id: 'my-feature-singleton',
component: 'my-feature',
config: {
translationKey: 'my-feature.title',
icon: { type: 'name', value: 'widget-default' }
}
}
widgetManager.openMainWidget(tabConfig)
}
return <Button onClick={ openMyFeature }>Open Feature</Button>
}
interface WidgetManagerTabConfig {
name: string
id?: string
component: string
config: {
translationKey?: string
label?: string
icon?: { type: 'name', value: string }
[key: string]: any
}
}
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| name | string | Yes | Display name for the tab |
| id | string | No | Unique ID. Auto-generated if omitted. Set a fixed value for singletons. |
| component | string | Yes | Must match a registered widget's name |
| config | object | Yes | Configuration passed to the widget component |
| config.translationKey | string | No | Translation key for the tab label |
| config.label | string | No | Static label (prefer translationKey) |
| config.icon | object | No | Icon displayed on the tab |
| config.[key] | any | No | Any additional data the widget needs (e.g., assetId) |
The config object is flexible — add any properties the widget component needs (e.g., assetId, mode, parentFolderId). Access them inside the widget via the widget context or props.
End-to-end example showing a widget wired into main navigation.
// your-bundle/assets/studio/js/src/modules/my-feature/components/my-feature-widget.tsx
import React from 'react'
import { Content, Header, Select, Button } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
export const MyFeatureWidget = (): React.JSX.Element => {
const { t } = useTranslation()
return (
<Content padded>
<Header title={ t('my-feature.title') }>
<Button type="primary" onClick={ () => {} }>
{ t('my-feature.action-button') }
</Button>
</Header>
<p>{ t('my-feature.description') }</p>
</Content>
)
}
// your-bundle/assets/studio/js/src/modules/my-feature/index.ts
import { type AbstractModule } from '@pimcore/studio-ui-bundle'
import { container, serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
import { type MainNavRegistry } from '@pimcore/studio-ui-bundle/modules/app'
import { MyFeatureWidget } from './components/my-feature-widget'
export const MyFeatureModule: AbstractModule = {
onInit: (): void => {
const widgetRegistryService = container.get<WidgetRegistry>(serviceIds.widgetManager)
widgetRegistryService.registerWidget({
name: 'my-feature',
component: MyFeatureWidget
})
const mainNavRegistry = container.get<MainNavRegistry>(serviceIds.mainNavRegistry)
mainNavRegistry.registerMainNavItem({
path: 'DataManagement/My Feature',
label: 'my-feature.navigation.title',
order: 10,
perspectivePermission: 'dataManagement.myFeature',
widgetConfig: {
name: 'My Feature',
id: 'my-feature',
component: 'my-feature', // Must match registered widget name
config: {
translationKey: 'my-feature.navigation.title',
icon: { type: 'name', value: 'widget-default' }
}
}
})
}
}
// your-bundle/assets/studio/js/src/plugin.ts
import { type AbstractPlugin } from '@pimcore/studio-ui-bundle'
import { MyFeatureModule } from './modules/my-feature'
export const YourBundlePlugin: AbstractPlugin = {
modules: [MyFeatureModule]
}
# your-bundle/translations/studio.en.yaml
my-feature:
navigation:
title: My Feature
title: My Feature
description: This is my custom feature widget.
action-button: Run action
perspective-editor:
form:
main-nav-permission:
dataManagement:
myFeature: Data Management > My Feature
The component field in widgetConfig is a string that must exactly match the name passed to registerWidget(). This is how the widget manager resolves which React component to render when the nav item is clicked.
// Register with name 'my-feature'
widgetRegistryService.registerWidget({
name: 'my-feature', // <-- This name...
component: MyFeatureWidget
})
// Reference in navigation's widgetConfig
mainNavRegistry.registerMainNavItem({
path: 'DataManagement/My Feature',
label: 'my-feature.navigation.title',
widgetConfig: {
component: 'my-feature', // <-- ...must match here
name: 'My Feature',
id: 'my-feature',
config: {
translationKey: 'my-feature.navigation.title',
icon: { type: 'name', value: 'widget-default' }
}
}
})
Widgets don't require a navigation entry — you can open them from any component by calling widgetManager.openMainWidget(config) (or any other open*Widget variant).
| Pattern | ID strategy | Behavior | Use for |
|---------|-------------|----------|---------|
| Singleton | Fixed id | Re-opening switches to existing tab | Settings, listings, dashboards |
| Multi-instance | Omit id or use dynamic id (e.g. asset-editor-${assetId}) | Each open creates a new tab | Element editors, detail views, comparisons |
// Singleton: always the same tab
{ name: 'Settings', id: 'settings', component: 'settings', config: { /* ... */ } }
// Multi-instance: new tab per asset
{ name: `Asset ${assetId}`, id: `asset-editor-${assetId}`, component: 'asset-editor',
config: { assetId, /* ... */ } }
// ❌ WRONG - Widget was never registered, fails silently
mainNavRegistry.registerMainNavItem({
widgetConfig: { component: 'my-feature', /* ... */ }
})
// ✅ CORRECT - Register first, then reference
widgetRegistryService.registerWidget({ name: 'my-feature', component: MyFeatureWidget })
mainNavRegistry.registerMainNavItem({
widgetConfig: { component: 'my-feature', /* ... */ }
})
// ❌ WRONG
const MyWidget = () => <div>Hello</div>
export default MyWidget
// ✅ CORRECT
export const MyWidget = (): React.JSX.Element => <div>Hello</div>
// ❌ WRONG - Case-sensitive mismatch
registerWidget({ name: 'my-feature', component: MyFeatureWidget })
widgetConfig: { component: 'My-Feature' }
// ✅ CORRECT - Exact string match
widgetConfig: { component: 'my-feature' }
// ❌ Tab looks incomplete without an icon
config: { translationKey: 'my-feature.title' }
// ✅ Always provide an icon
config: {
translationKey: 'my-feature.title',
icon: { type: 'name', value: 'widget-default' }
}
Omitting id on a nav-linked widget opens a new tab on every click. Set a fixed id for singletons.
tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
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
tools
Adding and customizing editor tabs in Pimcore Studio UI - tab managers, registration, override, permissions, and detachable tabs