Skip to content

Unify header nav - #13508

Merged
lokesh merged 36 commits into
masterfrom
claude/header-layout-styling-r55xpr
Sep 11, 2026
Merged

Unify header nav#13508
lokesh merged 36 commits into
masterfrom
claude/header-layout-styling-r55xpr

Conversation

@lokesh

@lokesh lokesh commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #13240. The header row was a mix of controls at three different heights with several glyphs that weren't sprite icons, and the two menus behind it (the hamburger drawer and the Browse popover) didn't match each other. This unifies them, and picks up a handful of design fixes along the way.

Frame 427318933 Frame 427318937

Header consistency

  • Search field, Browse, My Books, Log In, Sign Up and the account button now share one height and one label size (--header-control-height, --header-control-font-size). They were 45px, 40px and 30px at three different sizes.
  • Browse, My Books and the account button share one hover treatment (.nav-ghost-btn) instead of three.
  • Every header glyph comes from the Lucide sprite via the icon macro, so it inherits text color and scale: the down-arrow.png chevron, the CSS background-image magnifier, and the archive.org avatar photo — which also drops a third-party image request from every logged-in page load.
  • Header block margins 20px → 12px, giving 16px back to content on every page.

Menu consistency (drawer + popovers)

  • Menu rows share one height and one hover treatment across the drawer, the Browse panel and the Lit popovers; the hover fill is an inset pill rather than a band running to the panel's edges.
  • The drawer's scrim, shadow and surface color now come from the shared overlay tokens instead of its own hard-coded values, matching ol-dialog and the popover tray.
  • The Browse panel gets the standard hairline overlay border.

Shared tokens replacing hard-coded values

  • New static/css/tokens/motion.css (durations + easing) and press.css (press-scale tiers). Seven components — ol-button, ol-chip, ol-carousel, ol-pagination, ol-segmented-control, ol-toggle, the markdown editor — carried five different hard-coded scale() values on :active and now read from three tiers.
  • The menu-row contract (--menu-row-height, --menu-row-inset, --menu-row-padding-inline) lands in control-heights.css; --control-height-large steps 38px → 36px.
  • Removed the dead --account-icon-border token and the dead header-dropdown CSS. The account-icon rules inlined into legacy.css are kept in step.

Fixes

  • Squashed avatar — it was stretched to a square; now cropped and round.
  • "My Books" drawer row used a <br> inside a flex column, where a line break is an empty flex item rather than a break. Two spans instead.
  • Search modal's "Clear all" was a hand-rolled button duplicating ghost-button styles; it's now <ol-button variant="ghost">.

Testing

  • Logged out: Browse, My Books and Log In render as equal-height ghost buttons aligned with the search field, Sign Up filled beside them.
  • Logged in: the account button shows the glyph or the avatar beside the hamburger, with no request to archive.org for a patron with no linked account.
  • Browse popover: rows hover as pills inset from the panel edge; the panel has a hairline edge.
  • Hamburger drawer: the page behind blurs, rows sit on one height, and a librarian account's merge badge sits at the row's trailing edge without shifting the label.

Screenshot

See above.

Stakeholders

@cdrini

claude and others added 6 commits August 28, 2026 02:55
…tton

Unify the header row on a shared --header-control-height token (large on
touch widths for tap targets, the standard medium control height at
desktop) so the search field, My Books and Browse buttons, and the
account/menu button all line up exactly.

- Give the search bar a height lock (border-box, so its border is
  included) and swap its background-image magnifier for the Lucide
  `search` sprite glyph, which inherits the trigger's text color.
- Extract the Browse trigger's ghost styling into a shared .nav-ghost-btn
  class and apply it to the My Books link (header_dropdown now accepts an
  optional link-class prop for its singleton anchor), so both nav items
  render as matching ghost buttons.
- Group the avatar and hamburger icon into a single ghost button: the
  summary becomes a transparent pill that fills on hover and while the
  drawer is open, with the avatar shrunk to sit beside the 24px Lucide
  menu glyph and the pending-merge badge pinned to its corner.
- Replace the down-arrow.png dropdown chevron with the Lucide
  chevron-down sprite glyph.
