Skip to content

HyCard

Usage examples on this page are written for Lit / plain HTML (<hy-card>). The same component ships as HyCard in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A flexible card component that provides a structured container for content. Supports header, body, footer, and image sections with automatic layout management. Can be made clickable for interactive use cases with full keyboard navigation support. Can be disabled to prevent interaction while maintaining visual context.

Hybrid SSR/CSR Section Visibility System

The card works seamlessly in both SSR and CSR-only (SPAs, Storybook) environments using a hybrid visibility approach:

In SSR environments:

  1. The host framework detects slot presence at render time using its own slot API
  2. Passes boolean attributes (has-header, has-image, has-body, has-footer) to the Lit component
  3. CSS uses these attributes for immediate section visibility during initial render
  4. After hydration, HasSlotController confirms slot content and adds .has-content classes

In CSR-only environments (SPAs, Storybook):

  1. All sections render to DOM (no has-* attributes)
  2. Sections are hidden by default via CSS (display: none)
  3. firstUpdated() runs and hideSectionsWithoutContent() detects actual content
  4. Adds .has-content classes which trigger CSS visibility rules
  5. Property-based content also triggers visibility via attribute selectors

This approach ensures consistent behavior across all rendering environments while optimizing for SSR performance when available.

Border Management

Borders between sections are automatically managed using the general sibling combinator (~):

  • Any visible section preceded by another visible section gets a top border
  • Works in both SSR (via has-* attributes) and CSR (via .has-content class)
  • The ~ combinator correctly handles hidden sections between visible ones
  • No manual border removal logic needed - CSS handles all cases automatically

Example: If header and footer are visible but image/body are hidden, footer still gets a border because the rule .has-content ~ .has-content or :host([has-header]) .header ~ .footer applies even with hidden sections in between.

Rendering Strategy

All four sections (header, image, body, footer) are always rendered to the DOM. Visibility is controlled entirely through CSS, which provides:

  • Universal compatibility across SSR and CSR environments
  • No conditional rendering that could break in different contexts
  • Clean separation: JavaScript detects content, CSS controls visibility
  • Consistent DOM structure regardless of rendering environment

While this means empty sections exist in the DOM (hidden), the approach ensures maximum compatibility and follows standard component library patterns.

Content Sources

Each section can receive content from two sources:

  • Slot content: Takes precedence and overrides property-based content for that section
  • Property-based content: Used when no slot content is present (header and body only)

In SSR environments: The host framework uses its own slot-detection API to conditionally render slots, preventing empty slot elements and setting has-* attributes for immediate visibility.

In CSR-only environments: All slots are rendered, and the HasSlotController detects actual content post-mount, adding .has-content classes for CSS visibility control.

Accessibility Features

  • Full keyboard navigation support (Enter/Space keys for clickable cards)
  • Proper ARIA attributes (role="button", aria-disabled, tabindex management)
  • High contrast mode support with enhanced focus indicators
  • Screen reader utilities with .sr-only class for hidden but accessible content
  • Reduced motion support respecting user preferences

Themeable via the @cssprop override hooks documented below.

Usage Patterns

Examples

html
<!-- Basic card with properties (works in both SSR and CSR) -->
<hy-card header-title="Card Title" body-title="Content">
  <p>Main content goes here.</p>
</hy-card>
html
<!-- Card with slots (SSR frameworks can pass has-* hints for faster initial render) -->
<hy-card>
  <h3 slot="header">Card Title</h3>
  <img slot="image" src="photo.jpg" alt="Card image" />
  <p>Main content goes here.</p>
  <div slot="footer">
    <button>Action</button>
  </div>
</hy-card>
html
<!-- Clickable card (full keyboard support in all environments) -->
<hy-card clickable header-title="Clickable Card">
  <p>Click anywhere on this card to trigger an action.</p>
</hy-card>
html
<!-- Navigation card: a stretched anchor covers the card, so middle-click,
     Ctrl+click, and the screen-reader links list work natively. The anchor's
     accessible name comes from header-title or body-title. -->
<hy-card href="/products/42" body-title="Product 42">
  <p>Open the product page.</p>
</hy-card>
javascript
<!-- Direct usage in SPA/Storybook (no SSR wrapper needed) -->
<script type="module">
  import '@whitespaceux/harmony/card';
</script>
<hy-card header-title="SPA Card" body-title="Works perfectly">
  <p>No SSR required - works in any JavaScript environment!</p>
</hy-card>
html
<!-- Mixed content approach -->
<hy-card header-title="Product Card" body-subtitle="In Stock">
  <img slot="image" src="product.jpg" alt="Product image" />
  <p>Detailed product description here.</p>
  <div slot="footer">
    <button>Add to Cart</button>
    <button>View Details</button>
  </div>
</hy-card>
html
<!-- Header-only card (no borders applied automatically) -->
<hy-card header-title="Simple Card" header-subtitle="Just header content"> </hy-card>

API

Properties

