Skip to content

feat(button): add neutral variant to the button directive - #3853

Merged
Mpdreamz merged 1 commit into
mainfrom
flb/neutral-button-variant
Aug 13, 2026
Merged

feat(button): add neutral variant to the button directive#3853
Mpdreamz merged 1 commit into
mainfrom
flb/neutral-button-variant

Conversation

@florent-leborgne

@florent-leborgne florent-leborgne commented Aug 13, 2026

Copy link
Copy Markdown
Member

Follow-up to the hub pages thread. Martijn asked for a third button type exposed on the button directive, and Fabri described it as a monochrome secondary button.

Problem

The {button} directive offers two variants and both are blue. primary is a filled Elastic blue, secondary is an Elastic blue outline.

When the two sit next to each other, the secondary button competes with the primary for attention. A cluster of peer links has no correct option at all, because every button claims to be a call to action. That is what blocks the hub hero, where several buttons need equal rank.

What this adds

:type: neutral. It keeps the geometry of the existing variants and changes only the color and the weight.

Property Value Token
Border color #868e9a grey-70
Border width 1px border
Text color #1c1e23 ink-dark
Background transparent none
Font weight 500 font-medium
Hover border #1c1e23 ink-dark
Hover background #f6f9fc grey-10
Focus ring #85b7ff blue-elastic-50

Every value comes from theme.css. The change introduces no new tokens.

Height, padding, radius, and font size match primary and secondary exactly, so a neutral button lines up with its neighbors inside a button-group. The focus ring stays Elastic blue, so keyboard focus reads the same across all three variants.

Two deliberate divergences, please review

These are the parts I'd most like a second opinion on, because they depart from _LandingPage.cshtml, which is where the existing two styles came from.

  1. Border width is 1px, against 2px on secondary. A 2px grey border carried too much visual weight for a variant whose job is to recede.
  2. Font weight is 500, against 600 on both existing variants. Same reasoning.

Both keep box-sizing: border-box, so the outer geometry does not change and mixed-variant groups still align.

Accessibility

WCAG 1.4.11 asks for 3:1 contrast on the boundary that identifies a control. grey-70 measures 3.31:1 against white and is the lightest grey token that clears it. grey-60 fails at 2.67:1. Text contrast measures 16.68:1.

Worth flagging for the hub pages stack: the current proof of concept uses grey-20 (1.32:1) and grey-30 (1.63:1) borders. Both fail that threshold and need lifting when they adopt this variant.

Out of scope

  • A second monochrome level, such as an ink-dark bordered lead action. The hero can pair one primary with several neutral buttons instead. Worth revisiting if the all-monochrome hero wins.
  • An orthogonal :tone: property. It doubles the style surface for combinations nobody has asked for yet.
  • Migrating the hub pages CSS onto this class. That belongs in the hub pages stack.
  • Arrow and chevron behavior, which is a separate thread of feedback.

Testing

  • Four new test classes in ButtonTests.cs covering the type, the :variant: alias, rendering inside a button-group, and no diagnostics. dotnet test tests/Elastic.Markdown.Tests/ --filter "FullyQualifiedName~Button" passes 72 tests.
  • npm run build in src/Elastic.Documentation.Site confirms every Tailwind utility resolves to a real token.
  • docs/syntax/buttons.md documents the variant, so the rendered preview of /syntax/buttons exercises it.

🤖 Generated with Claude Code

The button directive offered two variants and both were blue. A
`secondary` button next to a `primary` one competes for attention. A
cluster of peer links had no correct option, because every button
claimed to be a call to action.

Add `:type: neutral`. It keeps the geometry of the existing variants and
changes only the color and the weight. All values come from theme.css,
so the change introduces no new tokens.

The border uses `grey-70`. That is the lightest grey token that clears
the 3:1 non-text contrast ratio in WCAG 1.4.11, at 3.31:1 against white.
`grey-60` fails at 2.67:1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@florent-leborgne
florent-leborgne requested review from a team as code owners August 13, 2026 10:41
@florent-leborgne
florent-leborgne requested a review from cotti August 13, 2026 10:41
@Mpdreamz
Mpdreamz merged commit 1ec2f77 into main Aug 13, 2026
28 of 30 checks passed
@Mpdreamz
Mpdreamz deleted the flb/neutral-button-variant branch August 13, 2026 14:14
@Mpdreamz

Copy link
Copy Markdown
Member

@itsalexcm merged this one in but your word is final.

