Skip to content

Admin UI: Add icon prop to Page header component#76469

Open
CGastrell wants to merge 10 commits intoWordPress:trunkfrom
CGastrell:add/page-header-logo
Open

Admin UI: Add icon prop to Page header component#76469
CGastrell wants to merge 10 commits intoWordPress:trunkfrom
CGastrell:add/page-header-logo

Conversation

@CGastrell
Copy link
Copy Markdown
Contributor

@CGastrell CGastrell commented Mar 12, 2026

See design convo #76709

What?

Adds a icon prop to the Page component's header, allowing consumers to display a logo or symbol alongside the title or breadcrumbs.

See #76448

Note: This PR depends on #76467 (Storybook stories for admin-ui). The story files in this diff include both the base stories from that PR and the new logo stories. The implementation changes are in header.tsx, index.tsx, and style.scss.

image image

Why?

As described in #76448, consumers currently pass logos inside the title prop, which places them inside the h2 tag. This is fragile and creates accessibility issues — when the title contains non-text content, a separate ariaLabel must be passed to maintain good semantics (see #75898).

An officially supported icon prop renders the logo outside the h2 tag, ensuring:

  • Consistent logo placement across consumers
  • Proper semantic structure (icon is not part of the heading tag)
  • No need for a separate ariaLabel if the only difference is the icon between title and ariaLabel.

How?

  • Added icon?: React.ReactNode prop to Page (index.tsx) and Header (header.tsx)
  • Logo renders before the title/breadcrumbs in the header row, wrapped in a div.admin-ui-page__header-logo
  • Added CSS for the logo container: flex-centered, non-shrinking, with object-fit: contain for img/svg children
  • Added Storybook stories demonstrating the logo with title, with breadcrumbs, and with a Jetpack logo

Testing Instructions

  1. Run npm run storybook:dev
  2. Navigate to Admin UI > Page in the sidebar
  3. Check the following stories:
    • With Logo: globe icon + "Page title" heading
    • With Jetpack Logo: Jetpack icon + "Jetpack" heading
    • With Logo And Breadcrumbs: globe icon + breadcrumb navigation
    • With Jetpack Logo And Breadcrumbs: Jetpack icon + breadcrumb navigation
  4. Verify the logo appears to the left of the title/breadcrumbs
  5. Inspect the DOM — the logo should be outside the h2 element

Testing Instructions for Keyboard

  1. Tab through the stories — the logo container should not be focusable (it's decorative
  2. The heading and breadcrumb links should remain keyboard accessible
    EOF
    )

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 12, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: CGastrell <cgastrell@git.wordpress.org>
Co-authored-by: simison <simison@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: aduth <aduth@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@simison
Copy link
Copy Markdown
Member

simison commented Mar 13, 2026

Thinking if icon as prop name instead of logo would be more aligned with other components (e.g. button)

@CGastrell CGastrell force-pushed the add/page-header-logo branch 4 times, most recently from afba908 to 09361c5 Compare March 16, 2026 17:53
@github-actions github-actions bot added the [Package] Private APIs /packages/private-apis label Mar 16, 2026
@simison simison changed the title Admin UI: Add logo prop to Page header component Admin UI: Add icon prop to Page header component Mar 17, 2026
@simison
Copy link
Copy Markdown
Member

simison commented Mar 17, 2026

FYI I updated PR title+description to use icon instead of logo as done also in commit 09361c5

@CGastrell CGastrell force-pushed the add/page-header-logo branch 2 times, most recently from 1f04fb7 to 6a17159 Compare March 17, 2026 17:58
@github-actions github-actions bot removed the [Package] Private APIs /packages/private-apis label Mar 17, 2026
@simison simison added [Type] Enhancement A suggestion for improvement. [Package] Admin UI /packages/admin-ui labels Mar 19, 2026
@ciampo
Copy link
Copy Markdown
Contributor

ciampo commented Mar 19, 2026

