# Frappe UI 1.0 Release

> Source: <https://frappe.io/blog/engineering/introducing-frappe-ui-1>
> Published: 2026-10-01 16:32:06+00:00

Frappe UI 1.0 is out. It's been four years since frappe-ui v0 was published on npm. I should have done this sooner, but better late than never.

It has 100+ components, automatic dark mode, real world examples, full screen recipes, utilities that connect to backend data, and it is AI ready. An all-in-one toolkit for building modern Frappe frontends. The rest of this post is about how it got here, because a lot of decisions went into it and I want to explain them.

## How it started

Frappe UI started as a `components` folder in the Frappe Books project. Button, Avatar, Badge, and a few more. When I moved to Frappe Cloud in 2020, I copied the folder over, like any naive engineer. Then in 2022 I was starting Gameplan and did not want to copy paste again. So I extracted it into an npm package.

Fast forward four years and we are on version 1.

After Frappe Cloud we started churning out apps. Gameplan, Drive, Helpdesk, Insights, Builder, HR, CRM, Learning, Mail, Studio, Wiki, Writer, Calendar, and it has not stopped. We are close to 25 apps now, excluding the tiny ones. It is very hard to build a UI library that has to work for all of them.

## Why version one, why now

v0 was a license to break things. I am never happy with an API. There is always one more thing to add, one name that could be better, one prop that means two things. When there were three apps this was fine.

With 25 apps it was not. Studio is built on top of Frappe UI, so every time a release changed a component, Studio's source had to change too. Other apps got hit too. And since everyone started building with agents, the number of apps depending on Frappe UI is going up faster than before. The team asked for a stable version.

So in March I started working on version one seriously. I thought it would take a few months. It took six.

## Good components, bad library

Frappe UI v0 was built by me and a few teammates over four years. Each component looked fine on its own. When I looked at all of them together, they did not fit.

Switch had a `toggle` event. That makes sense, you toggle a switch. But you almost always use a Switch next to other inputs, and all other inputs have a `change` event. Also, if you are using a v-for loop you wont have to make an exception for Switch. So the right design is `change` for Switch too.

Every chart had its own click event. `datapoint-click` on BarChart, `slice-click` on DonutChart, `stage-click` on FunnelChart, `cell-click`, `link-click`, `point-click`. Six names for one behaviour.

Dialog, Popover and BottomSheet each had a prop for "the user cannot close this by accident". `disableOutsideClickToClose`, `hideOnBlur`, `swipeDownToClose`. Very explicit. But there can be many ways to trigger a behaviour, escape, click outside, swipe down, and the behaviour is the same. Dismissing the thing.

Button shipped with `icon-left` and `icon-right` slots. Now they are `prefix` and `suffix`, and those names are shared across every component that has something before or after its content. TimePicker had `minTime` and `maxTime`. DatePicker had `minDate` and `maxDate`. Rating had `rating_from`. Each one made sense alone. Side by side, cutting all of them down to `min` and `max` made more sense.

I am not blaming anyone for the old names. Some of them were mine. It is hard to design a good API one component at a time. You fix Dialog in one month and Popover six months later and you never see both together.

The number I am most happy about from all this: Frappe UI now has 727 props across all components, and only 188 unique prop names. The number of names did not grow with the number of props.

## Everyone wanted one more Combobox prop

A lot of the API in 1.0 came from looking at what apps did when I couldn't think of them myself.

Combobox is the one example. You type something, the item is not in the list, and you expect to create it. v0 did not have this. So out of 25 apps, every one that needed it invented its own. Frappe Cloud added `allowCustomValue`. Someone added `creatable`. Insights added `allowCreate` and a `@createOption` event. Someone else made `options` accept a function that returns a promise.

I refused to build any of those, because in my mind "create" meant a different thing each time. In CRM, creating a contact means opening a new contact dialog. In the same app, creating a lost reason means creating it inline and refreshing the list. There is no one implementation that works for every app. And if I had listened to all the requests, Combobox would have `multi`, `searchable`, `creatable` and `clearable` as booleans, which is sixteen combinations I would have to test, and four props we could never remove.

So I did not add the prop. I added a way to put custom options in the list.

``` js
<script setup>
const options = computed(() => [
  ...tags.value,
  {
    type: 'custom',
    key: 'create',
    slot: 'create',
    condition: ({ query }) => query && !tags.value.includes(query),
    onClick: ({ query }) => createTag(query),
  },
])
</script>

<template>
  <Combobox v-model="tag" :options="options">
    <template #item-create="{ query }">Create "{{ query }}"</template>
  </Combobox>
</template>
```