florent-leborgne added a commit that referenced this pull request Aug 13, 2026
#3853 added `:type: neutral` to the {button} directive, a monochrome
outline for clusters where blue competes with the primary action. That is
exactly what the hero needs: three actions of equal rank, none of which
should claim to be the call to action.

#3853 also flagged that this stack used border colours failing the 3:1
non-text contrast ratio. The eyebrow pill moves from grey-20, which reads
at 1.32:1, to grey-70 at 3.31:1. The border is what identifies that
control, so it has to clear the threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mpdreamz pushed a commit that referenced this pull request Aug 14, 2026
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Mpdreamz added a commit that referenced this pull request Aug 14, 2026
* Add hub page layout and the {hero} directive

Adds the first two pieces of the hub page mechanism, rebuilt on the current
navigation implementation. The prototype branch is merged with nav-v2, which is
out of scope, so nothing from nav-v2 ports.

Adds `layout: hub`, which drops the right-rail table of contents and the
previous/next navigation, keeps the left sidebar, and gives the body the full
content column. Page-title detection gains two fallbacks, so a page composed
only of directives still resolves a title: an H1 nested in a directive, then the
first {hero} directive's :title: option.

Adds {hero}, the identity band. It carries a product icon, the page title, a
description, and up to three optional actions.

Promotes the prototype's hub-only link validator to a shared
DirectiveLinkValidator. Directive links never pass through Markdig's inline link
parser, so every directive that takes a URL had invented its own handling.
{page-card} moves onto the shared class and its hand-rolled copy goes. It keeps
its previous contract: relative links resolve against the source file, and no
file-existence check runs, because page-card links can target generated pages
that have no markdown file on disk.

Guards DirectiveBlockParser.TryContinue so an ancestor directive stops consuming
option lines once it has opened a nested directive child. Without the guard the
ancestor swallows every descendant's options and the last one wins. This affects
every nested directive, so it lands with regression tests over tab sets,
steppers, and dropdowns.

Adds LLM markdown and plain-text renderers for {hero}. A hub page has no body
prose, so without them it exports as an empty shell. The plain-text renderer
feeds the search index and emits the hero title and description only. Hub pages
answer generic "<product> docs" queries. Indexing section and card titles would
let a hub compete with the pages it links to on specific queries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix hub page regressions found in the rendered output

Three defects, all introduced in this branch rather than carried from the
prototype.

Suppress the page-level H1 and the applies-to badge on a hub page. The prototype
guards both in Index.cshtml and that guard was missed, so every hub page rendered
the page title twice: once from Index.cshtml, once from {hero}. This also
delivers the applies-to suppression that #1386 records but the code did not do.

Drop the markdown-content class from the hub article. The class was added to
reach the JS hooks, but those select on the markdown-content id, not the class.
typography.css is unlayered and hub.css sits in @layer components, so unlayered
rules win regardless of specificity. The class made typography.css restyle the
hero eyebrow as a blue underlined link and skew the icon alignment against the
heading. The id stays, so hljs, smooth-scroll and toc-nav keep working.

Give hero actions the same link treatment as inline links. An external action now
opens in a new tab with rel="noopener noreferrer" and no preload, and an anchor
action no longer preloads. Only an internal link preloads.

Fix an empty href for "/index.md". Stripping the whole "/index.md" segment left
an empty string rather than the site root.

The example page gains three actions, one internal and two external, to
demonstrate both forms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Align the hero product mark with the text column

The mark sat about 9px right of the eyebrow and the description below it. Two
insets stacked. Each icon's viewBox carried transparent padding around the glyph,
which cost 5px at the rendered size. The 48px chip then centred the 40px SVG
inside itself, which cost another 4px.

Crops every icon's viewBox to its glyph, so no transparent padding is left to
absorb, and lets the box hug the mark instead of forcing it into a fixed square.
Width now follows from the glyph's own aspect ratio.

The letter fallback keeps the chip. It has no glyph to align, and the chip's own
edge already lines up with the text column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Make the example a docs-builder hub, and keep cross-links out of new tabs

The example page was an Elasticsearch hub whose links had all been repointed at
docs-builder pages, leaving an Elasticsearch name, icon, hero and headings on top
of docs-builder content. It is now a docs-builder hub throughout, renamed to
docs/examples/products/docs-builder.md.

Its hero demonstrates all three link forms: a site-absolute path, a docs-content
cross-link, and an in-page anchor. The unknown `docs-builder` icon key also
exercises the single-letter fallback.

