Extending the Umbraco backoffice has always been a key part of its developer-friendly ecosystem. With the new backoffice, Umbraco introduces modern tools like Lit, Vite, and a UI Component Library, enabling developers to create powerful extensions without being tied to outdated frameworks. Goodbye AngularJS, hello flexibility!
Embracing Flexibility with Lit and Beyond
Officially, Umbraco recommends using Lit for building extensions—and with good reason. Lit generates Custom Elements and fully adheres to the Custom Element API, making it an excellent choice for creating a highly extensible, complex CMS. Lit ensures all components work seamlessly together, maintaining consistency and scalability across the entire platform. For large and intricate extensions, Lit is undoubtedly the way to go.
But what about smaller, more focused extensions? Often, simplicity can be just as powerful. For extensions aimed at improving the content editor’s experience, leveraging tools like Vite with frameworks such as Vue or React can provide an effective alternative without the overhead of learning a new framework. This approach allows developers to enhance the backoffice with minimal friction, fostering creativity and accessibility for a broader range of developers.
Why Use Familiar Frameworks?
Countless front-end developers working with Umbraco already excel at building reactive UIs using their framework of choice. By leveraging Vite’s ability to integrate with Web Components, developers can skip the hurdle of learning Lit and instead use their existing expertise with Vue, React, or whatever Javascript framework of chocie. This enables developers to:
Build extensions with their preferred tools.
Reduce learning curves.
Focus on improving the content editor’s experience.
This approach lowers barriers, makes package development exciting, and ensures that developers feel empowered to create extensions tailored to their workflows.
In this article, we’ll explore how you can extend the Umbraco backoffice, starting with a simple Vanilla Javascript apporach, and then expanding into how other frameworks can fit seamlessly into the ecosystem—all while adhering to the standards of the Custom Element API.
Getting Started: Vanilla JavaScript
Umbraco should be extensible. And build how you want to build can also mean bring your own framework or stack.
To demonstrate the possibilities, let’s start with a look at the simplest way to create a backoffice extension using vanilla JavaScript.
In which by far the most power lies in extending the UmbElementMixin.
This ensures seamless integration with the Backoffice state and provides the functionality of the Context API.
The Lit component
While the vanilla component provides a simple entry point, leveraging Lit unlocks more advanced features and scalability for complex extensions.
Using Lit is considered best practice in Umbraco, as it offers the cleanest and most efficient way to create extensions. However, for the sake of brevity, I won’t dive into a detailed example of this approach here.
If you’re interested in setting up a Lit-based extension, I recommend exploring the Vite Package Setup documentation provided by Umbraco, which offers a comprehensive guide.
Taking It Further: Using Other Frameworks
Utilizing the power of Vite
Vite is so powerful and easy to use.
It is truly an integration for your front-end bundle and it's very good at bringing front-end frameworks together. We as front-end developers love this tool. Lots of Umbraco developers also use it to bundle the front-end files for their Umbraco websites, like explained in this article. But the question is, can we also utilize the other templates of Vite for extending the backoffice?
Would be cool if we use these also, right?
The supported template presets of Vite
The Vue component
Mounting an app should be easy, but the more fun part is: we can even use the Context API.
The setup will be fine as long as it comes together as a webcomponent (which gets referenced in the umbraco-package.json).
Folder structure after creating Vite with template vue-ts
Now edit the vite.config.ts based on the Umbraco settings, but with some Vue specific settings for the compilor options. Which is at the bottom of this file.
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
build: {
lib: {
entry: "src/main.ts", // your web component source file
name: "hello",
formats: ["es"],
},
outDir: "../../App_Plugins/Vue", // all compiled files will be placed here
emptyOutDir: true,
sourcemap: true,
rollupOptions: {
external: [/^@umbraco/], // ignore the Umbraco Backoffice package in the build
},
},
define: {
'process.env.NODE_ENV': true
},
base: "/App_Plugins/Vue/", // the base path of the app in the browser (used for assets)
plugins: [
vue({
template: {
// this is important, because Vue needs to recognize the uui Umbraco components as Custom Elements and not as Vue components
compilerOptions: {
isCustomElement: tag => tag.startsWith("uui-"),
},
},
})
],
});
vite.config.ts
We have correctly set the vite.config.ts settings for a Vue bundle and now continue with mounting the actual app within an UmbElementMixin web component.
The code above results in the video below. As you can see, the notifications can be triggered through the Context API and the username can be displayed on the Vue template.
Because the rootNode of the mount element and it's host is an UmbLitElement we type it that way in the App and from that part on we even get all the Context API type definitions!
Vue extension with similar functionality as the Vanilla component
Cool right? That's all that is needed to mount a Vue app and consume the Context API.
But this is a Dashboard App, another interesting use case for simple extensions is of course the creation of a custom Property Editor.
The Vue property editor
For the property editor the same vite.config.ts and mounting mechanism applies. It's just that instead of consuming the Context API from the host. We set the value of the host and dispatch the UmbPropertyValueChangeEvent:
Which results in this field with a character counter that turns red after 80 characters:
Vue Property Editor with a character counter
The Vue example demonstrates how easily you can mount your favorite JavaScript framework while ensuring you have access to the host component—the web component extending the UmbLitElement. This approach isn’t limited to Vue; the same principles can be applied to any framework supported by Vite templates, such as React or Svelte. The flexibility provided by this setup allows developers to integrate their preferred tools seamlessly, making the new Umbraco backoffice a truly open and adaptable environment for modern web development.
The React component
So what would a React component look like? You can use the Vite template for React. The same mounting mechanism and vite.config.ts applies as Vue. Only the App.vue will be different of course:
import React, { useEffect, useRef, useState } from 'react';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import { UUIBoxElement } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from '@umbraco-cms/backoffice/notification';
interface Props {
mountElem: HTMLElement;
}
declare global {
namespace JSX {
interface IntrinsicElements {
'uui-box': React.DetailedHTMLProps<any, any>;
}
}
}
const ReactApp: React.FC<Props> = ({ mountElem }) => {
const rootElement = useRef<HTMLDivElement>(null);
const [username, setUsername] = useState<string>('');
const [notificationInstance, setNotificationInstance] = useState<UmbNotificationContext>();
useEffect(() => {
const host = (mountElem.getRootNode() as any).host as UmbLitElement;
host.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => {
context.currentUser.subscribe((user) => {
setUsername(user?.name ?? '');
});
});
host.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
setNotificationInstance(instance);
})
}, [mountElem]);
function sendNotification() {
notificationInstance?.peek("positive", {
data: {
message: "Wow, I sent a notification from within a React sapp",
}
})
}
return (
<div ref={rootElement}>
<uui-box headline={`Hello ${username} from React!`} headline-variant="h5">
<svg width="200px" height="200px" viewBox="-10.5 -9.45 21 18.9" style={{ color: '#58c4dc' }} fill="#58c4dc" xmlns="http://www.w3.org/2000/svg" ><circle cx="0" cy="0" r="2" fill="currentColor"></circle><g stroke="currentColor" strokeWidth="1" fill="none"><ellipse rx="10" ry="4.5"></ellipse><ellipse rx="10" ry="4.5" transform="rotate(60)"></ellipse><ellipse rx="10" ry="4.5" transform="rotate(120)"></ellipse></g></svg>
<p>
This is a UUI Box element. Unfortunately, it doesn’t work to use
uui-button element, for example, because the extension of UmbElement
causes issues within React.
</p>
<button onClick={() => { sendNotification() }}>Notification</button>
</uui-box>
</div>
);
};
export default ReactApp;
App.tsx
The App.tsx mounted as a React component will result in the following:
The React Component
I think you get the point and I think it's facinating how frameworks can come together in a Web Components world in which the Context API is a truely native Javascript state management machine.
As an extra bonus we can even put this idea into overdrive. In the video below Vue, React, Angular and (yes I know) even jQuery can come together in the same screen.
Awesome right?
Vue React Angular and jQuery in the same backoffice
If you're curious for the full code of these Umbraco Extensions you can find them here.
Pros and Cons of this flexible approach
Of course, this approach deviates from the officially recommended path and may feel a bit unconventional. However, in my experience as a front-end web developer, there’s often room for creative solutions that may not strictly adhere to best practices. These so-called 'hacky' approaches can serve a purpose, especially when they make life easier for content editors or make developers excited to make awesome extensions.
Pros:
Attracting more developers to the Umbraco ecosystem by lowering barriers to entry
Empowering developers to use the tools they already know and love, reducing learning curves and increasing enthusiasm
Enhancing the content editor experience with creative, custom extensions tailored to specific needs
Cons:
Increased bundle sizes, for each extension the framework specific bundle will be included within its javsacript bundle. This can be mediated to make the framework specific versions external source for Vite. But still this comes with a lot of complexity.
Potentially fragmented ecosystem if too many varied approaches are used, making maintainability a challenge for community-shared packages
Conclusion
When creating a large package that can be used by others in the community or even extended, then of course it's probably not so smart to go this route. But the truth is. We have lots of internal packages, custom dashboarding or custom client specific property editors. Therefore it's all about empowering the Content Editor.
Developers should be happy to empower the Content Editor and extend their experience within the backoffice. And if the developers can use the framework they like (and also use for the front-end of the Umbraco websites), then they will be more excited to create useful extensions.
So my message is simple: Build how you want to build.