Three hooks. A condition for when to show it, a slot for how it looks, and an `onClick` for what happens. By combining them you can build every scenario the four apps had, and Combobox has no new boolean.

I don't think the forks were a mistake. I cannot think of every use case on day one. Apps building their own version and the best one coming back to the library is part of the process. The hard part is finding the one API that works for 25 apps.

## A rich text editor cannot be standardised

In v0 I shipped a TextEditor. You pass a value, you get an editor with a toolbar. When I checked how people used it, almost every app had customised it. A different toolbar in one app, a comment system bolted on in another, a floating menu in a third. I went deep into it and realised a rich text editor cannot be standardised. The same editor needs to look like a full page document in Writer and like a single line in a comment box.

So `frappe-ui/editor` does not ship a ready-made editor. It ships parts that you compose. The editor, a fixed menu, a floating menu, a bubble menu, a table menu, and kits of extensions. The prose styles and the look of the menus are still decided for you.

```
<Editor :content="value" :extensions="[CommentKit]">
  <FloatingToolbar />
  <BubbleToolbar />
  <SaveDiscardButtons />
</Editor>
```

Lists are the same. `frappe-ui/list` is rows, cells, headers and groups. Not one ListView with forty props.

Once I saw this, I could see the whole library on one spectrum. Some components do not let you change how they look, like Badge and Switch. Some allow customisation through slots, like Button and Dialog. Then there are mid-size components like SettingsDialog and Sidebar, where the outer shell has to look exactly the same in every app but the inner panels are yours. And then there are fully composable ones like Editor and List, because they cannot be standardised.

Each component belongs at one point on this spectrum. A good part of the six months went into moving components to the right point. If I had listened to each request one at a time, I would have ended up with a Frankenstein of components with no harmony.

## Small opinionated decisions

What does it take to build a dropdown menu? Everyone has used one. Grouping, submenus, keyboard navigation, focus management, escape and outside click, collision detection so it flips when the button is at the edge of the screen, and the submenu has to flip too, screen reader semantics, typeahead, touch, long labels, disabled items, reduced motion. That is a partial list and I am sure I am missing things. One of the engineers Pedro Duarte, who built Radix UI has a talk where he says they spent six months on the dropdown menu. This was before AI.

When we wrote code by hand, the process was slow enough that you had time to think about each interaction. Agents have made us lazy. I have seen people prompt "add a dropdown". A production dropdown cannot be built in one-shot, it might not take six months but it will take a lot of iterations to get it right.

So the decisions are made once, in the component, and you do not have to prompt for them. Some examples.

Tooltips. When you hover the first button in a toolbar there is a delay. When you move to the next button there is no delay, because you are quickly exploring what each button does. Frappe UI does that.

Toasts. You edit five fields on a settings page and each one saves. A naive implementation shows five toasts. Frappe UI reuses the same toast, so there is no spam.

Chart labels. Some labels share a prefix, like "Frappe Cloud Enterprise" and "Frappe Cloud Standard". ECharts cuts them at the end, and you get two labels that say "Frappe Cloud…". Frappe UI cuts them in the middle, so even if they are not fully readable, you can make out that they are different values.

Dark mode elevation. In light mode a popover looks closer to you because it has a shadow. In dark mode a shadow does not show. So there are `bg-surface-elevation-1`, `-2` and `-3`, and in dark mode each one is a slightly lighter gray, because the thing nearest to you gets more light. In light mode they stay white.

There are hundreds of these. I hope I have made my point.

## How the decisions got made

I cannot talk about 1.0 without talking about AI. I have not written a line of code by hand in more than a year. Claude is always there.

Before AI, I worked on one component at a time. Now I take bigger chunks. Combobox, MultiSelect, Select and Dropdown together, because they look and feel similar and their APIs have to be designed together.

Then research. LLMs are great at it. A prompt like "I am working on combobox, multiselect, select, dropdown. Look at their usages in Builder, CRM, Insights, Studio, Helpdesk and Suite and suggest improvements" will give me a lot of useful usage backed suggestions.

Then a spec. For those four components there is a `spec/selection.md` that defines how each one behaves, with code samples, so I can see what app code will look like before the component exists. I spent a lot of time reading specs and iterating on them.

I also work on Gameplan. I wear both hats. When I work on Gameplan I look at Frappe UI as an app developer, and when I work on Frappe UI I do library design. Almost everything on a Gameplan screen, the rail, the sidebar, the header, the reactions, ships from Frappe UI. Gameplan is about 60% Frappe UI code surface area wise. Every migration was applied to Gameplan first.

In one of the migration audits, the agent recommended keeping the mixed `to` and `link` props for navigation, because 32 call sites was too many to break for naming consistency, I took the break anyway. `route` and `href` are the right names and the rename is worth it.

