skills/pimcore-studio-ui-tabs-editors/SKILL.md
Adding and customizing editor tabs in Pimcore Studio UI - tab managers, registration, override, permissions, and detachable tabs
npx skillsauth add pimcore/skills pimcore-studio-ui-tabs-editorsInstall 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 add, customize, and manage editor tabs in Pimcore Studio UI:
Use this when:
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Every element editor in Pimcore Studio is composed of tabs managed by Tab Managers -- injectable services in the DI container. Each element type (asset, document, data-object) has multiple tab managers, one per subtype.
ImageTabManager for image assets, PageTabManager for page documents.container.get<T>(serviceIds[...]).IEditorTab object, not a class.Every tab registration uses the IEditorTab interface:
interface IEditorTab {
/** Unique key identifying the tab. Must not collide with existing tabs. */
key: string
/** Display label shown in the tab bar. Can be a string or JSX for custom rendering. */
label: string | React.JSX.Element
/** The React component rendered as tab content. */
children: React.JSX.Element
/** Icon shown next to the label in the tab bar. */
icon: React.JSX.Element
/**
* Element-level permission required to see this tab.
* Checked against the element's permissions object.
* Example: 'properties', 'versions', 'settings'
*/
workspacePermission?: string
/**
* User-level permission required to see this tab.
* Checked against the current user's global permissions.
* Example: 'notes_events', 'tags_configuration'
*/
userPermission?: string
/**
* Dynamic visibility function. Return true to hide the tab.
* Receives the current element as argument.
*/
hidden?: (element: any) => boolean
/**
* Whether this tab can be popped out into a separate window.
* Defaults to false.
*/
isDetachable?: boolean
}
When a tab has multiple permission fields, all must pass for the tab to be visible:
userPermission -- Does the user have this global permission?workspacePermission -- Does the user have this permission on the element?hidden() -- Does the dynamic function return true?Use the service ID that matches the element subtype you want to extend. Service IDs always follow the pattern '{ElementType}/Editor/{SubType}TabManager'.
| Subtype | Service ID | Type |
|---------|-----------|------|
| Image | serviceIds['Asset/Editor/ImageTabManager'] | ImageTabManager |
| Video | serviceIds['Asset/Editor/VideoTabManager'] | VideoTabManager |
| Audio | serviceIds['Asset/Editor/AudioTabManager'] | AudioTabManager |
| Text | serviceIds['Asset/Editor/TextTabManager'] | TextTabManager |
| Document (PDF, etc.) | serviceIds['Asset/Editor/DocumentTabManager'] | DocumentTabManager |
| Archive (ZIP, etc.) | serviceIds['Asset/Editor/ArchiveTabManager'] | ArchiveTabManager |
| Folder | serviceIds['Asset/Editor/FolderTabManager'] | FolderTabManager |
| Unknown | serviceIds['Asset/Editor/UnknownTabManager'] | UnknownTabManager |
Asset types are imported from @pimcore/studio-ui-bundle/modules/asset.
| Subtype | Service ID | Type |
|---------|-----------|------|
| Page | serviceIds['Document/Editor/PageTabManager'] | PageTabManager |
| Snippet | serviceIds['Document/Editor/SnippetTabManager'] | SnippetTabManager |
| Email | serviceIds['Document/Editor/EmailTabManager'] | EmailTabManager |
| Link | serviceIds['Document/Editor/LinkTabManager'] | LinkTabManager |
| Hardlink | serviceIds['Document/Editor/HardlinkTabManager'] | HardlinkTabManager |
| Folder | serviceIds['Document/Editor/FolderTabManager'] | FolderTabManager |
Document types are imported from @pimcore/studio-ui-bundle/modules/document.
| Subtype | Service ID | Type |
|---------|-----------|------|
| Object | serviceIds['DataObject/Editor/ObjectTabManager'] | ObjectTabManager |
| Variant | serviceIds['DataObject/Editor/VariantTabManager'] | VariantTabManager |
| Folder | serviceIds['DataObject/Editor/FolderTabManager'] | FolderTabManager |
Data object types are imported from @pimcore/studio-ui-bundle/modules/data-object.
IMPORTANT: The FolderTabManager type exists in all three modules (asset, document, data-object). Always import it from the module that matches your target element type — mismatching will cause type errors.
The standard pattern uses AbstractModule.onInit() to get the tab manager from the container and call register().
// File: your-bundle/assets/studio/js/src/modules/image-metadata-tab-extension.tsx
import React from 'react'
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { Icon } from '@pimcore/studio-ui-bundle/components'
import { type ImageTabManager } from '@pimcore/studio-ui-bundle/modules/asset'
import { ImageMetadataTab } from '../components/image-metadata-tab/image-metadata-tab'
export const ImageMetadataTabExtension: AbstractModule = {
onInit (): void {
const imageTabManager = container.get<ImageTabManager>(
serviceIds['Asset/Editor/ImageTabManager']
)
imageTabManager.register({
key: 'my-bundle.image-metadata',
label: 'Metadata',
icon: <Icon value="info-circle" />,
children: <ImageMetadataTab />,
// Element-level: user must have 'settings' permission on this asset
workspacePermission: 'settings',
// User-level: user must have global 'assets' permission
userPermission: 'assets',
// Dynamic visibility: hide for unpublished elements
hidden: (element) => element?.published !== true,
// Allow popping out into a separate window
isDetachable: true
})
}
}
To target a different element type or subtype, change three things:
ImageTabManager -> your target)'Asset/Editor/ImageTabManager' -> your target from the tables above)modules/asset -> modules/document or modules/data-object)If your tab should appear across multiple subtypes, define it once and register with each tab manager:
export const UniversalAssetTabExtension: AbstractModule = {
onInit (): void {
const customTab = {
key: 'my-bundle.universal-tab',
label: 'Universal Tab',
icon: <Icon value="star" />,
children: <UniversalTabContent />
}
container.get<ImageTabManager>(serviceIds['Asset/Editor/ImageTabManager']).register(customTab)
container.get<VideoTabManager>(serviceIds['Asset/Editor/VideoTabManager']).register(customTab)
container.get<DocumentTabManager>(serviceIds['Asset/Editor/DocumentTabManager']).register(customTab)
}
}
Replace an existing tab's content while preserving its key, label, icon, and permissions.
// File: your-bundle/assets/studio/js/src/modules/custom-preview-tab.tsx
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type ObjectTabManager } from '@pimcore/studio-ui-bundle/modules/data-object'
import { CustomPreviewTab } from '../components/custom-preview-tab/custom-preview-tab'
export const CustomPreviewOverride: AbstractModule = {
onInit (): void {
const objectTabManager = container.get<ObjectTabManager>(
serviceIds['DataObject/Editor/ObjectTabManager']
)
// Always check that the tab exists — core may rename or remove it
const previewTab = objectTabManager.getTab('preview')
if (previewTab === undefined) {
console.warn('[my-bundle] Tab "preview" not found. Override skipped.')
return
}
// Re-register with the same key but new children (and optionally more)
objectTabManager.register({
...previewTab,
children: <CustomPreviewTab />,
// Optionally override more fields:
// label: 'Enhanced Preview',
// isDetachable: true
})
}
}
How it works:
getTab('preview') retrieves the existing tab registration....previewTab copies all existing properties.children replaces only the rendered content.register() with an existing key overwrites the previous registration.Tab content should use the standard layout pattern with Content and Header components. Access the current element via useElementContext() and element-specific draft hooks.
// File: your-bundle/assets/studio/js/src/components/asset-info-tab/asset-info-tab.tsx
import React from 'react'
import { Content, Header } from '@pimcore/studio-ui-bundle/components'
import { useElementContext } from '@pimcore/studio-ui-bundle/modules/element'
import { useAssetDraft } from '@pimcore/studio-ui-bundle/modules/asset'
import { useTranslation } from 'react-i18next'
export const AssetInfoTab = (): React.JSX.Element => {
const { t } = useTranslation()
const { id } = useElementContext()
const { asset } = useAssetDraft(id)
if (asset === undefined) {
return <Content padded>Loading...</Content>
}
return (
<Content padded>
<Header title={t('my-bundle.asset-info.title')} />
<p>Type: {asset.type}</p>
<p>Size: {asset.fileSize}</p>
</Content>
)
}
For tabs with toolbars or forms, wrap in <Flex vertical className="absolute-stretch"> and add a <Toolbar> alongside <Content>. For form-based tabs, use FormKit — see pimcore-studio-ui-forms-antd.
Tab extensions must be wired into the plugin system to be loaded.
// File: your-bundle/assets/studio/js/src/plugin.ts
import { type IAbstractPlugin } from '@pimcore/studio-ui-bundle'
import { ImageMetadataTabExtension } from './modules/image-metadata-tab-extension'
import { CustomPreviewOverride } from './modules/custom-preview-tab'
export const MyBundlePlugin: IAbstractPlugin = {
name: 'MyBundlePlugin',
onStartup ({ moduleSystem }) {
moduleSystem.registerModule(ImageMetadataTabExtension)
moduleSystem.registerModule(CustomPreviewOverride)
}
}
1. Plugin.onStartup() -> Register modules via moduleSystem.registerModule()
(DO NOT register tabs here -- too early!)
2. Module.onInit() -> Register tabs via tabManager.register()
(This is the correct place)
3. Application renders -> Tabs visible in element editors
Rule: Always register tabs in Module.onInit(), never in Plugin.onStartup().
// WRONG
serviceIds['AssetFolderTabManager'] // Does not exist
// CORRECT — always '{ElementType}/Editor/{SubType}TabManager'
serviceIds['Asset/Editor/FolderTabManager']
// WRONG — tab managers may not be ready, registration may fail or be overwritten
export const MyPlugin: IAbstractPlugin = {
name: 'MyPlugin',
onStartup ({ moduleSystem }) {
const tabManager = container.get<ImageTabManager>(serviceIds['Asset/Editor/ImageTabManager'])
tabManager.register({ ... })
}
}
// CORRECT — register the module in the plugin, register tabs in Module.onInit()
export const MyPlugin: IAbstractPlugin = {
name: 'MyPlugin',
onStartup ({ moduleSystem }) {
moduleSystem.registerModule(MyTabModule)
}
}
// WRONG — FolderTabManager imported from asset but service ID is for document
import { type FolderTabManager } from '@pimcore/studio-ui-bundle/modules/asset'
container.get<FolderTabManager>(serviceIds['Document/Editor/FolderTabManager'])
// CORRECT — match the import module to the element type
import { type FolderTabManager } from '@pimcore/studio-ui-bundle/modules/document'
container.get<FolderTabManager>(serviceIds['Document/Editor/FolderTabManager'])
// WRONG — generic keys may collide with built-in tabs
key: 'settings'
// CORRECT — namespace your keys
key: 'my-bundle.settings'
// WRONG — getTab() may return undefined
const previewTab = objectTabManager.getTab('preview')
objectTabManager.register({ ...previewTab, children: <CustomPreview /> })
// CORRECT — check before spreading
const previewTab = objectTabManager.getTab('preview')
if (previewTab === undefined) {
console.warn('[my-bundle] Tab not found. Override skipped.')
return
}
objectTabManager.register({ ...previewTab, children: <CustomPreview /> })
// CORRECT — both imports required when using JSX
import React from 'react'
import { Icon } from '@pimcore/studio-ui-bundle/components'
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