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

7.8 KiB

CardImage Component Documentation

Overview

The CardImage component is a responsive card implementation following the XRPL Brand Design System (BDS). It displays an image, title, subtitle, and call-to-action button with three responsive size variants that adapt to viewport width.

Features

  • Three Responsive Variants: LG (≥992px), MD (576px-991px), SM (<576px)
  • Interactive States: Default, Hover, Focus, Pressed, Disabled
  • Button Animation on Card Hover: Hovering the card triggers the button's hover animation
  • Flexible Usage: Supports both link navigation and click handlers
  • Accessibility: Keyboard navigation and screen reader support

Props API

interface CardImageProps {
  /** Image source URL */
  image: string;
  /** Alt text for the image */
  imageAlt: string;
  /** Card title (1 line only) */
  title: string;
  /** Card subtitle (max 3 lines). `\n` renders as a line break; an all-bullet subtitle renders as a list. */
  subtitle: string;
  /** Button label text */
  buttonLabel: string;
  /** Link destination (renders card as clickable link) */
  href?: string;
  /** Click handler for the button */
  onClick?: () => void;
  /** Disabled state */
  disabled?: boolean;
  /** Optional className for custom styling */
  className?: string;
  /** When true, image fills entire container with object-fit: cover (no visible background) */
  fullBleed?: boolean;
  /** Custom background color for image container (defaults to gray-100) */
  backgroundColor?: string;
}

Default Values

  • disabled: false
  • className: ''
  • fullBleed: false
  • backgroundColor: undefined (falls back to the gray-100 image background)

Subtitle Formatting

subtitle is a plain string, but it is not rendered blindly:

  • Lines are split on \n, and blank lines are dropped.
  • If every line begins with a bullet marker (•, -, or *), the subtitle renders as a <ul> (bds-card-image__subtitle--list) with one <li> per line.
  • Anything else renders as a single <p>, where white-space: pre-line means \n still produces a visible line break.
// Renders as a bulleted list
<CardImage subtitle={"• Fast settlement\n• Low fees\n• Built-in DEX"} ... />

// Renders as a paragraph with a line break
<CardImage subtitle={"Fast settlement.\nLow fees."} ... />

Image Sizing

By default the image sits in a fixed-height container against a gray-100 background, preserving its aspect ratio.

  • fullBleed — sets bds-card-image--full-bleed, which drops the card's fixed height (height: auto) and makes the image fill the container using a 1:1 aspect ratio with object-fit: cover. Use this when the image is the whole visual and you don't want letterboxing.
  • backgroundColor — sets the --bds-card-image-bg custom property on the image container, overriding the gray-100 default. Has no visible effect alongside fullBleed, since the image covers the container.
<CardImage fullBleed image="/images/hero.png" ... />
<CardImage backgroundColor="#F3F1EB" image="/images/logo.svg" ... />

Responsive Variants

LG (Large) - Desktop (≥992px)

  • Card height: 620px
  • Image height: 400px (1:1 aspect ratio preserved)
  • 3-column grid width

MD (Medium) - Tablet (576px - 991px)

  • Card height: 560px
  • Image height: 280px
  • 2-column grid width

SM (Small) - Mobile (<576px)

  • Card height: 536px
  • Image height: 268px
  • 1-column grid width (full width)

States

Default State

The default interactive state with the button showing its primary green background.

Hover State

When the user hovers over the card:

  • Button background fills from bottom-to-top (green-300 → green-200)
  • Arrow icon line shrinks
  • Gap between label and icon increases

Focus State

When the card receives keyboard focus:

  • Black border outline around the card
  • Button shows focus styling

Pressed State

When the button is being pressed:

  • Button returns to default styling momentarily

Disabled State

When disabled={true}:

  • Card is non-interactive
  • Button shows disabled styling (gray background, no icon)
  • Text colors muted
  • Cursor changes to not-allowed

Usage Examples

import { CardImage } from 'shared/components/CardImage';

<CardImage
  image="/images/docs-hero.png"
  imageAlt="Documentation illustration"
  title="Documentation"
  subtitle="Access everything you need to get started working with the XRPL."
  buttonLabel="Get Started"
  href="/docs"
/>

Card with Click Handler

<CardImage
  image="/images/feature.png"
  imageAlt="Feature illustration"
  title="New Feature"
  subtitle="Learn about our latest feature and how it can help you build better applications."
  buttonLabel="Learn More"
  onClick={() => console.log('clicked')}
/>

Disabled Card

<CardImage
  image="/images/coming-soon.png"
  imageAlt="Coming soon"
  title="Coming Soon"
  subtitle="This feature is not yet available."
  buttonLabel="Unavailable"
  disabled
/>

Card with Custom Class

<CardImage
  image="/images/hero.png"
  imageAlt="Hero image"
  title="Custom Styled"
  subtitle="Card with additional custom styling."
  buttonLabel="Explore"
  href="/explore"
  className="my-custom-card"
/>

Design Tokens

Spacing

Token Value Description
Image-to-content gap 24px Gap between image and content area
Title-to-subtitle gap 12px Gap between title and subtitle
Content horizontal padding 8px Left/right padding for content
Button margin-top 30px Button locked to bottom
Border radius 16px Card corner radius

Colors

Element Light Mode Dark Mode
Card background #FFFFFF Gray 800
Card border Gray 300 (#CAD4DF) Gray 700
Image background Gray 100 (#F0F3F7) Gray 700
Text color #141414 White

Typography

Element Token Specs
Title .sh-md-l 28px/35px, -0.5px letter-spacing, light weight
Subtitle .body-l 18px/26.1px, -0.5px letter-spacing, light weight

How It Works

Hover Animation

The card tracks hover state via React's useState. When hovered:

  1. A bds-card-image--hovered class is added to the card
  2. CSS rules target the nested Button component and apply hover styles
  3. The button's ::before pseudo-element animates (background fill)
  4. The arrow icon line shrinks via scaleX(0)

This approach ensures the button animation triggers even when hovering areas of the card outside the button itself.

When href is provided:

  • The entire card becomes clickable
  • Clicking anywhere on the card navigates to the href
  • The card is keyboard accessible (Enter/Space to activate)

Button Integration

The component uses the existing BDS Button component with:

  • variant="primary" - Solid green background
  • color="green" - Green color theme

Accessibility

Keyboard Navigation

  • Tab: Focus the card
  • Enter/Space: Activate the card (navigate to href or trigger onClick)

Screen Reader Support

  • Card has appropriate role="link" when href is provided
  • Image has descriptive alt text
  • aria-disabled attribute for disabled state

File Structure

shared/components/CardImage/
├── CardImage.tsx      # Component implementation
├── CardImage.scss     # Component styles
├── CardImage.md       # This documentation
└── index.ts           # Exports
  • Button: Used internally for the CTA button
  • CardOffgrid: Similar card component with different layout

Browser Support

The component uses modern CSS features:

  • CSS Grid/Flexbox
  • CSS transforms and transitions
  • :focus-visible pseudo-class
  • -webkit-line-clamp for text truncation

All features are widely supported in modern browsers.