Have you considered rendering an Icon component directly in the Header? What if consumers have different icon positioning needs? How would this interact in pages with different combinations of title / breadcrumbs (including none of them?

What I'm worried about is that we're introducing an abstraction that may not be flexible enough for future needs.

I'm also lacking the overall vision for Page and admin UI — rather than adding a little piece at a time and risk building the wrong abstractions, I think it would be better to take a step back, understand all the requirements, and figure out the best abstractions / implementations to satisfy them (cc @WordPress/gutenberg-components )

@mirka
Copy link
Copy Markdown
Member

mirka commented Mar 19, 2026

Yes, seeing the aria-label issue (#75899) too, there are already warning signs in the API that I would recommend considering a less monolithic approach, namely with subcomponents like Page.Header.

Warning signs: There are three props on root-level Page that are for the same conceptual element (heading), which is itself a sign that this architecture may not be tenable, and also the prop naming (aria-label — for what?, title, headingLevel) is already kind of haphazard and doesn't make those relationships clear.

@simison
Copy link
Copy Markdown
Member

simison commented Mar 19, 2026

@mirka context for the recent ariaLabel and headingLevel additions:

Both are related to accessibility, but the flexibility of h1-h6 heading option seems kinda lot to me. I don't know enough about use-cases if heading ever needs to be anything else than h1; maybe?

@simison
Copy link
Copy Markdown
Member

simison commented Mar 19, 2026

Noting that while it's good we're lookinga at different prop-combos and how they should work together in different variations, I'm not sure to what extend we should codify constraints and things always necessarily end up looking good. Consumers should have some responsibility to make sure e.g. this case continues to look good even if actual header is super narrow on full desktop width:

image

@CGastrell
Copy link
Copy Markdown
Contributor Author

What if consumers have different icon positioning needs?

As far as I can tell, the structure of admin-ui's Page is to provide a consistent look throughout pages that will live on the site admin. This is where we're coming from, the multiplicity of pages and styles built on wp-admin.

Now, we want to be flexible, but we also want to set some sort of "standard". In that regard I don't see many abstractions, hence why Header is not (yet) exposed, it only exists through Page props. And it aims to always look the same.

Title, Breadcrumbs and Badges are all Page props that then get passed down (internally) to Header. If we want to add full flexibility, then this shouldn't happen and we'd need to expose/export Header, allowing consumers to:

<Page>
  <Header />
  <SomeOther />
</Page>

But that would still suffer the same consequences: consumer would need to compose Header in the same way it would need to compose Page.

Right now Header is the one sorting out how/when things are rendered and, in some cases, it even delegates or expects props to provide the right components (Breadcrumbs needs to be passed down, it's not constructed from an array).

So, with this same approach, the proposed icon prop is meant to receive a component and be placed consistently so that the page looks is retained.

Is this a constraint we want? I assumed yes. Otherwise the consumer can use its own component for the page they need. Page, from my PoV, exists to provide a unified way to build pages to integrate seamlessly into the admin. Whatever magic we put in there is only meant to keep design aligned, that means restrictions, yes, but also the benefit of having things work out of the box.

Now, back to the icon prop and other warning signs flagging this as a haphazard situation, that depends on the level of control Page should hold over its structural design. As @mirka points out, this turns to a monolithic approach, yet I can't be sure this is not what we want in this particular case. Composing Header through Page props seems as functional as exposing Header and allow this to be ruling its own props.

Shall a consumer want to fully customize its page they can, if Page doesn't receive any of the three props (title, breadcrumbs or badges, and now icon) the Header is not rendered and the consumer can enjoy a fully blank canvas to work on. This is the "off-road" option we can offer, but once a single of those props is provided, we want it to be consistent and Page uses Header to render it, keeping the flexbox behavior and alignment in this unified way.

That said, we might also be more worried about actions, that don't even get considered to render the Header, meaning <Page actions={ someActions } /> would never render those actions anywhere.

@mirka @ciampo I see the concern, and I agree this might be a completely different discussion on structural design of the component. I'm just saying that this decision will affect the current way we expose/render the Header and, once we come to that decision, it should be the same work to migrate any of those props as it would be to migrate icon.

@mirka
Copy link
Copy Markdown
Member

mirka commented Mar 19, 2026

Both are related to accessibility

Not questioning the necessity of the prop, but the naming (or API structure) here. A plain root-level aria-label prop on a Page component doesn't make clear what exactly the aria-label is applying to. I looked at the code and it's just for the wrapping region, which also was unexpected from the component name "Page" — it's actually a Region component? There will never be sibling regions on the "Page"?

Not just the unclarity, but it's problematic when you eventually need to accept custom aria-labels on other internal elements that are also covered by root-level props. How exhaustive are the props we have now (genuine question because I don't know)? There are root-level props applying to the region, to the header area, and to the content area, often without differentiators in the name. This will lead to prop bloat and strained APIs.

There's a balance to be found between ergonomics, consistency, and modularity, but we've found it easier to find that balance by starting from the fully modular, then at the end tweaking ergonomics and consistency in the monolithic component. Maybe that is what is happening here, because the code does show internal modularity, but the way props are all flatly exposed on the monolithic component gives me pause, if we're not close to exhausting our prop set yet.

And there are type-friendly, ergonomic ways to mitigate root-level prop bloat, for example <Page header={ <Page.Header /* pass all the header props here */ /> }>.

@CGastrell
Copy link
Copy Markdown
Contributor Author

Both are related to accessibility

Not questioning the necessity of the prop, but the naming (or API structure) here. A plain root-level aria-label prop on a Page component doesn't make clear what exactly the aria-label is applying to. I looked at the code and it's just for the wrapping region, which also was unexpected from the component name "Page" — it's actually a Region component? There will never be sibling regions on the "Page"?

Not just the unclarity, but it's problematic when you eventually need to accept custom aria-labels on other internal elements that are also covered by root-level props. How exhaustive are the props we have now (genuine question because I don't know)? There are root-level props applying to the region, to the header area, and to the content area, often without differentiators in the name. This will lead to prop bloat and strained APIs.

There's a balance to be found between ergonomics, consistency, and modularity, but we've found it easier to find that balance by starting from the fully modular, then at the end tweaking ergonomics and consistency in the monolithic component. Maybe that is what is happening here, because the code does show internal modularity, but the way props are all flatly exposed on the monolithic component gives me pause, if we're not close to exhausting our prop set yet.

And there are type-friendly, ergonomic ways to mitigate root-level prop bloat, for example <Page header={ <Page.Header /* pass all the header props here */ /> }>.

Completely agree from a component design PoV. Though, this would be a breaking change, not for the sake of blocking it, just to make sure we take the necessary precautions before such change. On top of it, I like the <Page.Header /* props */ /> syntax, makes it very self explanatory on usage and scope 👍

Out of the scope of this PR but, anyone up to prepare such change?

@CGastrell CGastrell force-pushed the add/page-header-logo branch from 6a17159 to 242ded6 Compare March 19, 2026 18:49
@CGastrell
Copy link
Copy Markdown
Contributor Author

BTW, fixing the actions bug: #76695

@jameskoster
Copy link
Copy Markdown
Contributor

Outside of the excellent discussion so far I'd like to point out that the dimensions of the icon should be restricted somehow, and ideally an aspect ratio of 1/1 forced. If a user supplied a 1280x1024px jpg we wouldn't want it to render at that size. Even if a consumer supplied an Icon instance the size should ideally be pre-determined if possible.

#76709 suggests 20x20, but that won't play nicely with Icon which defaults to 24x24, so I'd suggest using 24px for consistency.

@CGastrell CGastrell force-pushed the add/page-header-logo branch from 242ded6 to 633e2a1 Compare March 23, 2026 16:18
@CGastrell CGastrell force-pushed the add/page-header-logo branch from 54bd9fb to f1fcfc5 Compare March 31, 2026 14:29
@jameskoster
Copy link
Copy Markdown
Contributor

Looking at the spec (#76709) it seems icons and images are to be treated the same way, so Visual seems like a good option to align with 👍

@CGastrell
Copy link
Copy Markdown
Contributor Author

Looking at the spec (#76709) it seems icons and images are to be treated the same way, so Visual seems like a good option to align with 👍

Just to confirm, the prop should be renamed from icon to visual? As in:

<Page title="Page title" visual={ <Icon icon={ wordpress } size={ 24 } /> }>...</Page>

@jameskoster
Copy link
Copy Markdown
Contributor

That makes more sense to me, given it can render things that are not icons.

@CGastrell CGastrell force-pushed the add/page-header-logo branch from f1fcfc5 to 42f87a2 Compare April 2, 2026 16:28
@CGastrell
Copy link
Copy Markdown
Contributor Author

@jameskoster I pushed the rename from icon to visual, looking good :)

title: 'Page title',
visual: (
<img
src="https://s.w.org/about/images/logos/wordpress-logo-notext-rgb.png"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny detail, but this kind of looks like an icon when rendered, while the story is supposed to demonstrate using an image. Is there an alternative placeholder we could use? Matt's gravatar or something?

@CGastrell
Copy link
Copy Markdown
Contributor Author

@jameskoster

I switched the sample image for Matt's gravatar. Then addressed the other comments, they all made sense to me.

@jameskoster
Copy link
Copy Markdown
Contributor

Seems good from a design perspective 👍

@CGastrell
Copy link
Copy Markdown
Contributor Author

Seems good from a design perspective 👍

@ciampo @simison any particular thoughts from engineering side?

@simison
Copy link
Copy Markdown
Member

simison commented Apr 9, 2026

Let's include a changelog entry for admin-ui.

CGastrell and others added 10 commits April 9, 2026 16:48
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace custom GlobeLogo SVG with the WordPress icon from @wordpress/icons
and remove the image-based logo story that didn't render well.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename the Page header logo prop to icon for consistency. Remove the
JetpackLogo component and its stories as brand logos don't belong in
the component library. Rename CSS class accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@CGastrell CGastrell force-pushed the add/page-header-logo branch from 0a7c313 to 925cd17 Compare April 9, 2026 19:49
@CGastrell
Copy link
Copy Markdown
Contributor Author

Let's include a changelog entry for admin-ui.

Added on 925cd17

Thanks for pointing it out!

Copy link
Copy Markdown
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I'd like to rethink the whole Page abstraction holistically, adding the visual prop can work as a short-term addition to unlock consumption of the component

Comment on lines +41 to +42
img,
svg {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason in particular why we're targeting img and svg elements? What if a different tag (eg figure, video, etc) is passed?

These styles may be unnecessary and introduce a wrong abstraction and/or a style leak: IMO, it's the consumer who's passing visual who should ensure the visual element that is being passed is styled correctly.

If we want to control the layout a bit more intentionally, we could potentially use CSS grid? ie. a 1x1 grid, forcing all children to occupy the grid cell;

.admin-ui-page__header-visual { 
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
  width: 24px;
  height: 24px;

  > * {
    grid-column: 1 / -1;
    grid-row: 1 / -1;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Admin UI /packages/admin-ui [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants