skills/pimcore-studio-ui-layout-components/SKILL.md
Fundamental layout components in Pimcore Studio UI - Content, Box, Flex, Space, ConfigLayout with real-world patterns
npx skillsauth add pimcore/skills pimcore-studio-ui-layout-componentsInstall 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.
Core layout components for building UIs in Pimcore Studio:
LIST-DETAIL-TABS-PATTERN.md)Use this when:
LIST-DETAIL-TABS-PATTERN.md for the complete pattern)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.
All layout components use a consistent spacing system:
mini - Smallest spacing (4px)extra-small - Very small spacing (8px)small - Small spacing (12px)medium - Medium spacing (16px) - defaultlarge - Large spacing (24px)extra-large - Extra large spacing (32px)You can also use numbers for pixel-perfect spacing.
The Content component is the primary container for page content sections. Use it as a wrapper for main content areas.
import { Content } from '@pimcore/studio-ui-bundle/components'
const MyPage = () => {
return (
<Content padded>
<h1>My Page Title</h1>
<p>Content goes here</p>
</Content>
)
}
import { Content } from '@pimcore/studio-ui-bundle/components'
import { useGetDataQuery } from '@pimcore/studio-ui-bundle/api/data'
const DataView = () => {
const { data, isLoading } = useGetDataQuery()
return (
<Content loading={isLoading} padded>
{data && <DataDisplay data={data} />}
</Content>
)
}
Why use loading on Content:
// Fine-grained padding control
<Content
padded
padding={{
x: 'extra-small', // Horizontal padding
y: 'small' // Vertical padding
}}
>
{/* Content */}
</Content>
// Automatic spacing between direct children
<Content
padded
gap="small"
>
<SearchForm />
<ResultsTable />
<Pagination />
</Content>
// No content, no padding
<Content none />
interface ContentProps {
padded?: boolean // Apply default padding
padding?: { // Custom padding control
x?: SpacingSize // Horizontal
y?: SpacingSize // Vertical
top?: SpacingSize
bottom?: SpacingSize
left?: SpacingSize
right?: SpacingSize
}
loading?: boolean // Show loading skeleton
gap?: SpacingSize // Gap between children
none?: boolean // Empty state
children?: ReactNode
}
import { Content, Header } from '@pimcore/studio-ui-bundle/components'
export const ExampleWidget = () => {
const { data, isLoading } = useWidgetDataQuery()
return (
<Content
loading={isLoading}
padded
padding={{
x: 'extra-small',
y: 'extra-small'
}}
gap="small"
>
<Header title="Example Widget" />
<div>Select a widget:</div>
{data && <WidgetList items={data} />}
</Content>
)
}
The Box component is a utility wrapper for adding spacing (margin/padding) around elements. Use it for fine-grained spacing control.
import { Box } from '@pimcore/studio-ui-bundle/components'
const MyComponent = () => {
return (
<Box padding="small">
<SomeContent />
</Box>
)
}
// String value (all sides)
<Box padding="small">
<Panel />
</Box>
// Object for specific sides
<Box padding={{ x: 'extra-small', y: 'small' }}>
<Panel />
</Box>
// Individual sides
<Box padding={{ top: 'small', bottom: 'large', left: 'extra-small', right: 'extra-small' }}>
<Panel />
</Box>
// Add margin to separate sections
<Box margin={{ top: 'small', bottom: 'small' }}>
<Section />
</Box>
// Specific side margins
<Box margin={{ bottom: 'large' }}>
<Section />
</Box>
<Box
padding="extra-small"
margin={{ top: 'extra-small', bottom: 'extra-small' }}
>
<Flex align="center" gap="extra-small">
<Icon value="info" />
<Text>Important information</Text>
</Flex>
</Box>
interface BoxProps {
padding?: SpacingSize | {
x?: SpacingSize
y?: SpacingSize
top?: SpacingSize
bottom?: SpacingSize
left?: SpacingSize
right?: SpacingSize
}
margin?: SpacingSize | {
x?: SpacingSize
y?: SpacingSize
top?: SpacingSize
bottom?: SpacingSize
left?: SpacingSize
right?: SpacingSize
}
children?: ReactNode
}
import { Box, Flex, Space } from '@pimcore/studio-ui-bundle/components'
export const ScheduleToolbar = () => {
return (
<Box padding={{ y: 'small' }}>
<Space>
<Text strong>Archived Schedules</Text>
<IconTextButton
icon={{ value: 'trash' }}
onClick={handleDelete}
>
Delete All
</IconTextButton>
</Space>
</Box>
)
}
Use Content when:
Use Box when:
The Flex component provides flexible layout control with CSS Flexbox. Use it for aligning, distributing, and organizing elements.
import { Flex } from '@pimcore/studio-ui-bundle/components'
const Toolbar = () => {
return (
<Flex align="center" gap="small">
<Icon value="search" />
<Input placeholder="Search..." />
<Button>Search</Button>
</Flex>
)
}
// Stack elements vertically
<Flex vertical gap="small">
<Title>Settings</Title>
<SettingsPanel />
<SaveButton />
</Flex>
// Center items horizontally and vertically
<Flex align="center" justify="center">
<LoadingSpinner />
</Flex>
// Align items to start/end
<Flex align="start" justify="space-between">
<Title>Header</Title>
<IconButton icon={{ value: 'close' }} />
</Flex>
// Use spacing system
<Flex gap="large">
<Panel />
<Panel />
<Panel />
</Flex>
// Or specific pixel value
<Flex gap={24}>
<Item />
<Item />
</Flex>
// Complex toolbar with multiple sections
<Flex justify="space-between" align="center">
<Flex gap="small">
<IconButton icon={{ value: 'arrow-left' }} />
<Title>Document Editor</Title>
</Flex>
<Flex gap="mini">
<Button>Cancel</Button>
<Button type="primary">Save</Button>
</Flex>
</Flex>
// Stretch to fill parent height
<Flex
className="absolute-stretch"
vertical
justify="space-between"
>
<Content padded>
{/* Main content */}
</Content>
<Toolbar justify="flex-end">
<Button>Save</Button>
</Toolbar>
</Flex>
interface FlexProps {
vertical?: boolean // Stack vertically
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
justify?: 'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly'
gap?: SpacingSize | number // Gap between children
wrap?: boolean // Allow wrapping
className?: string
style?: CSSProperties
children?: ReactNode
}
import { Flex, Icon } from '@pimcore/studio-ui-bundle/components'
export const EmailCard = ({ email }) => {
return (
<Flex align="center" gap="extra-small">
<Icon value="send-03" />
<span>{email.subject}</span>
</Flex>
)
}
import { Flex, Content, Toolbar, Button } from '@pimcore/studio-ui-bundle/components'
export const AppearanceForm = () => {
return (
<Flex
className="appearance-branding-form absolute-stretch"
justify="space-between"
vertical
>
<Content padded>
<FormKit formProps={{ form }}>
{/* Form fields */}
</FormKit>
</Content>
<Toolbar justify="flex-end">
<Button onClick={handleCancel}>Cancel</Button>
<Button type="primary" onClick={handleSave}>Save</Button>
</Toolbar>
</Flex>
)
}
The Space component adds consistent spacing between children. It's simpler than Flex when you just need spacing without alignment control.
import { Space } from '@pimcore/studio-ui-bundle/components'
const ButtonGroup = () => {
return (
<Space size="small">
<Button>Cancel</Button>
<Button type="primary">Save</Button>
</Space>
)
}
// Stack with spacing
<Space direction="vertical" size="large">
<ColorPanel />
<ImagePanel />
<LogoPanel />
</Space>
import { Space, TooltipIcon } from '@pimcore/studio-ui-bundle/components'
const PanelHeader = ({ title, tooltip }) => {
return (
<Space size="extra-small">
{title}
<TooltipIcon tooltip={tooltip} />
</Space>
)
}
// Switch with label
<Space
className="pimcore-schedule-toolbar__filters__active-switch"
size="extra-small"
>
<Switch
labelLeft="Show active only"
onChange={setActiveOnly}
value={activeOnly}
/>
</Space>
// Fill available width
<Space
className="w-full"
direction="vertical"
size="extra-small"
>
<FormItem1 />
<FormItem2 />
<FormItem3 />
</Space>
interface SpaceProps {
size?: SpacingSize // Spacing size
direction?: 'horizontal' | 'vertical' // Layout direction (default: horizontal)
className?: string
children?: ReactNode
}
import { Space, IconButton } from '@pimcore/studio-ui-bundle/components'
export const ColumnsConfiguration = () => {
return (
<Space size="mini">
<IconButton
icon={{ value: 'trash' }}
onClick={handleRemove}
theme="secondary"
/>
<IconButton
icon={{ value: 'drag-handle' }}
theme="secondary"
/>
</Space>
)
}
Use Space when:
Use Flex when:
The ConfigLayout component creates a two-column layout with an optional resizable divider. Perfect for sidebar + content layouts.
ConfigLayout has a standard pattern for managing collections of entities (settings, configurations, CRUD interfaces).
👉 See LIST-DETAIL-TABS-PATTERN.md for the complete implementation guide!
This pattern provides:
* in tab labelsPerfect for: Settings pages, entity management, configuration UIs, list-based CRUD
Examples in codebase: Target Groups (personalization-bundle), Email Log, Reports Editor, Field Definitions
import { ConfigLayout } from '@pimcore/studio-ui-bundle/components'
const EditorLayout = () => {
return (
<ConfigLayout
leftItem={{
children: <Sidebar />
}}
rightItem={{
children: <MainContent />
}}
/>
)
}
// Allow user to resize the sidebar
// Only specify size, minSize, maxSize when you need resizeAble
<ConfigLayout
leftItem={{
minSize: 250,
maxSize: 350,
children: <DetailSidebar />
}}
resizeAble
rightItem={{
children: <DetailContent />
}}
/>
interface ConfigLayoutProps {
leftItem: {
children: ReactNode
minSize?: number // Minimum width in pixels (only when resizeAble)
maxSize?: number // Maximum width in pixels (only when resizeAble)
size?: number // Default width in pixels (rarely needed, use default)
}
rightItem: {
children: ReactNode
}
resizeAble?: boolean // Enable resizing
}
Note: Usually you don't need to specify size, minSize, or maxSize. The default width is fine for most cases. Only use these when you specifically need a resizable sidebar.
import { ConfigLayout, Content } from '@pimcore/studio-ui-bundle/components'
export const ReportsEditor = () => {
return (
<Content loading={isLoading}>
<ConfigLayout
leftItem={{
children: (
<ReportsSidebar
handleOpenReport={handleOpenReport}
isFetching={isFetching}
isLoading={isLoading}
/>
)
}}
rightItem={{
children: <ReportContent />
}}
/>
</Content>
)
}
import { ConfigLayout, ContentLayout, Toolbar, Flex, IconButton } from '@pimcore/studio-ui-bundle/components'
export const FieldDefinitionDetail = () => {
return (
<ContentLayout
className="absolute-stretch"
renderToolbar={
<Toolbar>
<Flex gap="mini">
<IconButton icon={{ value: 'refresh' }} onClick={handleRefresh} />
</Flex>
<DetailSave />
</Toolbar>
}
>
<Content loading={isLoading}>
<ConfigLayout
leftItem={{
minSize: 250,
maxSize: 350,
children: <DetailSidebar />
}}
resizeAble
rightItem={{
children: (
<Content padded>
<FormKit formProps={{ form }}>
{/* Field configuration form */}
</FormKit>
</Content>
)
}}
/>
</Content>
</ContentLayout>
)
}
Use case: Edit forms with save/cancel actions
import { Flex, Content, Toolbar, Button } from '@pimcore/studio-ui-bundle/components'
export const EntityEditForm = ({ entity, onSave, onCancel }) => {
return (
<Flex
className="absolute-stretch"
vertical
justify="space-between"
>
<Content padded>
<FormKit formProps={{ form, onFinish: onSave }}>
{/* Form fields */}
</FormKit>
</Content>
<Toolbar justify="flex-end">
<Button onClick={onCancel}>Cancel</Button>
<Button type="primary" htmlType="submit">Save</Button>
</Toolbar>
</Flex>
)
}
Use case: Settings pages, editors with navigation
import { ConfigLayout, Content } from '@pimcore/studio-ui-bundle/components'
export const SettingsPage = () => {
return (
<ConfigLayout
leftItem={{
children: <SettingsNavigation />
}}
rightItem={{
children: (
<Content padded>
<SettingsContent />
</Content>
)
}}
/>
)
}
Use case: Multi-section forms, complex toolbars, data views
import { Content, Space, Box, Flex } from '@pimcore/studio-ui-bundle/components'
// Multi-section form
export const MultiSectionForm = () => {
return (
<Content padded padding={{ x: 'extra-small', y: 'extra-small' }}>
<Space direction="vertical" size="large">
<Box><Title level={2}>Basic Info</Title><BasicInfoForm /></Box>
<Box><Title level={2}>Advanced</Title><AdvancedForm /></Box>
</Space>
</Content>
)
}
// Complex toolbar with grouped actions
export const ComplexToolbar = () => {
return (
<Flex justify="space-between" align="center">
<Flex gap="small"><IconButton icon={{ value: 'arrow-left' }} /><Title level={3}>Editor</Title></Flex>
<Space size="mini"><IconButton icon={{ value: 'undo' }} /><IconButton icon={{ value: 'redo' }} /></Space>
<Flex gap="mini"><Button>Cancel</Button><Button type="primary">Save</Button></Flex>
</Flex>
)
}
// Data view with loading/empty states
export const DataView = () => {
const { data, isLoading } = useGetDataQuery()
return (
<Content loading={isLoading} padded>
{data?.length ? (
<Space direction="vertical" size="small">
{data.map(item => <DataCard key={item.id} item={item} />)}
</Space>
) : (
<EmptyState message="No data" />
)}
</Content>
)
}
✅ Do this:
<Content loading={isLoading} padded>
<MyForm />
</Content>
❌ Not this:
{isLoading ? <Spinner /> : (
<div style={{ padding: '16px' }}>
<MyForm />
</div>
)}
Why: Content provides consistent loading UI and spacing.
✅ Do this:
<Box padding="small" margin={{ bottom: 'large' }}>
<Section />
</Box>
❌ Not this:
<div style={{ padding: '12px', marginBottom: '24px' }}>
<Section />
</div>
Why: Uses design system values, consistent across app.
✅ Do this:
<Space size="small">
<Button>Cancel</Button>
<Button type="primary">Save</Button>
</Space>
❌ Not this:
<Flex gap="small">
<Button>Cancel</Button>
<Button type="primary">Save</Button>
</Flex>
Why: Space is simpler when you don't need alignment.
✅ Do this:
<Flex align="center" justify="space-between">
<Title>Header</Title>
<IconButton icon={{ value: 'close' }} />
</Flex>
✅ Also good:
<Space size="small">
<Button>Action 1</Button>
<Button>Action 2</Button>
</Space>
Why: Choose the right tool - Flex when you need alignment, Space when you don't.
✅ Do this:
// Basic usage - no size specs needed
<ConfigLayout
leftItem={{
children: <Sidebar />
}}
rightItem={{
children: <Content padded><MainView /></Content>
}}
/>
// Only add size props when you need resizeAble
<ConfigLayout
leftItem={{
minSize: 250,
maxSize: 400,
children: <Sidebar />
}}
resizeAble
rightItem={{
children: <Content padded><MainView /></Content>
}}
/>
Why: Built-in two-column layout with optional resize behavior, consistent UX. Use default width unless you specifically need custom sizing.
✅ Do this:
<Flex vertical className="absolute-stretch">
<Content padded>
<Space direction="vertical" size="large">
<Section1 />
<Section2 />
</Space>
</Content>
<Toolbar justify="flex-end">
<Button>Save</Button>
</Toolbar>
</Flex>
Why: Composable components create flexible, maintainable layouts.
✅ Do this:
<Content loading={isLoading} padded>
{data && <DataDisplay data={data} />}
</Content>
❌ Not this:
<Content padded>
{isLoading ? <Skeleton /> : data && <DataDisplay data={data} />}
</Content>
Why: Content has built-in loading skeleton that prevents layout shift.
// BAD - nested Content components
<Content padded>
<Content padded>
<Form />
</Content>
</Content>
✅ Use Box or Flex for inner sections:
// GOOD
<Content padded>
<Box padding="small">
<Form />
</Box>
</Content>
// BAD
<div style={{ margin: '8px 16px' }}>
<Component />
</div>
✅ Use Box or spacing props:
// GOOD
<Box margin={{ y: 'extra-small', x: 'small' }}>
<Component />
</Box>
// BAD - mixing px values with spacing system
<Flex gap={8}>
<Box padding="small">
<Component />
</Box>
</Flex>
✅ Be consistent:
// GOOD
<Flex gap="extra-small">
<Box padding="small">
<Component />
</Box>
</Flex>
// BAD - unnecessary size specification without resizeAble
<ConfigLayout
leftItem={{
size: 300,
minSize: 250,
maxSize: 400,
children: <Sidebar />
}}
rightItem={{ children: <Main /> }}
/>
✅ Use default size, only specify when resizeAble:
// GOOD - use default width (most cases)
<ConfigLayout
leftItem={{ children: <Sidebar /> }}
rightItem={{ children: <Main /> }}
/>
// GOOD - only specify sizes when you need resizeAble
<ConfigLayout
leftItem={{
minSize: 250,
maxSize: 400,
children: <Sidebar />
}}
resizeAble
rightItem={{ children: <Main /> }}
/>
// BAD - overkill for simple spacing
<Flex direction="horizontal" gap="small" align="start">
<Button>Cancel</Button>
<Button>Save</Button>
</Flex>
✅ Use Space for simple cases:
// GOOD
<Space size="small">
<Button>Cancel</Button>
<Button>Save</Button>
</Space>
| Need | Use | Props |
|------|-----|-------|
| Main content wrapper | Content | padded, loading, gap |
| Add spacing around element | Box | padding, margin |
| Align/distribute elements | Flex | align, justify, gap, vertical |
| Simple spacing | Space | size, direction |
| Sidebar + content | ConfigLayout | leftItem, rightItem, resizeAble |
mini = 4pxextra-small = 8pxsmall = 12pxmedium = 16px (default)large = 24pxextra-large = 32pxAfter mastering layout 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