Repository: widgetbook/widgetbook
Pull request #1443: feat: add `header` parameter
Author: login: Sourav-Sonkar
Created: 2025-05-06T17:07:59Z | Merged: 2025-05-13T15:22:23Z

# Add custom header support to Widgetbook sidebar

This PR adds the ability to display a custom header at the top of the Widgetbook navigation sidebar. This feature allows users to add branding, descriptions, or any custom content to personalize their Widgetbook instance.

### List of issues which are fixed by the PR
- Fixes #1438: Allow custom header in the Widgetbook UI sidebar

<img width="1183" alt="Screenshot 2025-05-06 at 10 45 34 PM" src="https://github.com/user-attachments/assets/008d9ab9-e15c-4cf4-b61e-1b47052d18da" />
<img width="211" alt="Screenshot 2025-05-06 at 10 47 00 PM" src="https://github.com/user-attachments/assets/5024905e-f07a-4977-a39a-3e361450fa57" />



### Implementation details
- Added `customHeader` parameter to the `Widgetbook` class and its constructors
- Added `customHeader` field to the `WidgetbookState` class
- Modified the `NavigationPanel` widget to display the custom header
- Updated the `ResponsiveLayout` to pass the custom header to the navigation panel
- Created an example implementation in `examples/custom_header_example`

### Checklist

- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making.
- [x] All existing and new tests are passing.


Linked issue #1438: [Feature Request] Allow custom header in the Widgetbook UI sidebar.
### Description

Ability to add a custom header would be nice for branding and description 

Something like this: 

