Homepage customization
The Homepage is the landing page of the Strapi admin panel. By default, it provides an overview of your content with 5 default widgets:
- Last edited entries: Displays recently modified content entries, including their content type, status, and when they were updated.
- Last published entries: Shows recently published content entries, allowing you to quickly access and manage your published content.
- Profile: Displays a short summary of your profile, including your name, email address, and role.
- Entries: Displays the total number of Draft & Published entries.
- Project statistics: Displays statistics about your entries, content-types, locales, assets, and more.


These default widgets cannot currently be removed, but you can customize the Homepage by creating your own widgets.
If you recently created a Strapi project, the Homepage may also display a guided tour above widgets if you haven't skipped it yet (see Admin Panel documentation for details).
Adding custom widgetsβ
To add a custom widget, you can:
- install a plugin from the Marketplace
- or create and register your own widgets
The present page will describe how to create and register your widgets.
Registering custom widgetsβ
To register a widget, use app.widgets.register()
:
- in the pluginβs
register
lifecycle method of theindex
file if you're building a plugin (recommended way), - or in the application's global
register()
lifecycle method if you're adding the widget to just one Strapi application without a plugin.
The examples on the present page will cover registering a widget through a plugin. Most of the code should be reusable if you register the widget in the application's global register()
lifecycle method, except you should not pass the pluginId
property.
- JavaScript
- TypeScript
import pluginId from './pluginId';
import MyWidgetIcon from './components/MyWidgetIcon';
export default {
register(app) {
// Register the plugin itself
app.registerPlugin({
id: pluginId,
name: 'My Plugin',
});
// Register a widget for the Homepage
app.widgets.register({
icon: MyWidgetIcon,
title: {
id: `${pluginId}.widget.title`,
defaultMessage: 'My Widget',
},
component: async () => {
const component = await import('./components/MyWidget');
return component.default;
},
/**
* Use this instead if you used a named export for your component
*/
// component: async () => {
// const { Component } = await import('./components/MyWidget');
// return Component;
// },
id: 'my-custom-widget',
pluginId: pluginId,
});
},
bootstrap() {},
// ...
};
import pluginId from './pluginId';
import MyWidgetIcon from './components/MyWidgetIcon';
import type { StrapiApp } from '@strapi/admin/strapi-admin';
export default {
register(app: StrapiApp) {
// Register the plugin itself
app.registerPlugin({
id: pluginId,
name: 'My Plugin',
});
// Register a widget for the Homepage
app.widgets.register({
icon: MyWidgetIcon,
title: {
id: `${pluginId}.widget.title`,
defaultMessage: 'My Widget',
},
component: async () => {
const component = await import('./components/MyWidget');
return component.default;
},
/**
* Use this instead if you used a named export for your component
*/
// component: async () => {
// const { Component } = await import('./components/MyWidget');
// return Component;
// },
id: 'my-custom-widget',
pluginId: pluginId,
});
},
bootstrap() {},
// ...
};
The app.widgets.register
API only works with Strapi 5.13 and above. Trying to call the API with older versions of Strapi will crash the admin panel.
Plugin developers who want to register widgets should either:
-
set
^5.13.0
as their@strapi/strapi
peerDependency in their pluginpackage.json
. This peer dependency powers the Marketplace's compatibility check. -
or check if the API exists before calling it:
if ('widgets' in app) {
// proceed with the registration
}
The peerDependency approach is recommended if the whole purpose of the plugin is to register widgets. The second approach makes more sense if a plugin wants to add a widget but most of its functionality is elsewhere.
Widget API referenceβ
The app.widgets.register()
method can take either a single widget configuration object or an array of configuration objects. Each widget configuration object can accept the following properties:
Property | Type | Description | Required |
---|---|---|---|
icon | React.ComponentType | Icon component to display beside the widget title | Yes |
title | MessageDescriptor | Title for the widget with translation support | Yes |
component | () => Promise<React.ComponentType> | Async function that returns the widget component | Yes |
id | string | Unique identifier for the widget | Yes |
link | Object | Optional link to add to the widget (see link object properties) | No |
pluginId | string | ID of the plugin registering the widget | No |
permissions | Permission[] | Permissions required to view the widget | No |
Link object properties:
If you want to add a link to your widget (e.g., to navigate to a detailed view), you can provide a link
object with the following properties:
Property | Type | Description | Required |
---|---|---|---|
label | MessageDescriptor | The text to display for the link | Yes |
href | string | The URL where the link should navigate to | Yes |
Creating a widget componentβ
Widget components should be designed to display content in a compact and informative way.
Here's how to implement a basic widget component:
- JavaScript
- TypeScript
import React, { useState, useEffect } from 'react';
import { Widget } from '@strapi/admin/strapi-admin';
const MyWidget = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
// Fetch your data here
const fetchData = async () => {
try {
// Replace with your actual API call
const response = await fetch('/my-plugin/data');
const result = await response.json();
setData(result);
setLoading(false);
} catch (err) {
setError(err);
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return <Widget.Loading />;
}
if (error) {
return <Widget.Error />;
}
if (!data || data.length === 0) {
return <Widget.NoData />;
}
return (
<div>
{/* Your widget content here */}
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default MyWidget;
import React, { useState, useEffect } from 'react';
import { Widget } from '@strapi/admin/strapi-admin';
interface DataItem {
id: number;
name: string;
}
const MyWidget: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true);
const [data, setData] = useState<DataItem[] | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// Fetch your data here
const fetchData = async () => {
try {
// Replace with your actual API call
const response = await fetch('/my-plugin/data');
const result = await response.json();
setData(result);
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return <Widget.Loading />;
}
if (error) {
return <Widget.Error />;
}
if (!data || data.length === 0) {
return <Widget.NoData />;
}
return (
<div>
{/* Your widget content here */}
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
};
export default MyWidget;
For simplicity, the example below uses data fetching directly inside a useEffect hook. While this works for demonstration purposes, it may not reflect best practices in production.
For more robust solutions, consider alternative approaches recommended in the React documentation. If you're looking to integrate a data fetching library, we recommend using TanStackQuery.
Data management:
The green box above represents the area where the userβs React component (from widget.component
in the API) is rendered. You can render whatever you like inside of this box. Everything outside that box is, however, rendered by Strapi. This ensures overall design consistency within the admin panel. The icon
, title
, and link
(optional) properties provided in the API are used to display the widget.