Theme slots
Swap any of ten components inside the admin without forking the framework.
The second customization layer is L2: theme. flowpanel exposes ten
named slots — MetricCard, Button, Badge, Avatar, StatusBadge,
EmptyState, PageHeader, Pagination, ConfirmDialog, and
SkeletonTable — that you can replace with your own components via
theme.components.
Per invariant I-11, slot keys are append-only across minor versions.
Swap one
import { defineAdmin } from "@flowpanel/kit";
import { MyMetricCard } from "@/components/my-metric-card";
export default defineAdmin({
adapter: /* ... */,
auth: /* ... */,
theme: {
components: {
MetricCard: MyMetricCard,
},
},
resources: [/* ... */],
});theme.components is typed as Partial<FlowpanelComponentSlots>. Each
key carries the exact prop interface of the default — MetricCardProps,
ButtonProps, etc. — so prop typos and signature drift fail at compile
time.
The 10 slots
interface FlowpanelComponentSlots {
EmptyState: ComponentType<EmptyStateProps>;
MetricCard: ComponentType<MetricCardProps>;
Button: ComponentType<ButtonProps>;
Badge: ComponentType<BadgeProps>;
Avatar: ComponentType<AvatarProps>;
StatusBadge: ComponentType<StatusBadgeProps>;
PageHeader: ComponentType<PageHeaderProps>;
Pagination: ComponentType<PaginationProps>;
ConfirmDialog: ComponentType<ConfirmDialogProps>;
SkeletonTable: ComponentType<SkeletonTableProps>;
}The interface is defined in @flowpanel/core and augmented by
@flowpanel/react with the shipped 10. Your override for Button
should be forwardRef-aware to avoid warnings from Radix UI when
the Button is used with asChild.
Adding your own slot keys
You can extend FlowpanelComponentSlots with extra slot keys via
module augmentation. This is how you'd register a slot your custom
widgets read from useComponents():
import type { ComponentType } from "react";
import type { MyToolbarProps } from "@/components/my-toolbar";
declare module "@flowpanel/core" {
interface FlowpanelComponentSlots {
MyToolbar: ComponentType<MyToolbarProps>;
}
}After augmentation, theme.components.MyToolbar = MyToolbar is
type-checked, and useComponents().MyToolbar returns a typed
component.
Dark mode and persistence
FlowPanel ships a small theme runtime alongside the slot registry:
<ThemeScript defaultMode="auto" />— drop this into the<head>of your root layout (setsuppressHydrationWarningon the host<html>element). The inline script applies the persisted theme before React hydration, so the page never flashes the wrong palette.useTheme()— client hook that returns{ theme, toggle, setTheme }. Persists explicit user choices tolocalStorageunderfp-themeand watchesprefers-color-schemewhile no explicit choice exists.
import { ThemeScript } from "@flowpanel/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ThemeScript defaultMode="auto" />
</head>
<body>{children}</body>
</html>
);
}When to eject
Theme slots replace components, not behavior. If you need to change how a page works — say, custom data fetching for one resource — move up to L3: eject.