<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Anton Dosov - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Anton Dosov - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/anton-dosov</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/anton-dosov</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/anton-dosov.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Wed, 16 Sep 2026 20:04:58 GMT</lastBuildDate>
  <item>
    <title><![CDATA[One button, three places: How we rebuilt Kibana's page headers with stricter APIs]]></title>
    <description><![CDATA[We gave Kibana's shared shell typed contracts, which is how design system governance became the default, and why the new page headers have no breadcrumbs.]]></description>
    <content:encoded><![CDATA[<p>We replaced the open-ended React APIs behind every page header in Kibana with typed contracts, which put design system governance in the shell itself. Dozens of teams no longer have to get each header right by hand. Now a page declares what a control means, and the shared shell decides how it looks and where it goes. We removed breadcrumbs along the way, because the redesigned navigation already shows you where you are. The new chrome is live in Elastic Cloud Serverless today and ships to Elastic Cloud Hosted and self-managed in 9.6.</p><h2>What is the Kibana chrome?</h2><p>The chrome is everything that frames an application in <a href="https://www.elastic.co/kibana">Kibana</a>: the global header and the navigation, along with the header of the page that you’re currently on. Every application renders inside it. Because the chrome is shared, its APIs decide how consistent the whole product feels and how hard the next redesign will be.</p><h2>What inconsistent page headers looked like to users</h2><p>The clearest way to see the problem is to look at what users saw:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt63a6acf0d30ae807/6aaa27addcc13f41b2527962/unnamed_(1).png" alt="Three Kibana pages with different page header layouts, showing inconsistent action button and link placement" /><p></p><p>Take something as basic as a primary action, the one button a page most wants you to click, like <strong>Create index</strong>. Before this work, primary actions lived in three different places, depending on the page:</p><ul><li><p>In the old app header.</p></li><li><p>In the page template header.</p></li><li><p>Somewhere in the page body itself.</p></li></ul><p>Common links had the same problem, with share, feedback, and documentation links appearing in separate spots with different styling from one app to the next. Breadcrumbs were misconfigured on many pages. For example, some showed a breadcrumb for each tab within a page, and some weren't clickable. Still others repeated the title of the page you were already on.</p><p>Every one of these was the output of a well-intentioned team using a flexible API in the way it’s usually used. Users had to relearn each page's layout, and every attempt to redesign the chrome had to account for hundreds of local variations.</p><h2>Goals for the page header redesign</h2><p>The goal of the redesign was simple; that is, pages should express what they need (for example, a title, badge, or primary action), and the shared shell should decide how it looks and where it goes. Common links and primary actions should get one home, everywhere. If it's the primary action, it's always in the same place, styled the same way. And consistency should stop being something that each of dozens of teams has to get right by hand, forever.</p><p>Getting there wasn't a restyling exercise; it required changing the contract between applications and the platform.</p><h2>How typed props replaced EuiPageHeader's open React nodes</h2><p>Each page header was easy to build in isolation, and the local choices piled up into visible differences across the product: inconsistent spacing, title styling, and action placement from one page to the next.</p><p>The fix, everywhere this showed up, was the same mechanism: stricter APIs. Instead of accepting arbitrary React nodes, a control declares what it means through typed props, and the shared shell decides how it looks and where it goes. That one change buys two things at once: consistency, and governance over what the shared surface is allowed to contain.</p><p>Take the page header. With <code>EuiPageHeader</code>, the API exposed layout settings and accepted React nodes for nearly every visible area:</p>&lt;EuiPageHeader
  pageTitle={
    &lt;&gt;
      Index Management
      &lt;EuiBadge color="accent"&gt;Beta&lt;/EuiBadge&gt;
    &lt;/&gt;
  }
  description={
    &lt;&gt;
      View and manage your Elasticsearch indices.{' '}
      &lt;EuiLink href={docsUrl}&gt;Learn more&lt;/EuiLink&gt;
    &lt;/&gt;
  }
  bottomBorder
  alignItems="top"
  responsive={false}
  tabs={tabs}
  rightSideItems={[
    &lt;RefreshButton onClick={onRefresh} /&gt;,
    &lt;CreateButton onClick={onCreate} /&gt;,
  ]}
/&gt;<p>Because <code>pageTitle</code>, <code>description</code>, and <code>rightSideItems</code> all accept arbitrary React nodes, every team filled them in differently. One page put a badge next to the title, while another styled its own pill. One team's actions were plain buttons in one order; the next team's were a different mix in another order, some collapsing into a menu and others not. Everyone used the same shell component, yet the headers looked and behaved like they came from different products. The shared component guaranteed the wrapper, not what went inside it.</p><p>The <code>AppHeader</code> API closes that off. The same header is expressed as typed props:</p>&lt;AppHeader
  title="Index Management"
  badges={[
    {
      label: 'Beta',
      color: 'accent',
      tooltip: 'This feature is in beta.',
    },
  ]}
  description={{
    text: 'View and manage your Elasticsearch indices.',
    learnMoreUrl: docsUrl,
  }}
  tabs={tabs}
  menu={{
    primaryActionItem: {
      id: 'create',
      label: 'Create index',
      iconType: 'plusInCircle',
      run: onCreate,
    },
    items: [
      {
        id: 'refresh',
        label: 'Refresh',
        iconType: 'refresh',
        run: onRefresh,
      },
    ],
  }}
/&gt;<p>Now there’s only one way to express each part, so every page renders it the same. Badges live in their own typed collection, separate from the title. A description carries its text and, optionally, a "Learn more" URL. Actions declare whether they’re primary or secondary, and the component decides how they look and when they collapse. Plus, richer controls fit the same shape. An editable title or a favorite toggle is a structured config rather than a bespoke React tree:</p>&lt;AppHeader
  title={{
    text: indexName,
    onSave: renameIndex,
  }}
  favorite={{
    status: favoriteStatus,
    onToggle: toggleFavorite,
  }}
/&gt;<p>The split is consistent throughout; the application supplies state and behavior (the current name, what happens on rename, how to toggle a favorite) and the header owns presentation. It renders the controls, handles keyboard interaction, shows validation errors, and reflects pending changes. Because those roles are explicit, layout and responsive behavior stay inside the shared component, and applications don't need a coordinated update every time the header changes.</p><h3>Why a shared UI shell needs a closed set of controls</h3><p>The global shell had a similar problem that was one step more open-ended. Any plugin could register arbitrary content on the left or right and pick its position with a number, and no conversation with the platform team or designers was required:</p>chrome.navControls.registerLeft({
  content: &lt;ProjectPicker /&gt;,
});

chrome.navControls.registerRight({
  order: 10,
  content: &lt;AiAssistantButton /&gt;,
});

chrome.navControls.registerRight({
  order: 20,
  content: &lt;FeedbackButton /&gt;,
});<p>That <code>registerRight</code> API is an open door. Any team can add a control, and the header fills up with elements no one designed together. The controls compete for space and carry their own styling, and their order is decided by whoever picked the larger number. The shell only knows that one React node follows another; it can't tell that one opens an AI assistant and another collects feedback, so it can't reason about them or keep them coherent.</p><p>The new shell replaces that open canvas with a closed set of named roles under <code>chrome.controls</code> and <code>chrome.help</code>. The old registry hasn't gone anywhere yet, because the classic header still runs alongside the new one while applications migrate, but nothing in the new shell is reachable through it:</p>chrome.controls.projectPicker.set(&lt;ProjectPicker /&gt;);
chrome.controls.aiButton.register({ content: &lt;AiAssistantButton /&gt; });
chrome.controls.globalSearch.set({ onClick: openGlobalSearch });
chrome.help.registerFeedbackHandler(openFeedback);<h3>Global chrome header versus application page header</h3><p>Part of what made the old chrome hard to evolve was that "the header" was really several things tangled together. The redesign draws a hard line between two surfaces with different owners:</p><p>
</p><p><strong>Chrome (global) header</strong></p><p><strong>Application header</strong></p><p>Owner</p><p>The platform</p><p>The page</p><p>Scope</p><p>What's true everywhere in Kibana</p><p>What's true on this page</p><p>Contents</p><p>Navigation, project or deployment picker, search, help, AI assistant, feedback</p><p>Title, badges, description, tabs, page actions</p><p>How it's populated</p><p>Named slots under <code>chrome.controls</code> and <code>chrome.help</code></p><p><code>AppHeader</code> typed props, rendered directly or set via <code>chrome.appHeader.set()</code></p><p>Because each surface has one owner and a typed contract between them, the platform can redesign the chrome without auditing hundreds of pages, and an application can evolve its header without colliding with global controls. Setting a header config returns a cleanup callback, so an application tears down its own contribution when it unmounts.</p><h2>Design decisions that strict APIs force</h2><p>Stricter APIs forced design decisions that flexible APIs had let everyone defer. Two are worth calling out.</p><h3>Why we removed breadcrumbs from Kibana's navigation</h3><p>Removing breadcrumbs from the chrome was one of the more debated calls, and the state of the data made it easier than expected. In practice, breadcrumbs across Kibana were widely misconfigured:</p><ul><li><p>Some pages generated a breadcrumb for every tab on the page.</p></li><li><p>Some breadcrumbs weren't clickable.</p></li><li><p>Some repeated the title of the current page.</p></li></ul><p>They added visual weight without reliably adding orientation.</p><p>At the same time, the redesigned navigation already conveys hierarchy. The primary and secondary navigation show you where you are and give you a direct way back up. Keeping breadcrumbs would have meant maintaining a third (and frequently wrong) representation of the same information, so we stopped drawing the trail and let the navigation do that job.</p><p>The breadcrumb API itself didn't disappear. Applications still register breadcrumbs, and the new shell reads them to derive the back button and to fall back to a page title when an app hasn't supplied one. The data changed jobs rather than going away, which is why migrating an app to the new header rarely starts with ripping breadcrumbs out:</p><p><strong>Before:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt45c047fdfcad7c14/6aaa27d97d925c9a3baf92a0/unnamed.png" alt="Kibana page header with a breadcrumb trail repeating the GenAI Settings page title" /><p><strong>After:</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6b04544a35718f97/6aaa27fe92336926efb1c2d0/unnamed.png" alt="the same Kibana page header with breadcrumbs removed, showing only the page title" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2ecbc17acb523ead/6aaa28219936f5d5f78b976d/unnamed.png" alt="Kibana breadcrumbs repeating a page title and tab name, annotated to show misconfigured navigation hierarchy" /><h3>How many action buttons a page header should show</h3><p>The other recurring debate was priority. With every page's actions now flowing through one menu structure, which buttons deserve to be visible, and in what order? Priorities shift as products evolve, so this conversation is ongoing.</p><p>For launch, we made one deliberate simplification: we locked the number of buttons that can appear in the app menu. A page gets one primary action and a bounded set of secondary actions before the rest collapse into a menu. The constraint keeps any single page from dividing the user's attention across a wall of buttons, and it makes the priority conversation explicit instead of letting it be settled by whoever adds the next button.</p><p>We went through several iterations to land here. Initially, we had many visible actions. A page could have up to three buttons, plus a secondary action to the left of the primary one. That took up too much space, so we gradually simplified the layout by removing the secondary action and reducing the number of visible buttons. We also made the overflow menu more structured. Some items, such as feedback and documentation, now have a fixed place in the footer of the overflow menu.</p><h2>What strict design system governance costs teams</h2><p>Under the old API, a plugin could add a novel UI by choosing a side, picking an order, and mounting a React tree. Under the stricter API, a new kind of control may need a shared capability before a plugin can add it. That takes more work up front and forces a design decision that teams could previously avoid.</p><p>We accepted the cost of stricter APIs because the alternative pushed it into every later redesign. As long as arbitrary React trees mounted at arbitrary points, every change to the shell's layout or accessibility had to account for all of them.</p><p>That’s the trade we made, and it pays back on both fronts. Consistency stops being something each team has to get right by hand; there’s one way to express a control, so pages match by default. And the shared surface stays governed. Its set of controls is a deliberate design decision, not whatever accumulated at the edges. Applications describe what their controls mean, and the shell is free to decide how they look, both today and in the next redesign.</p><h2>Where the redesigned Kibana chrome is available</h2><p>The redesigned chrome is available now in <a href="https://www.elastic.co/cloud/serverless">Elastic Cloud Serverless</a>. Open any project, and you're already using it. For <a href="https://www.elastic.co/cloud">Elastic Cloud Hosted</a> and self-managed users, it ships in 9.6.</p><p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/design-system-governance-kibana-page-headers</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/design-system-governance-kibana-page-headers</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[Inside Elastic]]></category>
    <category><![CDATA[Developer Experience]]></category>
    <dc:creator><![CDATA[Anton Dosov,Ryan Keairns,Krzysztof Kowalczyk,Alex Marhaba]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc5228b5443fe9d20/6aaa28eb27e4f0a047befaa4/unnamed.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>