Fixes a defect the cross-link uncovered. A cross-link resolves to a full URL, so
the hero treated it as external and opened it in a new tab. Inline links make the
opposite call, and deliberately so: a cross-link still points at documentation
this site serves. DirectiveLinkValidator now records the URLs it resolved from a
cross-link scheme, and the view model consults that rather than guessing from the
scheme.

Adds doesNotContainHtml to the authoring assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Apply review feedback to the hero and hub layout

Martijn's review on #3825 and theletterf's questions.

- The hero no longer adds its own horizontal padding. The content column
  supplies the gutter, so the hero aligned with the sections below it only
  above the shared max-width. Below it the hero sat 24px further in.
- Hero type moves onto the site's scale. The root font size is 14px, so
  values written as 16px-root arithmetic rendered around 12% small. The
  description in particular used a size found nowhere else on the site.
- The three hero actions carry equal weight and take the secondary button
  treatment from the {button} directive, rather than a bespoke hero button.
  The option names set order, not weight.
- A page with `layout: hub` and no {hero} now fails the build. The layout
  removes the page H1, so without a hero the page has no title at all and
  falls back to its file path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Raise the hero eyebrow to body size

Martijn's point about tiny fonts applies to every piece of hub text, not
only the ones in his screenshot. The eyebrow is a reading affordance, so it
takes the body size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Follow the site's rule for arrows on links

The site puts an arrow on two kinds of link: one that sends the reader
onward out of a section, such as "View all reference docs" on the landing
page, and a row that is itself the target, such as {page-card} or the
previous and next footer. It marks no ordinary link and no button.

The hero followed neither. Its actions are buttons, so they carried an
arrow the site gives no button, and only when the target was an in-page
anchor. Both are gone.

The eyebrow keeps its arrow, because it sends the reader onward to the docs
home. It now uses the site's glyph and hover slide through one shared
`.hub-arrow` class, rather than a second arrow drawn by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Put the hero actions on the neutral button variant

#3853 added `:type: neutral` to the {button} directive, a monochrome
outline for clusters where blue competes with the primary action. That is
exactly what the hero needs: three actions of equal rank, none of which
should claim to be the call to action.