PropertyAttributeTypeDefaultDescription
clickableclickablebooleanfalseMakes the entire card clickable and applies appropriate styling with keyboard support (Enter/Space keys).
hrefhrefstring | undefinedRenders the card as a navigation link: a stretched anchor covers the card, so middle-click / Ctrl+click / "open in new tab" and the screen-reader links list all work natively. The anchor's accessible name comes from headerTitle or bodyTitle — set one of them (or slot your own anchor). When href is set, clickable's button semantics and the card-click event are suppressed — navigation is the activation. Avoid combining with other interactive content inside the card (the anchor covers it).
targettargetstring | undefinedAnchor target — only meaningful when href is set. _blank auto-adds rel="noreferrer noopener".
disableddisabledbooleanfalseDisables the card, preventing all interaction and applying disabled styling. Takes precedence over all other states.
elevatedelevatedbooleanfalseApplies a resting shadow to lift the card off the surface. Opt-in — flat by default.
emphasisemphasis'solid' | 'outlined' | 'tinted' | 'plain''solid'Visual chrome weight on the card surface.
orientationorientation'vertical' | 'horizontal''vertical'Layout orientation. vertical (default) stacks sections top-to-bottom. horizontal places the image alongside the content column (left or right depending on imagePosition and writing direction). Horizontal cards collapse back to vertical below a 480px container width via a container query, so consumers don't have to toggle this prop based on viewport size. Being an inline-size query container, a horizontal card fills its container's inline size (inline-size: 100%) instead of sizing to its content — in a flex/grid parent, constrain the parent (or the card) rather than expecting shrink-wrap.
imagePositionimage-position'start' | 'end''start'Where the image sits relative to the rest of the content. start (default) places the image at the top in vertical orientation and at the inline-start (left in LTR, right in RTL) in horizontal orientation. end mirrors it — image at the bottom vertically, at the inline-end horizontally. Symmetric across orientations; image radii and container-query collapse follow the same logical rules.
hasHeaderhas-headerbooleanfalseSSR optimization hint indicating header slot has content.
hasImagehas-imagebooleanfalseSSR optimization hint indicating image slot has content.
hasBodyhas-bodybooleanfalseSSR optimization hint indicating body slot has content.
hasFooterhas-footerbooleanfalseSSR optimization hint indicating footer slot has content.
headerTitleheader-titlestring''The primary title text displayed in the card header. Ignored when the header slot is used.
headerSubtitleheader-subtitlestring''Secondary descriptive text displayed below the header title. Ignored when the header slot is used.
bodyTitlebody-titlestring''The title text displayed in the card body. Rendered above slot content when both are present.
bodySubtitlebody-subtitlestring''Secondary descriptive text displayed below the body title. Rendered above slot content when both are present.

Events

EventDetailDescription
card-clickFired when a clickable card is activated via mouse or keyboard. Not fired in href mode — navigation is the activation.

Slots

SlotDescription
defaultThe main body content of the card. When present, property-based body content (body-title, body-subtitle) is still rendered above slot content.
headerContent to display in the card header section. Completely overrides all header properties when present.
imageImage content to display at the top of the card. Uses line-height: 0 container to eliminate baseline spacing around images.
footerContent to display in the card footer section. Seamless by default; brands can opt into a top-border divider via --hy-card-section-divider.

CSS Parts

PartDescription
baseThe card's base wrapper element
headerThe header section container
imageThe image section container with line-height: 0 to prevent spacing issues
bodyThe main body content container
footerThe footer section container
linkThe stretched anchor rendered in href mode (covers the whole card)

CSS Custom Properties

PropertyDescription
--hy-card-shadow-hoverBox shadow on hover/focus for clickable cards
--hy-card-background-elevation-restSurface fill when the card has [elevated]. Default: --hy-background-surface-elevation-4 (same as surface-base in light mode; lifts to a tonally-raised gray in dark mode so the card stays visible on OLED-true-black surfaces where the cast shadow alone can't darken). Set this to override per-card.
--hy-card-strokeBorder width
--hy-card-radius-top-startTop-start corner radius override. Defaults to --hy-radius-container-md-top-start; cascades through --hy-card-radius so a single radius override still shapes all four corners.
--hy-card-radius-top-endTop-end corner radius override. See top-start.
--hy-card-radius-bottom-startBottom-start corner radius override. See top-start.
--hy-card-radius-bottom-endBottom-end corner radius override. See top-start.
--hy-card-image-widthImage column width in horizontal orientation. Default: minmax(120px, 40%)
--hy-card-section-dividerBorder shorthand between adjacent visible sections in vertical orientation. Default: 0 (seamless, matches Material/shadcn/Carbon). Set to var(--hy-stroke-sm) solid var(--hy-border-subtle-rest) for Ant/Bootstrap-style dashboard cards. Horizontal cards stay seamless regardless.
--hy-card-hover-durationTransition duration for hover and focus state feedback
--hy-card-hover-easingTransition easing for hover and focus state feedback

Built with Lit. Documented with VitePress.