## DRY in the age of AI

Before, DRY meant you saw repeated code and pulled it into a function. Now the agent does that. What are you repeating in the age of AI? The repetition is in prompts. Don't do this. Do this instead. Follow this. Those repetitions are decisions, and humans mostly do not document their decisions. I think now you have to.

Frappe UI has a `PHILOSOPHY.md` with fifteen principles. Name behaviours, not interactions. Prefer v-model. Split components instead of overloading them. No class props. Every review and every new component is checked against it.

The toast rule, the alignment rule, gray first and red only for danger, one primary action per screen, those live in `SKILL.md`. Install the skill and Claude Code, Cursor or Codex follow the same rules.

```
npx skills add https://github.com/frappe/frappe-ui/tree/main/skills/frappe-ui
```

I have spent my tokens making these decisions. You don't have to.

## Espresso 2.0

A lot of the time also went into the design system layer, which we did with Timeless, a design studio in Chennai. They have designed with us since 2020, Frappe Books first, then Frappe Cloud, then most of the product line, and they pulled the shared patterns into a design system called Espresso.

This year we had so many screens that the patterns had drifted. So we put the designs of every Frappe product on one canvas, reviewed each pattern, and unified the recurring ones. That is Espresso 2.0. It is tested in the apps we use every day. Frappe UI 1.0 is Espresso 2.0 in code.

## What is in the box

Tokens. Colors are `surface`, `ink` and `outline`. Each has a light and a dark value, and they flip under `[data-theme="dark"]`. There is no `dark:` class in Frappe UI and you do not need one in your app. Typography, radius and shadows work the same way.

```
<div class="bg-surface-white text-ink-gray-8 border-outline-gray-2">
  <!-- correct in both modes -->
</div>
```

We have primitive components like Badge, Button, Avatar, then compound components like Sidebar, Dialog, and fully composable components like Rich Text Editor and List views. Built on Vue 3, Tailwind, Reka UI, keyboard accessible, TypeScript.

Every Frappe document has a `name`, `creation`, `modified` and `owner`, and every list endpoint pages the same way, so the data layer can be smarter than a generic fetch. `useDoc` keeps one copy of each document, so two screens always are in sync. `useList` rows are the same objects, so saving a document updates every list that has it. Responses are cached in memory and IndexedDB per user, so when you come back to the app you see the last data at once while a fresh copy loads. No spinner.

``` js
const task = useDoc({ doctype: 'GP Task', name: 'TASK-0042' })
await task.setValue.submit({ status: 'Done' })
// every list with TASK-0042 shows Done
```

Here is a task list. Light and dark are the same file.

``` js
<script setup>
import { useList } from 'frappe-ui'
import { List, ListRow, ListCell } from 'frappe-ui/list'

const tasks = useList({
  doctype: 'GP Task',
  fields: ['name', 'title', 'status', 'assigned_to', 'modified'],
  filters: { status: 'Open' },
  orderBy: 'modified desc',
})
</script>

<template>
  <List :rows="tasks.data">
    <ListRow v-for="task in tasks.data" :key="task.name" :row="task">
      <ListCell>{{ task.title }}</ListCell>
      <ListCell><Badge>{{ task.status }}</Badge></ListCell>
      <ListCell><Avatar :label="task.assigned_to" /></ListCell>
    </ListRow>
  </List>
</template>
```

Recipes. Eight full screens, each with a desktop and a mobile version. Discussions, compose, deals, tickets, mail, files, tasks, accounting. They are live on the docs site, you can click around, and the code is there for you or your agent to refer to.

Docs. Every component has a playground, real world examples of when to use which one, documented behaviours, and an API reference. ui.frappe.io.

## Upgrading from 0.x

1.0 is a breaking release. The migration guide is long because every change has a before and an after. Point your agent to the migration guide, and review what it did. I expect one to three days for most apps including review.

There will be no more 0.x releases. It is not supported and not recommended.

## What's next

Data Fetching layer is in review. A typed client with types generated from your DocTypes, realtime updates over Socket.IO, and optimistic writes with automatic rollback. In the frontend world, some call this a sync engine.

And more recipes. Recipes are helpful for humans as well as agents. They document the right frappe-ui patterns and also remain flexible to adopt for your use cases.

## Try it

```
npm create frappe-ui@latest
```

Docs at [ui.frappe.io](https://ui.frappe.io). If you are on 0.x, upgrade and tell me what broke.

If you write Vue and do not use Frappe, the components, tokens and recipes work without a Frappe backend.

Watch the IndiaFOSS talk