#3853 also flagged that this stack used border colours failing the 3:1
non-text contrast ratio. The eyebrow pill moves from grey-20, which reads
at 1.32:1, to grey-70 at 3.31:1. The border is what identifies that
control, so it has to clear the threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add Logstash icon to hub ProductIcons (#3843)

Hub heroes fall back to initials when a product key is missing from the
inline SVG map. Logstash hubs need the product mark.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Brandon Morelli <brandon.morelli@elastic.co>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>
yetanothertw pushed a commit that referenced this pull request Aug 14, 2026
* Add hub page layout and the {hero} directive

Adds the first two pieces of the hub page mechanism, rebuilt on the current
navigation implementation. The prototype branch is merged with nav-v2, which is
out of scope, so nothing from nav-v2 ports.

Adds `layout: hub`, which drops the right-rail table of contents and the
previous/next navigation, keeps the left sidebar, and gives the body the full
content column. Page-title detection gains two fallbacks, so a page composed
only of directives still resolves a title: an H1 nested in a directive, then the
first {hero} directive's :title: option.

Adds {hero}, the identity band. It carries a product icon, the page title, a
description, and up to three optional actions.

Promotes the prototype's hub-only link validator to a shared
DirectiveLinkValidator. Directive links never pass through Markdig's inline link
parser, so every directive that takes a URL had invented its own handling.
{page-card} moves onto the shared class and its hand-rolled copy goes. It keeps
its previous contract: relative links resolve against the source file, and no
file-existence check runs, because page-card links can target generated pages
that have no markdown file on disk.

Guards DirectiveBlockParser.TryContinue so an ancestor directive stops consuming
option lines once it has opened a nested directive child. Without the guard the
ancestor swallows every descendant's options and the last one wins. This affects
every nested directive, so it lands with regression tests over tab sets,
steppers, and dropdowns.

Adds LLM markdown and plain-text renderers for {hero}. A hub page has no body
prose, so without them it exports as an empty shell. The plain-text renderer
feeds the search index and emits the hero title and description only. Hub pages
answer generic "<product> docs" queries. Indexing section and card titles would
let a hub compete with the pages it links to on specific queries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix hub page regressions found in the rendered output

Three defects, all introduced in this branch rather than carried from the
prototype.

Suppress the page-level H1 and the applies-to badge on a hub page. The prototype
guards both in Index.cshtml and that guard was missed, so every hub page rendered
the page title twice: once from Index.cshtml, once from {hero}. This also
delivers the applies-to suppression that #1386 records but the code did not do.

Drop the markdown-content class from the hub article. The class was added to
reach the JS hooks, but those select on the markdown-content id, not the class.
typography.css is unlayered and hub.css sits in @layer components, so unlayered
rules win regardless of specificity. The class made typography.css restyle the
hero eyebrow as a blue underlined link and skew the icon alignment against the
heading. The id stays, so hljs, smooth-scroll and toc-nav keep working.

Give hero actions the same link treatment as inline links. An external action now
opens in a new tab with rel="noopener noreferrer" and no preload, and an anchor
action no longer preloads. Only an internal link preloads.

Fix an empty href for "/index.md". Stripping the whole "/index.md" segment left
an empty string rather than the site root.

The example page gains three actions, one internal and two external, to
demonstrate both forms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Align the hero product mark with the text column

The mark sat about 9px right of the eyebrow and the description below it. Two
insets stacked. Each icon's viewBox carried transparent padding around the glyph,
which cost 5px at the rendered size. The 48px chip then centred the 40px SVG
inside itself, which cost another 4px.

Crops every icon's viewBox to its glyph, so no transparent padding is left to
absorb, and lets the box hug the mark instead of forcing it into a fixed square.
Width now follows from the glyph's own aspect ratio.

The letter fallback keeps the chip. It has no glyph to align, and the chip's own
edge already lines up with the text column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Make the example a docs-builder hub, and keep cross-links out of new tabs

The example page was an Elasticsearch hub whose links had all been repointed at
docs-builder pages, leaving an Elasticsearch name, icon, hero and headings on top
of docs-builder content. It is now a docs-builder hub throughout, renamed to
docs/examples/products/docs-builder.md.

Its hero demonstrates all three link forms: a site-absolute path, a docs-content
cross-link, and an in-page anchor. The unknown `docs-builder` icon key also
exercises the single-letter fallback.

Fixes a defect the cross-link uncovered. A cross-link resolves to a full URL, so
the hero treated it as external and opened it in a new tab. Inline links make the
opposite call, and deliberately so: a cross-link still points at documentation
this site serves. DirectiveLinkValidator now records the URLs it resolved from a
cross-link scheme, and the view model consults that rather than guessing from the
scheme.

Adds doesNotContainHtml to the authoring assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Apply review feedback to the hero and hub layout

Martijn's review on #3825 and theletterf's questions.

- The hero no longer adds its own horizontal padding. The content column
  supplies the gutter, so the hero aligned with the sections below it only
  above the shared max-width. Below it the hero sat 24px further in.
- Hero type moves onto the site's scale. The root font size is 14px, so
  values written as 16px-root arithmetic rendered around 12% small. The
  description in particular used a size found nowhere else on the site.
- The three hero actions carry equal weight and take the secondary button
  treatment from the {button} directive, rather than a bespoke hero button.
  The option names set order, not weight.
- A page with `layout: hub` and no {hero} now fails the build. The layout
  removes the page H1, so without a hero the page has no title at all and
  falls back to its file path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Raise the hero eyebrow to body size

Martijn's point about tiny fonts applies to every piece of hub text, not
only the ones in his screenshot. The eyebrow is a reading affordance, so it
takes the body size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Follow the site's rule for arrows on links

The site puts an arrow on two kinds of link: one that sends the reader
onward out of a section, such as "View all reference docs" on the landing
page, and a row that is itself the target, such as {page-card} or the
previous and next footer. It marks no ordinary link and no button.

The hero followed neither. Its actions are buttons, so they carried an
arrow the site gives no button, and only when the target was an in-page
anchor. Both are gone.

The eyebrow keeps its arrow, because it sends the reader onward to the docs
home. It now uses the site's glyph and hover slide through one shared
`.hub-arrow` class, rather than a second arrow drawn by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Put the hero actions on the neutral button variant

#3853 added `:type: neutral` to the {button} directive, a monochrome
outline for clusters where blue competes with the primary action. That is
exactly what the hero needs: three actions of equal rank, none of which
should claim to be the call to action.

#3853 also flagged that this stack used border colours failing the 3:1
non-text contrast ratio. The eyebrow pill moves from grey-20, which reads
at 1.32:1, to grey-70 at 3.31:1. The border is what identifies that
control, so it has to clear the threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add Logstash icon to hub ProductIcons (#3843)

Hub heroes fall back to initials when a product key is missing from the
inline SVG map. Logstash hubs need the product mark.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Brandon Morelli <brandon.morelli@elastic.co>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants