* 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>
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 |
Links (ButtonGroup)
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
- Base:
- Object Fit: All media uses
object-fit: coverto 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 returnsnull(error logged) - Missing
subtitle,links, ormedia: Warning logged, component still renders
CSS Classes
The component generates the following CSS classes:
bds-header-hero-primary-media- Root header elementbds-header-hero-primary-media--stacked- Root modifier, present whenstackedis setbds-header-hero-primary-media__stack- Single-column wrapper, only rendered whenstackedis setbds-header-hero-primary-media__headline- Headline containerbds-header-hero-primary-media__subtitle- Subtitle elementbds-header-hero-primary-media__cta-container- CTA containerbds-header-hero-primary-media__cta-buttons- CTA buttons wrapperbds-header-hero-primary-media__media-container- Media containerbds-header-hero-primary-media__media-element- Media element
Best Practices
- Media Selection: Choose media that works well with the responsive aspect ratios (16:9 base, 2:1 md+, 3:1 lg+)
- Alt Text: Always provide meaningful alt text for images and videos
- Performance: Use
loading="lazy"for images below the fold - CTAs: Keep CTA text concise and action-oriented
- Headlines: Keep headlines concise and impactful