![Image](https://github.com/user-attachments/assets/1ab9589b-3141-4396-a7ab-ded483ca9e0a)

Diff:
diff --git a/docs/guides/customization.mdx b/docs/guides/customization.mdx
index 38eba13a1..158a51ad4 100644
--- a/docs/guides/customization.mdx
+++ b/docs/guides/customization.mdx
@@ -152,6 +152,21 @@ Widget build(BuildContext context) {
 }
 ```
 
+## Header Widget
+
+You can add a custom header to the navigation panel by providing a `header` parameter to the `Widgetbook` constructor.
+This can be used for branding or additional information.
+
+```dart
+@override
+Widget build(BuildContext context) {
+  return Widgetbook(
+    header: const MyCustomHeaderWidget(),
+    // ...
+  );
+}
+```
+
 ## Widgetbook's UI
 
 If you want to change some Widgetbook's UI colors, or want to enforce a specific theme mode, you can do so using these parameters:
diff --git a/packages/widgetbook/CHANGELOG.md b/packages/widgetbook/CHANGELOG.md
index 4f5b9e268..5a3a48416 100644
--- a/packages/widgetbook/CHANGELOG.md
+++ b/packages/widgetbook/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- **FEAT**: Add [`header` parameter](https://docs.widgetbook.io/guides/customization#header-widget) to Widgetbook to allow adding a custom header to the navigation sidebar. ([#1443](https://github.com/widgetbook/widgetbook/pull/1443) - by [@Sourav-Sonkar](https://github.com/Sourav-Sonkar))
 - **FEAT**: Add [`panels` query param](https://docs.widgetbook.io/guides/embedding#customizing-panels) to show/hide panels when embedding Widgetbook. ([#1439](https://github.com/widgetbook/widgetbook/pull/1439))
 - **FIX**: Ensure a fresh state is used when building the use-case. This prevented some rebuilds from happening, causing knobs to not be registered properly. ([#1441](https://github.com/widgetbook/widgetbook/pull/1441))
 - **FIX**: Handle `null` in `durationOrNull` knob. ([#1444](https://github.com/widgetbook/widgetbook/pull/1444))
diff --git a/packages/widgetbook/lib/src/layout/responsive_layout.dart b/packages/widgetbook/lib/src/layout/responsive_layout.dart
index 6487c0065..ecbda6101 100644
--- a/packages/widgetbook/lib/src/layout/responsive_layout.dart
+++ b/packages/widgetbook/lib/src/layout/responsive_layout.dart
@@ -19,6 +19,7 @@ class ResponsiveLayout extends StatelessWidget {
     return NavigationPanel(
       initialPath: state.path,
       root: state.root,
+      header: state.header,
       onNodeSelected: (node) {
         WidgetbookState.of(context).updatePath(node.path); // Fresh context
 
diff --git a/packages/widgetbook/lib/src/navigation/widgets/navigation_panel.dart b/packages/widgetbook/lib/src/navigation/widgets/navigation_panel.dart
index 1a5ac385e..d31f87e1f 100644
--- a/packages/widgetbook/lib/src/navigation/widgets/navigation_panel.dart
+++ b/packages/widgetbook/lib/src/navigation/widgets/navigation_panel.dart
@@ -12,12 +12,17 @@ class NavigationPanel extends StatefulWidget {
     this.initialPath,
     this.onNodeSelected,
     required this.root,
+    this.header,
   });
 
   final String? initialPath;
   final ValueChanged<WidgetbookNode>? onNodeSelected;
   final WidgetbookNode root;
 
+  /// An optional widget to display at the top of the navigation panel.
+  /// This can be used for branding or additional information.
+  final Widget? header;
+
   @override
   State<NavigationPanel> createState() => _NavigationPanelState();
 }
@@ -50,6 +55,11 @@ class _NavigationPanelState extends State<NavigationPanel> {
     return Column(
       crossAxisAlignment: CrossAxisAlignment.stretch,
       children: [
+        if (widget.header != null)
+          Padding(
+            padding: const EdgeInsets.all(16),
+            child: widget.header!,
+          ),
         Padding(
           padding: const EdgeInsets.all(16),
           child: SearchField(
diff --git a/packages/widgetbook/lib/src/state/widgetbook_state.dart b/packages/widgetbook/lib/src/state/widgetbook_state.dart
index 929652a3c..a91fcb932 100644
--- a/packages/widgetbook/lib/src/state/widgetbook_state.dart
+++ b/packages/widgetbook/lib/src/state/widgetbook_state.dart
@@ -34,6 +34,7 @@ class WidgetbookState extends ChangeNotifier {
     required this.root,
     this.home = const DefaultHomePage(),
     this.panels = null,
+    this.header,
   }) {
     this.knobs = KnobsRegistry(
       onLock: () {
@@ -68,6 +69,10 @@ class WidgetbookState extends ChangeNotifier {
   final WidgetbookRoot root;
   final Widget home;
 
+  /// An optional widget to display at the top of the navigation panel.
+  /// This can be used for branding or additional information.
+  final Widget? header;
+
   List<WidgetbookNode> get directories => root.children!;
 
   WidgetbookUseCase? get useCase => path == null ? null : root.table[path!];
diff --git a/packages/widgetbook/lib/src/widgetbook.dart b/packages/widgetbook/lib/src/widgetbook.dart
index cb4fdd11f..b3add83b0 100644
--- a/packages/widgetbook/lib/src/widgetbook.dart
+++ b/packages/widgetbook/lib/src/widgetbook.dart
@@ -30,6 +30,7 @@ class Widgetbook extends StatefulWidget {
     this.darkTheme,
     this.themeMode,
     this.home = const DefaultHomePage(),
+    this.header,
   });
 
   /// A [Widgetbook] with [CupertinoApp] as an [appBuilder].
@@ -44,6 +45,7 @@ class Widgetbook extends StatefulWidget {
     this.darkTheme,
     this.themeMode,
     this.home = const DefaultHomePage(),
+    this.header,
   });
 
   /// A [Widgetbook] with [MaterialApp] as an [appBuilder].
@@ -58,6 +60,7 @@ class Widgetbook extends StatefulWidget {
     this.darkTheme,
     this.themeMode,
     this.home = const DefaultHomePage(),
+    this.header,
   });
 
   /// The initial route for that will be used on first startup.
@@ -105,6 +108,10 @@ class Widgetbook extends StatefulWidget {
   /// not the [Theme] from the [appBuilder] or the [ThemeAddon].
   final Widget home;
 
+  /// An optional widget to display at the top of the navigation panel.
+  /// This can be used for branding or additional information.
+  final Widget? header;
+
   @override
   State<Widgetbook> createState() => _WidgetbookState();
 }
@@ -120,6 +127,7 @@ class _WidgetbookState extends State<Widgetbook> {
     state = WidgetbookState(
       appBuilder: widget.appBuilder,
       home: widget.home,
+      header: widget.header,
       addons: widget.addons,
       integrations: widget.integrations,
       root: WidgetbookRoot(
diff --git a/packages/widgetbook/test/src/navigation/widgets/navigation_panel_test.dart b/packages/widgetbook/test/src/navigation/widgets/navigation_panel_test.dart
index 7e4a95a30..666055349 100644
--- a/packages/widgetbook/test/src/navigation/widgets/navigation_panel_test.dart
+++ b/packages/widgetbook/test/src/navigation/widgets/navigation_panel_test.dart
@@ -9,6 +9,42 @@ void main() {
   group('$NavigationPanel', () {
     const query = '1';
 
+    testWidgets(
+      'given a header is provided to NavigationPanel, '
+      'then it is displayed above the search field',
+      (tester) async {
+        await tester.pumpWidgetWithQueryParams(
+          queryParams: {},
+          builder: (_) => NavigationPanel(
+            root: treeRoot,
+            header: const Placeholder(),
+          ),
+        );
+
+        expect(find.byType(Placeholder), findsOneWidget);
+
+        final headerDy = tester.getTopLeft(find.byType(Placeholder)).dy;
+        final searchDy = tester.getTopLeft(find.byType(TextFormField)).dy;
+
+        expect(headerDy < searchDy, isTrue);
+      },
+    );
+
+    testWidgets(
+      'given header is not provided to NavigationPanel, '
+      'then only the search field is displayed at the top',
+      (tester) async {
+        await tester.pumpWidgetWithQueryParams(
+          queryParams: {},
+          builder: (_) => NavigationPanel(
+            root: treeRoot,
+          ),
+        );
+
+        expect(find.byType(TextFormField), findsOneWidget);
+      },
+    );
+
     testWidgets(
       'when search for query, '
       'then only matching nodes are shown',
diff --git a/packages/widgetbook/test/src/widgetbook_test.dart b/packages/widgetbook/test/src/widgetbook_test.dart
new file mode 100644
index 000000000..ceca221a2
--- /dev/null
+++ b/packages/widgetbook/test/src/widgetbook_test.dart
@@ -0,0 +1,55 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:widgetbook/widgetbook.dart';
+
+import '../helper/helper.dart';
+
+void main() {
+  group('Widgetbook', () {
+    testWidgets(
+      'given a header in desktop mode, '
+      'then it appears in the NavigationPanel',
+      (tester) async {
+        tester.view.physicalSize = const Size(1200, 800);
+        tester.view.devicePixelRatio = 1.0;
+
+        await tester.pumpWidget(
+          const Widgetbook.material(
+            header: Placeholder(),
+            directories: [],
+          ),
+        );
+
+        await tester.pumpAndSettle();
+
+        expect(find.byType(Placeholder), findsOneWidget);
+
+        addTearDown(tester.view.resetPhysicalSize);
+      },
+    );
+
+    testWidgets(
+      'given a header in mobile mode, '
+      'when clicking on the bottom navigation bar,'
+      'then the header appears in the NavigationPanel',
+      (tester) async {
+        tester.view.physicalSize = const Size(640, 1200);
+        tester.view.devicePixelRatio = 1.0;
+
+        await tester.pumpWidget(
+          const Widgetbook.material(
+            header: Placeholder(),
+            directories: [],
+          ),
+        );
+
+        await tester.pumpAndSettle();
+        await tester.findAndTap(find.text('Navigation'));
+
+        expect(find.byType(Placeholder), findsOneWidget);
+
+        addTearDown(tester.view.resetPhysicalSize);
+      },
+    );
+  });
+}


Reviews:
user: login: yousinix
body: Thanks a lot for contributing to Widgetbook! 💙 
We have some minor changes to make things fit better into our coding style.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T10:12:56Z
user: login: Sourav-Sonkar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T12:48:57Z
user: login: Sourav-Sonkar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T12:49:08Z
user: login: Sourav-Sonkar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T13:05:23Z
user: login: Sourav-Sonkar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T13:17:21Z
user: login: yousinix
body: Thanks for contributing to Widgetbook! 💙 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:13:49Z

Review comments:
path: examples/custom_header_example/README.md
user: login: yousinix
body: **suggestion:** Let's remove this example, and add it to the docs [customization page](https://github.com/widgetbook/widgetbook/blob/main/docs/guides/customization.mdx) instead ([similar to the `home` parameter](https://docs.widgetbook.io/guides/customization#home-widget))
created_at: 2025-05-07T09:55:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +108,10 @@ class Widgetbook extends StatefulWidget {
   /// not the [Theme] from the [appBuilder] or the [ThemeAddon].
   final Widget home;
 
+  /// An optional widget to display at the top of the navigation panel.
+  /// This can be used for branding or additional information.
+  final Widget? customHeader;
path: packages/widgetbook/lib/src/widgetbook.dart
user: login: yousinix
body: **suggestion:** Let's rename this to `header` instead of `customHeader`. To follow the same conventions used for the `home` parameter, which also allows you to provide a "custom" home page.
created_at: 2025-05-07T09:57:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +132,7 @@ class _WidgetbookState extends State<Widgetbook> {
       root: WidgetbookRoot(
         children: widget.directories,
       ),
+      customHeader: widget.customHeader,
path: packages/widgetbook/lib/src/widgetbook.dart
user: login: yousinix
body: **nitpick:** We usually keep one-line parameters (e.g. `header`) above multi-line parameters (e.g. `root`); for better readability.
created_at: 2025-05-07T09:58:37Z
author_association: CONTRIBUTOR
path: packages/widgetbook/test/src/widgetbook_custom_header_test.dart
user: login: yousinix
body: **suggestion:** This test doesn't really test much. It's making sure that Flutter passes the parameter properly from the `StatefulWidget` to its `State`. It's better if we combine the 2 tests into one, that tests the feature end-to-end, so the parameter is passed to `Widgetbook` constructor, and is asserted in the `NavigationPanel`.

Please also place the test inside `packages/widgetbook/test/src/widgetbook_test.dart`, as our `test` files match our `lib` files structure, and using filenames to group tests is not used anywhere else.

created_at: 2025-05-07T10:11:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +108,10 @@ class Widgetbook extends StatefulWidget {
   /// not the [Theme] from the [appBuilder] or the [ThemeAddon].
   final Widget home;
 
+  /// An optional widget to display at the top of the navigation panel.
+  /// This can be used for branding or additional information.
+  final Widget? customHeader;
path: packages/widgetbook/lib/src/widgetbook.dart
user: login: Sourav-Sonkar
body: Changed to header
created_at: 2025-05-10T12:48:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -125,6 +132,7 @@ class _WidgetbookState extends State<Widgetbook> {
       root: WidgetbookRoot(
         children: widget.directories,
       ),
+      customHeader: widget.customHeader,
path: packages/widgetbook/lib/src/widgetbook.dart
user: login: Sourav-Sonkar
body: Changed to header
created_at: 2025-05-10T12:49:08Z
author_association: CONTRIBUTOR
path: examples/custom_header_example/README.md
user: login: Sourav-Sonkar
body: Done
created_at: 2025-05-10T13:05:23Z
author_association: CONTRIBUTOR
path: packages/widgetbook/test/src/widgetbook_custom_header_test.dart
user: login: Sourav-Sonkar
body: Done
created_at: 2025-05-10T13:17:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: docs-page[bot]
created_at: 2025-05-06T17:08:04Z
body: To view this pull requests documentation preview, visit the following URL:
  

  [docs.page/widgetbook/widgetbook~1443](https://docs.page/widgetbook/widgetbook~1443)
  

  Documentation is deployed and generated using [docs.page](https://docs.page).
author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-10T12:44:08Z
body: @Sourav-Sonkar is attempting to deploy a commit to the **Widgetbook** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Widgetbook&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%2289a66c9ef9cc045a8af79468df7da5f86c264ce8%22%7D%2C%22id%22%3A%22QmaRR9gGzKyjq2SDxb2Don1H6spKgbymwd5WYW5kyx1rAa%22%2C%22org%22%3A%22widgetbook%22%2C%22prId%22%3A1443%2C%22repo%22%3A%22widgetbook%22%7D).


author_association: NONE
user: login: TahaTesser
created_at: 2025-05-13T17:52:26Z
body: Thank you so much! I'll immediately use this header in the new release. cc: @LucasJosefiak 
author_association: CONTRIBUTOR