Files
Calvin 5021556c8e BDS component polish: link contrast, static cards, uniform CTAs, stacked hero (#3851)
* BDS component polish: link visibility, static cards, uniform CTAs

Component-level fixes and refinements across the 2026 brand components.

Accessibility / contrast
- StandardCard: pin description link colors. Every card variant has a light
  background in both themes, so theme-level link colors were wrong inside it —
  dark mode painted plain anchors white and BdsLink lilac-300, both of which
  wash out on a pale card. Links (including :visited) now use the card's own
  text color with an underline carrying the affordance; hover uses lilac-500,
  which clears 4.5:1 on all four card backgrounds.
- Breadcrumbs: point the open dropdown menu at the breadcrumb color tokens so
  the trail, trigger, and menu read as one color in both themes, and give dark
  mode gray-6 (7.41:1, matching light mode's 7.23:1).

Interaction
- CardOffgrid: drop every hover affordance when a card has neither href nor
  onClick. Static cards no longer get a pointer cursor, the color-wipe overlay,
  or a pressed state — the tokenization and trading carousels pass link-less
  cards and were advertising a click target that did not exist.
- CarouselFeatured: add a 'fade' transition alongside the default 'slide', for
  decks whose slides share a heading. Inactive slides are now inert, keeping
  focus and pointer events out of them in both styles.
- Add a bds-reduced-motion mixin so components can collapse motion to an
  instant state change.

Design consistency
- ButtonGroup: add forceVariant and forceNoPadding overrides, letting a section
  opt out of the count-based variant defaults and render one uniform treatment.
  Existing consumers are unaffected.
- FeatureTwoColumn: render every link as a tertiary button regardless of count,
  flush with the title and description.
- CardImage: render an all-bullet subtitle as a real <ul> so wrapped text hangs
  under the first character and screen readers announce it as a list.
- docs: give the node-installation card descriptions a paragraph break before
  their "Learn More" link.

Dependencies
- Bump @codemirror/state and view, add lang-javascript, lang-json, and lint,
  with overrides pinning state/view to one copy.

Docs updated alongside each component. Built CSS regenerated with the
production script to match the committed artifact's minified format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ButtonGroup: don't strip padding from forced filled variants

`noPadding` was derived as `forceNoPadding || isMultiButton`, which was safe
while the 3+ block layout was always tertiary. `forceVariant` also accepts
`primary` and `secondary`, so a 3-button group forcing a filled variant had
`padding: 0 !important` (Button.scss) applied with no way to opt out.

Tie the implied no-padding to the resolved variant being tertiary. Behavior is
unchanged for every caller that doesn't pass `forceVariant`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stacked hero variant, fade-by-default carousel, component docs

Layout
- HeaderHeroPrimaryMedia: add a `stacked` prop that puts the headline, subtitle,
  and buttons in one column (full width at base, 7/8 at md, 9/12 at lg) instead
  of the default headline-left / CTA-right split. The headline and CTA block
  bottom-align against each other in the two-column layout, so the stacked
  variant undoes that. Both arrangements now share the same headline and CTA
  elements, so they can't drift apart. Used on the docs landing hero.

Motion
- CarouselFeatured: make `fade` the default transition. Slides that share a
  heading are the common case, and a horizontal wipe drags the identical heading
  across the screen only to set it back down. `slide` is now opt-in for decks
  whose panels are genuinely distinct. Nudge the crossfade to 260ms in / 200ms
  out. The home page carousel opts into fade explicitly; developer-funding now
  inherits it.

Fixes
- CardTextIconCard: pass `headingAs` through to `cardContent`, which was
  accepting the prop but never receiving it.

Docs
- Expand the Button, CardImage, CardTextIcon, PageGrid, CalloutMediaBanner,
  LogoSquareGrid, and HeaderHeroPrimaryMedia references.

Built CSS regenerated with the production script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:49:10 -07:00

6.9 KiB

HeaderHeroPrimaryMedia Pattern

A page-level hero pattern featuring a headline, subtitle, call-to-action buttons, and a primary media element. Supports images, videos, or custom React elements with enforced aspect ratios and object-fit constraints.

Overview

The HeaderHeroPrimaryMedia component provides a structured hero section with:

  • Responsive headline and subtitle layout
  • Primary and optional secondary call-to-action buttons
  • Media element (image, video, or custom) with responsive aspect ratios
  • Development-time validation warnings

Basic Usage

import HeaderHeroPrimaryMedia from "shared/sections/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";

function MyPage() {
  return (
    <HeaderHeroPrimaryMedia
      headline="Build on XRPL"
      subtitle="Start developing today with our comprehensive developer tools."
      links={[{ label: "Get Started", href: "/docs" }]}
      media={{
        type: "image",
        src: "/img/hero.png",
        alt: "XRPL Development",
      }}
    />
  );
}

Props

Prop Type Required Description
headline React.ReactNode Yes Hero headline text (display-md typography)
subtitle React.ReactNode Yes Hero subtitle text (label-l typography)
links DesignConstrainedLink[] No Array of { label, href } for ButtonGroup
media HeaderHeroMedia Yes Media element (image, video, or custom)
stacked boolean No Stack headline, subtitle, and buttons in one column. Defaults to false. See Layout
className string No Additional CSS classes for the header element
...rest HTMLHeaderElement attributes No Any other HTML header attributes

The links prop accepts an array of { label, href } objects for consistent ButtonGroup rendering; variant and color are set by the component:

  • First link: variant="primary", color="green"
  • Second link: variant="tertiary", color="green"
  • Max 2 links supported (ButtonGroup validation)

Layout

stacked Layout
false (default) Two columns: headline on the left (5 of 12 on lg), subtitle and buttons on the right. The headline bottom-aligns against the CTA column.
true One column (full width at base, 7 of 8 at md, 9 of 12 at lg — note this grid is 4/8/12 columns by breakpoint, not 12 throughout): headline, then subtitle, then buttons, with 16px between the headline and subtitle. The subtitle-to-buttons gap keeps the standard 24px (40px on lg).
<HeaderHeroPrimaryMedia
  stacked
  headline="Build on the XRP Ledger"
  subtitle="Everything you need to get started."
  media={{ type: "image", src: "/hero.jpg", alt: "Hero" }}
/>

Media placement is unchanged — it stays full-width below the text in both layouts.

Media Types

The media prop accepts a discriminated union of three types:

Image Media

media={{
  type: "image",
  src: string,        // Required
  alt: string,        // Required
  // ... all native <img> props except className and style
}}

Example:

media={{
  type: "image",
  src: "/img/hero.png",
  alt: "Hero image",
  loading: "lazy",
  decoding: "async"
}}

Video Media

media={{
  type: "video",
  src: string,        // Required
  alt?: string,       // Optional but recommended
  // ... all native <video> props except className and style
}}

Example:

media={{
  type: "video",
  src: "/video/intro.mp4",
  alt: "Introduction video",
  autoPlay: true,
  loop: true,
  muted: true,
  playsInline: true
}}

Custom Element Media

media={{
  type: "custom",
  element: React.ReactElement  // Required
}}

Example:

media={{
  type: "custom",
  element: <MyAnimationComponent />
}}

Examples

With Secondary CTA

<HeaderHeroPrimaryMedia
  headline="Real-world asset tokenization"
  subtitle="Learn how to issue crypto tokens and build solutions."
  links={[
    { label: "Get Started", href: "/docs" },
    { label: "Learn More", href: "/about" },
  ]}
  media={{
    type: "image",
    src: "/img/tokenization.png",
    alt: "Tokenization",
  }}
/>

Video Media

<HeaderHeroPrimaryMedia
  headline="Watch and Learn"
  subtitle="Explore our video tutorials."
  links={[{ label: "Watch Tutorials", href: "/tutorials" }]}
  media={{
    type: "video",
    src: "/video/intro.mp4",
    alt: "Introduction video",
    autoPlay: true,
    loop: true,
    muted: true,
  }}
/>

Custom Element

<HeaderHeroPrimaryMedia
  headline="Interactive Experience"
  subtitle="Engage with custom media."
  links={[{ label: "Explore", href: "/interactive" }]}
  media={{
    type: "custom",
    element: <MyAnimationComponent />,
  }}
/>

Design Constraints

The component enforces specific design requirements:

  • Aspect Ratios: Media maintains responsive aspect ratios:
    • Base: 16:9
    • Medium (md+): 2:1
    • Large (lg+): 3:1
  • Object Fit: All media uses object-fit: cover to fill the container
  • Type Safety: TypeScript discriminated unions ensure type-safe media selection

Validation

The component includes development-time validation that logs warnings to the console when required props are missing:

  • Missing headline: Component returns null (error logged)
  • Missing subtitle, links, or media: Warning logged, component still renders

CSS Classes

The component generates the following CSS classes:

  • bds-header-hero-primary-media - Root header element
  • bds-header-hero-primary-media--stacked - Root modifier, present when stacked is set
  • bds-header-hero-primary-media__stack - Single-column wrapper, only rendered when stacked is set
  • bds-header-hero-primary-media__headline - Headline container
  • bds-header-hero-primary-media__subtitle - Subtitle element
  • bds-header-hero-primary-media__cta-container - CTA container
  • bds-header-hero-primary-media__cta-buttons - CTA buttons wrapper
  • bds-header-hero-primary-media__media-container - Media container
  • bds-header-hero-primary-media__media-element - Media element

Best Practices

  1. Media Selection: Choose media that works well with the responsive aspect ratios (16:9 base, 2:1 md+, 3:1 lg+)
  2. Alt Text: Always provide meaningful alt text for images and videos
  3. Performance: Use loading="lazy" for images below the fold
  4. CTAs: Keep CTA text concise and action-oriented
  5. Headlines: Keep headlines concise and impactful