- Keep the inlined header mirrors in legacy.css in step.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ra2Ro6hPFJVqRkXaLJNAyz
Give the Browse and My Books ghost buttons muted leading icons
(book-open-text, bookmark), swap the Subjects tile glyph from the grid to
the more playful wand and soften the featured-tile icons to blue-400, and
replace the archive.org avatar image in the account button with a loose
circle-user-round glyph, dropping its border/sizing CSS and the now-unused
--account-icon-border token. Keep the large control height at every width
and tighten the mobile tooltip and drawer typography.
Full text color for the Search label and the muted icon color for the
magnifier, so the search field reads as part of the same control set as
the Browse and My Books buttons. The barcode-scan glyph follows the
magnifier to the muted color to stay matched on touch layouts.
…55xpr' into claude/header-layout-styling-r55xpr
The two auth links were the last header controls with their own look: a bare
.btn text link and a 5px-radius blue block, neither locked to the header
control height. Log In now uses the same .nav-ghost-btn as My Books and
Browse, and Sign Up gets a .nav-primary-btn twin of <ol-button
variant="primary"> — same fill, radius, lift, and specular top edge.

The ghost rules move out of the browse-popover section and drop their
.navigation-component scope so the auth links can share them.
Log In and Sign Up inherited the body type while My Books and Browse were
15px/500, so the row read as two sets. The one-off size moves onto
--header-control-font-size beside --header-control-height, and both control
classes pin it instead of inheriting.
38px was a touch tall next to the 32px medium step; 36 keeps the same
touch-friendly floor while tightening the header row and every other large
control (ol-button, ol-segmented-control, ol-select-popover, inputs).
My Books and Browse are both .nav-ghost-btn, whose inline-flex centering is
what puts the label in the middle of the control. Two blanket rules left over
from the old <details> dropdowns outranked it and forced display: block, so
the My Books anchor sat ~10px above the Browse button (which still centers its
own content the way a <button> does) at every width, and stretched its hover
fill across the full half-width of the row. Removing those two declarations
lets the class do its job.

The nav no longer contains a <details>, a <summary> or a .dropdown-menu at all
since Browse became an <ol-popover> and My Books collapsed to a single link,
so the rules that styled them are unreachable and go with it.
@lokesh
lokesh marked this pull request as ready for review September 9, 2026 22:05
…out-styling-r55xpr

# Conflicts:
#	openlibrary/i18n/messages.pot
#	openlibrary/templates/lib/browse_popover.html
#	openlibrary/templates/lib/header_dropdown.html
#	openlibrary/templates/lib/nav_head.html
#	static/css/components/header-bar--desktop.css
#	static/css/components/header-bar.css
#	static/css/legacy.css
The account button assumed every signed-in patron had an avatar to show. A
patron with no linked archive.org account has none, and that URL 403s, so the
button rendered a broken image. Render the glyph instead when there is no
itemname, and swap to it from an onerror handler when the request fails for
some other reason. The glyph gets its own class rather than reusing
account__icon, whose 2px border frames a photo and looks wrong around a glyph.

A linked account that never uploaded a picture still returns 200 -- archive.org
serves its own silhouette there, which we can't tell apart from a real photo
without a round-trip, so that case is unchanged.
The avatar was a rounded square, but the glyph it falls back to when a patron
has no picture is a circle, so the button changed shape between the two states.
Round the avatar to match. It also had no object-fit while being forced into a
30px square, which stretched the portraits and landscapes archive.org serves
alongside square images; cover crops them instead.
…out-styling-r55xpr

# Conflicts:
#	static/icons/src/lucide/bookmark.svg
The unified header styles add about 140 bytes gzipped to every page
bundle, which pushed page-plain.css roughly 90 bytes over its 27KB
limit. The other page bundles still fit under theirs.
The avatar's frame was the only consumer of a one-off primitive alias.
--border-width-thick and --color-border-muted say the same thing in the
shared vocabulary, so the token list loses an entry nothing else used.
Durations and easings in two tiers, like colors: named Penner curves as
primitives, and semantic tokens for the question a component actually has
-- is something entering, leaving, moving, or changing state.

The press scale is tiered by control width so a pressed edge always travels
about 1.5px: 3% on a 32px icon button is sub-pixel, and 3% on a 500px search
bar is a lurch.

Shared with the design-polish branch verbatim, so whichever lands second
merges as a no-op.
The hamburger drawer and the Browse popover are the two overlays the header
owns, and neither read as the same gesture as the rest of the site's modal
surfaces.

The drawer now takes the shared scrim: --overlay-backdrop-color at 32% with
a 3px blur. The blur is what lets the dim stay light -- 32% over a page of
covers still reads as content; blurred, it doesn't. Its panel moves to
--color-surface-header and --box-shadow-overlay.

The popover panel restates --border-overlay rather than zeroing the UA's
[popover] border, since the hairline is what separates a floating panel from
the page under it. Its tray radius and drag handle take tokens too.

Both files are byte-identical to the design-polish branch.
A menu row is now inset from its panel by --menu-row-inset and rounded, so
its hover fill reads as a pill rather than a band running to the panel's
edges. The row gives that inset back as --menu-row-padding-inline, which
keeps the label column at 16px. Light-DOM panels that can't set a margin on
the row -- the Browse popover, the hamburger drawer -- put the inset on the
panel's own side padding instead.

Drawer rows become column flex so a single-line row centres on the shared
row height and the one row with a subheading still stacks; that turns the
My Books <br> into an empty flex item, so it becomes two spans. The merge
badge leaves the float for absolute positioning, since a flex row pushes off
a float rather than letting it sit over the trailing edge.

With a mouse there is no thumb to land, so rows drop to the medium control
height and a menu reads as a stack of medium controls inside a panel.

The header's own controls take the press and motion tokens: the ghost and
primary nav buttons, the hamburger trigger, the search bar, the chevron, and
the drawer spinner. Hover transitions come off -- hover changes are instant.
The barcode button's reduced-motion override goes with the transition it
guarded.
The pill menu rows and popover edge added about 200 bytes gzipped to
every page bundle, which pushed page-edit.css about 60 bytes over its
27KB limit. It now matches page-plain.css and page-form.css.
The hardcoded press scales become the tiered tokens from --press-scale:
text-width controls (ol-button, ol-chip, ol-segmented-control, the static
button) take the default tier, and narrow ones (icon buttons, pagination
items, carousel arrows, the banner close, markdown toolbar buttons, search
result covers) take --press-scale-compact. A full-width ol-button takes
--press-scale-wide, since 3% across a stretched button reads as a lurch
rather than a press. ol-toggle's button variant gains a press of its own --
it has its own border, fill, and raised shadow, so it should squeeze like a
button; its transition joins the reduced-motion override.

The two Lit popovers get the pill menu rows the drawer and Browse panels
already have: --menu-row-inset as a margin, --menu-row-padding-inline giving
that inset back so the control column stays at 16px, and a rounded row. A
nested row's indent is measured from the panel, so it subtracts the inset it
already carries.

Selected-row styling comes off both popovers. The radio or checkbox already
is the state, and the select popover pins chosen items to the top of the
panel besides; a tint plus a blue bolded label on top of that says the same
thing three times, and the tint made the selected row look hovered.

The search modal's Clear all becomes a ghost ol-button instead of a
hand-rolled one, leaving only its place in the row to the modal.

Removing the header's blanket .auth-component link color fixes the Sign Up
label, which had been painting --color-text over the primary button's blue
fill -- the rule outweighed .nav-primary-btn on specificity. Log In is
unchanged; .nav-ghost-btn already declares the same color.
The panel a composer slots into ol-popover clamped itself to a width meant
for the anchored case, so in tray mode the sheet went full-bleed while its
content stopped at 360px, leaving dead space down one side. The width cap
now comes from --ol-popover-content-max-width, which the tray clears; the
height cap comes from --ol-popover-content-max-height, which the tray
replaces with its own so an inner scroll region still resolves. Keying that
off the .tray class rather than a media query keeps it from drifting away
from the JS breakpoint that sets the class.

