skills/pimcore-studio-ui-ux-ui-guidelines/SKILL.md
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
npx skillsauth add pimcore/skills pimcore-studio-ui-ux-design-guidelinesInstall 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.
Use this when:
These five principles govern every UI decision in Pimcore Studio.
| Principle | Core idea | |---|---| | Consistency | Shared visual vocabulary — colors, typography, spacing, iconography, and interaction patterns are uniform across all modules. | | Accessibility | Keyboard navigation, semantic HTML, ARIA attributes, and high-contrast colors (exceeding WCAG thresholds) are requirements, not extras. | | Scalability | The platform must support third-party extensions, custom branding, and widget integrations without breaking existing patterns. Build modular. | | Efficiency | Predictable, inclusive interfaces let users work faster. Reusable patterns let developers build faster. Efficiency is the result of the other principles. | | Predictability | Repeat patterns across contexts. Once a user learns an interaction in one place, it must work identically everywhere else. |
Pimcore Studio uses a three-panel layout:
┌──────────────┬─────────────────────────────┬──────────────┐
│ Left Panel │ Content Area │ Right Panel │
│ (widgets) │ 12-column responsive grid │ (widgets) │
└──────────────┴─────────────────────────────┴──────────────┘
Rules:
Extension placement:
All spacing values come from the named token set — never use arbitrary pixel values.
| Token | Value | Typical use |
|---|---|---|
| none | 0px | Resets |
| mini | 4px | Tight internal gaps (icon–label) |
| extra-small | 8px | Compact component padding |
| small | 12px | Table cells, tag groups, form item gaps |
| normal | 16px | Default content padding |
| medium | 20px | Section padding |
| large | 24px | Section separation |
| extra-large | 32px | Major layout gaps |
| maxi | 48px | Panel separation, page-level breathing room |
Use Box, Content, Space, or Flex components to apply these tokens — see pimcore-studio-ui-layout-components. Global utility classes (.p-small, .m-y-large, etc.) are available but prefer component props.
Lato, sans-serif — the only permitted font; never override fontFamily in component styles12px (token.fontSize)35px (token.fontSizeHeading1)token.fontSize* / token.fontSizeHeading* — never hardcode font sizesUse semantic tokens for all color decisions. Never pick a hex value manually.
| Role | Token | Value | Use |
|---|---|---|---|
| Primary / brand | colorPrimary | #722ed1 purple | Brand actions, active states |
| Accent | colorAccent / colorBorderActiveTab | #13C2C2 / #00bab3 teal | Active tab border, secondary highlights |
| Success | colorSuccess | #52c41a green | Confirmations, publish success |
| Warning | colorWarning | #faad14 amber | Caution states |
| Error | colorError | #ff4d4f red | Destructive actions emphasis, validation errors |
| Info | colorInfo | maps to primary purple | Informational alerts |
For element/icon type color coding (50+ colorCoding* tokens — Red, Beige, Gold, Orange, Green, Mint, Blue, Purple, Violet, Magenta families), see pimcore-studio-ui-icons.
Always pair color with an icon or text label — never rely on color alone.
| Status | Tag color | Icon |
|---|---|---|
| Published | geekblue | — |
| Unpublished / Draft | gold | eye-off |
| Success feedback | colorSuccess bg | checkmark |
| Error / destructive | colorError | trash or close |
Use ElementTag for element status — it applies these conventions automatically.
Use the exact label that matches the semantic of the action.
| Label | Meaning | Icon | Emphasis | |---|---|---|---| | Add | Insert something that already exists into a container | Magnifying glass | — | | New | Create something from scratch | Plus (icon-only contexts only) | — | | Delete | Permanently erase from the system | Trash | Red / danger | | Remove | Remove from current UI/context only — data stays in backend | Close/X | — |
"Clear," "Remove," and "Close" intentionally share the same icon — do not add a second icon to distinguish them.
Delete is always the last item in context menus (highest priority number, e.g. 800).
Every action that permanently deletes or irreversibly modifies data must show a confirmation before execution. Three mechanisms exist — choose based on context:
| Mechanism | When to use |
|---|---|
| modal.confirm() via useStudioModal() | Standard delete confirmations — works across detached tab iframes |
| useFormModal().confirm() | When the user may want a "don't ask again" option |
| <Popconfirm> | Lightweight inline confirmation close to the trigger element |
Never execute a Delete action silently. Always include translated title, content (with item name), okText, and cancelText.
Use Title Case for: menu items, tab titles, context menu entries, section headings, panel titles.
Examples: Custom Reports, Output Channels, Asset Management
Use Sentence case for: button labels, modal titles, inline actions, any element with a verb.
Examples: Save draft, Merge version, Export CSV
All actionable labels must start with a verb:
| ❌ Don't | ✅ Do | |---|---| | CSV Export | Export CSV | | Asset Upload | Upload asset | | Template deletion | Delete template | | Configuration reset | Reset configuration |
Rule: button / menu / control → Sentence case, verb-first. Heading / title → Title case.
All user-facing strings must go through t() from useTranslation() (react-i18next). No hardcoded English strings in components — every label, placeholder, tooltip, and error message must have a translation key.
Toolbars use justify='space-between' by default, creating a natural left/right split.
| Side | Content | |---|---| | Left | Filters, search inputs, navigation, breadcrumbs | | Right | Primary actions (Save, Apply, Confirm), secondary actions |
Position and theme rules:
position='top': Filter bars, search bars at the top of a panelposition='bottom': Action bars with save/cancel at the bottom (default)position='content': Toolbars between two content sections (adds borders on both sides)theme='primary': Main toolbars (lavender #F5F3FA background)theme='secondary': Neutral/white-background toolbars (secondary emphasis)Apply these states through the Content component — not custom DIVs with spinners.
| State | How to apply | When |
|---|---|---|
| Loading | <Content loading> | Data is being fetched; hides children until ready |
| Empty | <Content none noneOptions={{ text: t('...') }}> | No data to display |
| Error toast | useMessage().error(t('...')) | Non-blocking operation failure |
| Error modal | useAlertModal().error(...) | Blocking error requiring user acknowledgment |
Priority order inside Content: loading → none → children. See pimcore-studio-ui-layout-components for the full Content API.
Always use a predefined modal size — never set an arbitrary width.
| Size key | Width |
|---|---|
| M | 530px |
| L | 700px |
| ML | 872px |
| XL | 1000px |
| XXL | max 1200px / 85vw |
Collapsible sections use BaseView / CollapseItem with named themes. Choose by visual hierarchy:
| Theme | Visual | Use for |
|---|---|---|
| card-with-highlight | White bg + primary-colored top separator | Default — most sections |
| fieldset | Light bg + left 3px accent border | Grouped form fields |
| border-highlight | White bg + left 3px neutral border | Sub-sections within a fieldset |
| default | White bg, optional border | Plain grouping without emphasis |
| success / error | Semantic green / red bg + border | Validation results, status panels |
createStyles from antd-style — never inline styles or raw CSS filestoken argument in createStyles(({ token, css }) => ({...}))studio-default-light) and dark (studio-default-dark) themes are active; dark extends light — components built with tokens adapt automaticallycreateStyles patternEvery new component must satisfy:
<button>, <nav>, <dialog>, etc.):focus-visible)t()❌ Using "Delete" for a UI-only removal
// "Delete widget" — but the widget data still exists in the backend
✅ Use "Remove" for non-destructive UI-only actions: "Remove widget"
❌ Executing a Delete without confirmation
// onClick={() => deleteAsset(id)} ← no confirmation
✅ Always confirm first via modal.confirm() or <Popconfirm>
❌ Mixing Title case and Sentence case on buttons
"Save Draft" ← Title case on an action button
"merge version" ← No capitalisation
✅ Sentence case on all action buttons: "Save draft", "Merge version"
❌ Noun-first action labels
"CSV Export", "Asset Upload", "Property Edit"
✅ Verb-first: "Export CSV", "Upload asset", "Edit property"
❌ Hardcoded user-facing strings
<Button>Save draft</Button>
✅ All strings through t():
<Button>{t('asset.save-draft')}</Button>
❌ Arbitrary spacing or color values
style={{ padding: '14px 18px', color: '#722ed1' }}
✅ Use spacing tokens via Box/Content props, and color tokens via createStyles:
// In createStyles: color: token.colorPrimary
// In JSX: <Box padding={{ x: 'normal', y: 'small' }}>
❌ Custom loading spinner instead of Content state
{isLoading && <Spin />}
{!isLoading && data && <List />}
✅ Use Content loading state:
<Content loading={isLoading}>
<List data={data} />
</Content>
❌ Placing a widget in the content area
// "Quick Stats" widget rendered inside the main editor content area
✅ Register as a left/right panel widget via WidgetManager
createStyles CSS-in-JS patternuseStudioModal / useFormModal hookstools
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
tools
Adding and customizing editor tabs in Pimcore Studio UI - tab managers, registration, override, permissions, and detachable tabs