frappe-ui-patterns
UI/UX patterns and guidelines derived from official Frappe apps (CRM, Helpdesk, HRMS). Use when designing interfaces for custom Frappe applications to ensure consistency with the ecosystem.
Best use case
frappe-ui-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
UI/UX patterns and guidelines derived from official Frappe apps (CRM, Helpdesk, HRMS). Use when designing interfaces for custom Frappe applications to ensure consistency with the ecosystem.
Teams using frappe-ui-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/frappe-ui-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How frappe-ui-patterns Compares
| Feature / Agent | frappe-ui-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
UI/UX patterns and guidelines derived from official Frappe apps (CRM, Helpdesk, HRMS). Use when designing interfaces for custom Frappe applications to ensure consistency with the ecosystem.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Frappe UI Patterns
UI/UX patterns and design guidelines extracted from official Frappe applications.
## When to use
- Designing UI for a new Frappe app
- Building CRUD interfaces with Frappe UI
- Implementing list views, detail panels, or forms
- Ensuring consistent UX with CRM, Helpdesk, HRMS
- Choosing component patterns and layouts
## Inputs required
- App type (CRM-like, Helpdesk-like, data management)
- Key entities and their relationships
- Primary user workflows
## Reference apps
Study these official apps for patterns:
| App | Repo | Key Patterns |
|-----|------|--------------|
| Frappe CRM | [github.com/frappe/crm](https://github.com/frappe/crm) | Lead/Deal pipelines, Kanban, activity feeds |
| Frappe Helpdesk | [github.com/frappe/helpdesk](https://github.com/frappe/helpdesk) | Ticket queues, SLA indicators, agent views |
| Frappe HRMS | [github.com/frappe/hrms](https://github.com/frappe/hrms) | Employee self-service, approvals, dashboards |
| Frappe Insights | [github.com/frappe/insights](https://github.com/frappe/insights) | Query builders, visualizations, dashboards |
| Frappe Builder | [github.com/frappe/builder](https://github.com/frappe/builder) | Drag-drop interfaces, property panels |
## Procedure
### 0) App shell structure
All Frappe apps follow a consistent shell:
```
┌─────────────────────────────────────────────────────────────┐
│ Header (App title, search, user menu) │
├──────────────┬──────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ │ │
│ - Nav items │ ┌─────────────────┬──────────────────────┐ │
│ - Filters │ │ List View │ Detail Panel │ │
│ - Actions │ │ │ │ │
│ │ │ │ │ │
│ │ └─────────────────┴──────────────────────┘ │
│ │ │
└──────────────┴──────────────────────────────────────────────┘
```
**Implementation:**
```vue
<template>
<div class="flex h-screen">
<!-- Sidebar -->
<Sidebar />
<!-- Main content with optional split view -->
<div class="flex-1 flex">
<ListView
:class="selectedDoc ? 'w-1/2' : 'w-full'"
@select="selectDoc"
/>
<DetailPanel
v-if="selectedDoc"
:doc="selectedDoc"
class="w-1/2 border-l"
/>
</div>
</div>
</template>
```
### 1) Sidebar patterns
**Standard structure:**
```vue
<template>
<aside class="w-56 border-r bg-gray-50 flex flex-col">
<!-- App logo/title -->
<div class="p-4 border-b">
<h1 class="font-semibold">My App</h1>
</div>
<!-- Primary navigation -->
<nav class="flex-1 p-2">
<SidebarLink
v-for="item in navItems"
:key="item.name"
:label="item.label"
:icon="item.icon"
:to="item.route"
:count="item.count"
/>
</nav>
<!-- Quick filters (context-dependent) -->
<div v-if="filters.length" class="p-2 border-t">
<p class="text-xs text-gray-500 px-2 mb-1">Filters</p>
<SidebarLink
v-for="filter in filters"
:key="filter.name"
:label="filter.label"
:count="filter.count"
@click="applyFilter(filter)"
/>
</div>
<!-- User/settings at bottom -->
<div class="p-2 border-t">
<UserMenu />
</div>
</aside>
</template>
```
**CRM example nav items:**
- Leads (with count badge)
- Deals (with count badge)
- Contacts
- Organizations
- Activities
- ---
- Settings
### 2) List view patterns
**Standard list with filters:**
```vue
<template>
<div class="flex-1 flex flex-col">
<!-- Toolbar -->
<div class="flex items-center justify-between p-4 border-b">
<div class="flex items-center gap-2">
<Input
type="search"
placeholder="Search..."
v-model="searchQuery"
:debounce="300"
/>
<FilterDropdown :filters="availableFilters" v-model="activeFilters" />
</div>
<div class="flex items-center gap-2">
<ViewToggle v-model="viewMode" :options="['list', 'kanban', 'grid']" />
<Button variant="solid" @click="createNew">
<template #prefix><FeatherIcon name="plus" /></template>
New
</Button>
</div>
</div>
<!-- View modes -->
<ListView v-if="viewMode === 'list'" :data="items" @row-click="select" />
<KanbanView v-else-if="viewMode === 'kanban'" :data="items" :columns="stages" />
<GridView v-else :data="items" @card-click="select" />
</div>
</template>
```
**List row structure:**
```vue
<template>
<div class="flex items-center p-3 hover:bg-gray-50 cursor-pointer border-b">
<!-- Selection checkbox (for bulk actions) -->
<Checkbox v-if="selectable" v-model="selected" class="mr-3" />
<!-- Avatar/icon -->
<Avatar :label="row.name" :image="row.image" class="mr-3" />
<!-- Primary content -->
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ row.title }}</p>
<p class="text-sm text-gray-500 truncate">{{ row.subtitle }}</p>
</div>
<!-- Status badge -->
<Badge :variant="statusVariant">{{ row.status }}</Badge>
<!-- Metadata -->
<span class="text-sm text-gray-500 ml-4">{{ timeAgo(row.modified) }}</span>
<!-- Actions dropdown -->
<Dropdown :options="rowActions" class="ml-2">
<Button variant="ghost" icon="more-horizontal" />
</Dropdown>
</div>
</template>
```
### 3) Kanban view patterns
**Used in:** CRM (Deals), Helpdesk (Tickets by status)
```vue
<template>
<div class="flex overflow-x-auto p-4 gap-4">
<div
v-for="column in columns"
:key="column.name"
class="flex-shrink-0 w-72 bg-gray-100 rounded-lg"
>
<!-- Column header -->
<div class="p-3 font-medium flex items-center justify-between">
<span>{{ column.label }}</span>
<Badge>{{ column.items.length }}</Badge>
</div>
<!-- Cards -->
<div class="p-2 space-y-2 min-h-[200px]">
<KanbanCard
v-for="item in column.items"
:key="item.name"
:data="item"
@click="select(item)"
draggable
@dragend="handleDrop"
/>
</div>
<!-- Add new in column -->
<Button variant="ghost" class="w-full" @click="addTo(column)">
+ Add {{ column.singular }}
</Button>
</div>
</div>
</template>
```
**Kanban card structure:**
```vue
<template>
<div class="bg-white rounded-lg p-3 shadow-sm border cursor-pointer hover:shadow">
<p class="font-medium mb-1">{{ data.title }}</p>
<p class="text-sm text-gray-500 mb-2">{{ data.subtitle }}</p>
<div class="flex items-center justify-between">
<Avatar :label="data.assigned_to" size="sm" />
<span class="text-xs text-gray-400">{{ data.due_date }}</span>
</div>
</div>
</template>
```
### 4) Detail panel / side panel
**Split view pattern (CRM/Helpdesk style):**
```vue
<template>
<aside class="w-[480px] border-l bg-white flex flex-col">
<!-- Header with close -->
<div class="flex items-center justify-between p-4 border-b">
<h2 class="font-semibold">{{ doc.name }}</h2>
<Button variant="ghost" icon="x" @click="$emit('close')" />
</div>
<!-- Tabs -->
<Tabs v-model="activeTab">
<Tab name="details" label="Details" />
<Tab name="activity" label="Activity" />
<Tab name="notes" label="Notes" />
</Tabs>
<!-- Tab content -->
<div class="flex-1 overflow-auto p-4">
<DetailsTab v-if="activeTab === 'details'" :doc="doc" />
<ActivityFeed v-else-if="activeTab === 'activity'" :doctype="doctype" :name="doc.name" />
<NotesTab v-else :doctype="doctype" :name="doc.name" />
</div>
<!-- Footer actions -->
<div class="p-4 border-t flex justify-end gap-2">
<Button @click="edit">Edit</Button>
<Button variant="solid" @click="primaryAction">{{ primaryActionLabel }}</Button>
</div>
</aside>
</template>
```
### 5) Form patterns
**Standard form layout:**
```vue
<template>
<div class="max-w-2xl mx-auto p-6">
<!-- Form header -->
<div class="mb-6">
<h1 class="text-xl font-semibold">{{ isNew ? 'New' : 'Edit' }} {{ doctype }}</h1>
</div>
<!-- Sections -->
<FormSection title="Basic Information">
<div class="grid grid-cols-2 gap-4">
<FormControl label="Name" v-model="doc.name" :required="true" />
<FormControl label="Status" type="select" v-model="doc.status" :options="statusOptions" />
</div>
<FormControl label="Description" type="textarea" v-model="doc.description" class="mt-4" />
</FormSection>
<FormSection title="Details" collapsible>
<!-- More fields -->
</FormSection>
<!-- Actions -->
<div class="flex justify-end gap-2 mt-6 pt-4 border-t">
<Button @click="cancel">Cancel</Button>
<Button variant="solid" @click="save" :loading="saving">Save</Button>
</div>
</div>
</template>
```
**FormSection component:**
```vue
<template>
<div class="mb-6">
<div
class="flex items-center justify-between mb-3 cursor-pointer"
@click="collapsible && (collapsed = !collapsed)"
>
<h3 class="font-medium text-gray-700">{{ title }}</h3>
<FeatherIcon v-if="collapsible" :name="collapsed ? 'chevron-down' : 'chevron-up'" />
</div>
<div v-show="!collapsed">
<slot />
</div>
</div>
</template>
```
### 6) Activity feed pattern
**Used across all apps for tracking changes:**
```vue
<template>
<div class="space-y-4">
<!-- Add comment -->
<div class="flex gap-3">
<Avatar :label="$user.name" />
<div class="flex-1">
<Textarea v-model="newComment" placeholder="Add a comment..." rows="2" />
<Button class="mt-2" @click="addComment" :disabled="!newComment">Comment</Button>
</div>
</div>
<!-- Activity items -->
<div v-for="item in activities" :key="item.name" class="flex gap-3">
<Avatar :label="item.owner" size="sm" />
<div class="flex-1">
<div class="flex items-baseline gap-2">
<span class="font-medium text-sm">{{ item.owner }}</span>
<span class="text-xs text-gray-400">{{ timeAgo(item.creation) }}</span>
</div>
<!-- Different activity types -->
<CommentContent v-if="item.type === 'comment'" :content="item.content" />
<StatusChange v-else-if="item.type === 'status'" :from="item.from" :to="item.to" />
<FieldChange v-else-if="item.type === 'change'" :field="item.field" :value="item.value" />
</div>
</div>
</div>
</template>
```
### 7) Empty states
**Always provide helpful empty states:**
```vue
<template>
<div class="flex flex-col items-center justify-center h-64 text-center">
<FeatherIcon name="inbox" class="w-12 h-12 text-gray-300 mb-4" />
<h3 class="font-medium text-gray-700 mb-1">{{ title }}</h3>
<p class="text-sm text-gray-500 mb-4">{{ description }}</p>
<Button v-if="action" variant="solid" @click="action.handler">
{{ action.label }}
</Button>
</div>
</template>
<!-- Usage -->
<EmptyState
title="No leads yet"
description="Create your first lead to get started"
:action="{ label: 'Create Lead', handler: createLead }"
/>
```
### 8) Loading states
**Skeleton loaders for perceived performance:**
```vue
<!-- List skeleton -->
<template>
<div v-if="loading" class="space-y-2 p-4">
<div v-for="i in 5" :key="i" class="flex items-center gap-3 p-3">
<Skeleton class="w-10 h-10 rounded-full" />
<div class="flex-1">
<Skeleton class="h-4 w-1/3 mb-2" />
<Skeleton class="h-3 w-1/2" />
</div>
</div>
</div>
<ListView v-else :data="data" />
</template>
```
### 9) Color and status conventions
| Status Type | Color | Usage |
|-------------|-------|-------|
| Success/Active | Green (`bg-green-100 text-green-700`) | Completed, Active, Resolved |
| Warning/Pending | Yellow (`bg-yellow-100 text-yellow-700`) | Pending, In Progress, Due Soon |
| Error/Blocked | Red (`bg-red-100 text-red-700`) | Failed, Blocked, Overdue |
| Info/Default | Blue (`bg-blue-100 text-blue-700`) | New, Open, Info |
| Neutral | Gray (`bg-gray-100 text-gray-700`) | Draft, Cancelled, Closed |
**Badge component usage:**
```vue
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Overdue</Badge>
<Badge variant="info">New</Badge>
<Badge variant="subtle">Draft</Badge>
```
### 10) Responsive patterns
**Mobile-first considerations:**
```vue
<template>
<!-- Hide sidebar on mobile, show as drawer -->
<Sidebar v-if="!isMobile" />
<Drawer v-else v-model="sidebarOpen">
<Sidebar />
</Drawer>
<!-- Stack list and detail on mobile -->
<div :class="isMobile ? 'flex-col' : 'flex'">
<ListView v-show="!isMobile || !selectedDoc" />
<DetailPanel v-if="selectedDoc" :fullScreen="isMobile" />
</div>
</template>
```
## Component reference
Use these Frappe UI components consistently:
| Component | Usage |
|-----------|-------|
| `<Button>` | All actions, with variants: solid, subtle, ghost |
| `<Input>` | Text inputs, search fields |
| `<FormControl>` | Form fields with labels, validation |
| `<Select>` | Dropdowns, status selectors |
| `<Checkbox>` | Boolean inputs, bulk selection |
| `<Avatar>` | User images, entity icons |
| `<Badge>` | Status indicators, counts |
| `<Dropdown>` | Action menus, context menus |
| `<Dialog>` | Modal confirmations, forms |
| `<Tabs>` | Content organization |
| `<Tooltip>` | Helpful hints, truncated text |
## Verification
- [ ] App shell matches standard layout (sidebar + main + optional detail)
- [ ] List views have search, filters, view toggle, create button
- [ ] Detail panel has tabs (Details, Activity, Notes)
- [ ] Empty states are helpful with actions
- [ ] Loading states use skeletons, not spinners
- [ ] Status colors follow conventions
- [ ] Forms are sectioned and consistent
- [ ] Mobile experience is considered
## Failure modes / debugging
- **Inconsistent spacing**: Use TailwindCSS spacing scale (p-2, p-4, gap-2, gap-4)
- **Wrong component**: Check Frappe UI docs for correct component and props
- **Broken responsiveness**: Test at mobile breakpoints; use `sm:`, `md:`, `lg:` prefixes
- **Missing states**: Ensure loading, empty, and error states are handled
## Escalation
- For component implementation → `frappe-frontend-development`
- For backend API integration → `frappe-api-development`
- For enterprise workflows → `frappe-enterprise-patterns`
## References
- [references/app-shell-patterns.md](references/app-shell-patterns.md) — Detailed shell layouts
- [references/component-patterns.md](references/component-patterns.md) — Component usage guide
- [references/mobile-patterns.md](references/mobile-patterns.md) — Responsive design
## Guardrails
- **Study official apps first**: Before designing UI, review CRM, Helpdesk, or relevant official app for patterns
- **Use Frappe UI components**: Never create custom components when Frappe UI has an equivalent
- **Follow spacing conventions**: Use consistent padding/margins (4px increments)
- **Provide all states**: Every view needs loading, empty, and error states
- **Keep navigation consistent**: Sidebar structure should match official apps
- **Test responsively**: Ensure mobile experience works
## Common Mistakes
| Mistake | Why It Fails | Fix |
|---------|--------------|-----|
| Custom app shell design | Unfamiliar UX for users | Copy CRM/Helpdesk shell structure |
| Missing empty states | Users confused when no data | Add EmptyState component with action |
| Spinner instead of skeleton | Jarring loading experience | Use Skeleton components for loading |
| Inconsistent status colors | User confusion | Follow color conventions table |
| Deep nesting without breadcrumbs | Users get lost | Add breadcrumb navigation |
| Modal overuse | Disruptive workflow | Prefer side panels for detail views |
| No keyboard navigation | Accessibility issues | Ensure Tab/Enter work for key flows |