Handing the content the tray's full height made the tray itself scroll by
the height of the drag handle above the slot, so the tray's height and its
handle's now come from shared variables the content cap subtracts against.
The min(90vw, ...) half of each old cap went with it: the tray owns every
viewport below 768px, so 90vw never won.
<ol-button> declares no font-weight in either OLButton.js or its pre-upgrade
sheet, so it renders at the page's normal weight. The header's ghost, primary
and search controls were pinned to 500, leaving the row a step heavier than
every button beside it. They ask for `normal` explicitly rather than dropping
the declaration, because two of them are native <button>s that would otherwise
take the UA stylesheet's weight instead of inheriting the page's.
The search label was still on --font-size-label-large (14px) while My Books,
Browse and the auth links had moved to --header-control-font-size (15px), so
the one control that sits between them was a step smaller. It was missed when
that token was introduced, not held back deliberately.
--menu-row-height is applied as a min-height everywhere else, and it now
changes with the pointer -- 36px for a thumb, 32px for a mouse. The rating
row was the one consumer taking it as a fixed height, so its 24px star
targets and caption had no room if the row height landed under them. The
token's comment now says min-height, never a fixed height, so the next
consumer doesn't have to rediscover that.
@lokesh
lokesh requested a review from RayBB September 10, 2026 17:50
@lokesh lokesh changed the title Unify header nav into matching ghost buttons with sprite icons Unify header nav Sep 10, 2026
@RayBB RayBB self-assigned this Sep 10, 2026
@github-project-automation github-project-automation Bot moved this to Waiting Review/Merge from Staff in Ray's Project Sep 10, 2026

@RayBB RayBB left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The UX is great (I've been playing with it on testing before this) and I think it's really close.

Some suggestions for improving the code are commented inline.

Also, on mobile the little number is kinda clipped so maybe that could be improved since this is making that change?

Image

Comment thread static/css/legacy.css Outdated
Comment thread static/css/components/header-bar.css
Comment thread static/css/components/header-bar.css Outdated
Comment thread openlibrary/templates/lib/nav_head.html Outdated
Comment thread static/css/tokens/motion.css Outdated
Comment thread openlibrary/components/lit/OlDrawer.js
Comment thread openlibrary/components/lit/OlOptionsPopover.js Outdated
@github-project-automation github-project-automation Bot moved this from Waiting Review/Merge from Staff to Someone else is working on it in Ray's Project Sep 10, 2026
header-bar--tablet.css and header-bar--desktop.css were imported behind media
queries by six page entry points and hand-inlined a second time in legacy.css,
which the LESS deprecation left behind. The two copies had already drifted:
tablet padding-top was 5px in legacy.css against the token's 4px, and the
desktop logo padding-right 15px against 16px, so pages on legacy.css rendered
the header a pixel off from the six that aren't.

Both blocks now live at the foot of header-bar.css, last in the file so they
still outrank the max-width: 960px block above as they did when they were
trailing imports. The header rule set every bundle resolves to is unchanged.

The three .header-bar .navigation-component .dropdown-menu rules that only ever
existed in legacy.css are dropped rather than carried over: header_dropdown.html
only renders that <ul> in its non-singleton branch, and both callers pass a
single link, so nothing in the header has matched them since.
The badge is pinned 6px outside the hamburger trigger's corner, but the
component's 4px gutter is narrower than that, so on a phone -- where the
header spans the full viewport -- the last 2px ran off the screen. Widen the
gutter to 8px so the badge clears it.
Four easing primitives had no semantic token referencing them, which the
file's own rule forbids: --ease-out-quart, --ease-out-expo, --ease-in-cubic
and --ease-in-out-quart are dropped. --ease-move and --ease-state stay and
are noted as the published vocabulary rather than dead entries.

The migration had also missed ol-button.css and OlShelfButton.js, which still
carried the hard-coded 0.08s and scale() values the tokens replaced. The save
button is a 32px icon control, so it takes the compact tier.
The header re-implemented ol-button's ghost and primary variants in the light
DOM, down to a third copy of the specular-edge recipe, under a comment asking
the next person to keep them in step with OLButton.js by hand.

ol-button.css already solves this for the secondary look: .ol-button-static
joins the pre-upgrade selector lists so the two can't drift. Add --ghost and
--primary modifiers the same way and the header keeps only what it actually
does differently -- its taller control height, its one-off label size, and a
hover toned to the header's own canvas.

The neutral hover is now scoped away from the two modifiers, mirroring how
OLButton.js scopes hover per variant; without that the primary button would
take the grey secondary fill alongside its brightness().

These stay light-DOM classes rather than becoming <ol-button>s: they are real
<a href>s that must navigate with JS blocked, and <ol-button href> renders no
anchor until it upgrades.
The $if ia_id gate above it is what actually fixes the broken avatar: an
account with no linked archive.org identity no longer requests one. A linked
account's image 200s -- archive.org serves its own silhouette when there's no
picture -- so the handler was guarding a case that doesn't arise, at the cost
of putting JavaScript back in the markup on every logged-in page.
The same three-line explanation sat above the same four declarations in all
three popovers, differing only in which column it named. OlMenuPopover holds
the explanation; the other two point at it.
OlDrawer.js cites design.md#blur-follows-modality-not-viewport-width, which
was never written. Write it: blur tracks whether the page behind is reachable,
which is why ol-popover blurs as a modal mobile tray and not as a non-modal
desktop popover.

The token-file table listed four of the twelve files, and the Animations
section still taught the hard-coded 0.08s and scale(0.97) that motion.css and
press.css replaced. The anti-pattern sample keeps its raw value -- it is code
you shouldn't write, so it shouldn't model token use.
@lokesh

lokesh commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Of the 8 issues you noted, 6 are fixed fully, 1 partly, and 1 deferred.

# What Ray said What we did
1 Header CSS shouldn't live in legacy.css Fixed. The rules existed twice in the repo and had already drifted apart (1px differences on some pages). Now one copy. Also deleted 3 dead rules.
2 header-bar.css is too big, split it Deferred. It's real (1,030 lines, next-biggest file is 416). But splitting a file and changing 240 lines in it makes the diff unreadable. Better as a follow-up.
3 The header's Log In / Sign Up / Browse buttons re-implement ol-button by hand Fixed. They now share ol-button's styles, so they can't drift. Found and fixed 2 real bugs doing it: Sign Up would have gone grey on hover, and its text rendered dark on blue.
4 Inline JavaScript (onerror) back in the HTML Fixed. Deleted. It was guarding a case that can't actually happen.
5 Some new motion tokens are unused Fixed. Ray spotted 1; there were 4. Removed them. Kept 2 others on purpose and wrote down why. Also finished the job — the PR had left old hard-coded values behind in 2 files.
6 Docs are stale and link to a section that doesn't exist Fixed. Wrote the missing section, listed all 12 token files (was 4), updated the outdated examples.
7 Same 4 lines of CSS copy-pasted into 3 popovers Partly. Took the light option: the duplicated comment is gone, the duplicated CSS is still there. It's minimal and using design tokens so unlikely to drift.
8 Number badge clipped on mobile Fixed. It was a bug this PR introduced — badge sat 2px past the screen edge. One-token change.

Net −129 lines of CSS.

Several of the new comment blocks ran to four, five and six lines where two or
three carry the same reasoning, and the motion.css and press.css headers were
the longest in tokens/ against a 5-13 line house style. Trimmed the ones this
branch introduced; the pre-existing long blocks elsewhere are left alone.

Comments only -- stripping all comments from both revisions leaves the files
identical.
@lokesh
lokesh requested a review from RayBB September 11, 2026 05:18
@lokesh lokesh added Needs: Review This issue/PR needs to be reviewed in order to be closed or merged (see comments). [managed] Module: Main Site Navigation Theme: Design Issues related to UI design, branding, etc. [managed] Affects: UI Issues with the web site's user interface. [managed] labels Sep 11, 2026

@RayBB RayBB left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks like everything was fixed and it's good to go (deployed to testing).

Created a followup issue here that you can refine as needed and do or assign to someone.

#13606

@lokesh
lokesh merged commit 221c701 into master Sep 11, 2026
8 checks passed
@github-project-automation github-project-automation Bot moved this from Someone else is working on it to Done in Ray's Project Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Affects: UI Issues with the web site's user interface. [managed] Module: Main Site Navigation Needs: Review This issue/PR needs to be reviewed in order to be closed or merged (see comments). [managed] On Testing Theme: Design Issues related to UI design, branding, etc. [managed]

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants