HyCard
Usage examples on this page are written for Lit / plain HTML (
<hy-card>). The same component ships asHyCardin@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:
- The host framework detects slot presence at render time using its own slot API
- Passes boolean attributes (
has-header,has-image,has-body,has-footer) to the Lit component - CSS uses these attributes for immediate section visibility during initial render
- After hydration, HasSlotController confirms slot content and adds
.has-contentclasses
In CSR-only environments (SPAs, Storybook):
- All sections render to DOM (no
has-*attributes) - Sections are hidden by default via CSS (
display: none) firstUpdated()runs andhideSectionsWithoutContent()detects actual content- Adds
.has-contentclasses which trigger CSS visibility rules - 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-contentclass) - 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-onlyclass for hidden but accessible content - Reduced motion support respecting user preferences
Themeable via the @cssprop override hooks documented below.
Usage Patterns
Examples
<!-- 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><!-- 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><!-- 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><!-- 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><!-- 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><!-- 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><!-- Header-only card (no borders applied automatically) -->
<hy-card header-title="Simple Card" header-subtitle="Just header content"> </hy-card>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
clickable | clickable | boolean | false | Makes the entire card clickable and applies appropriate styling with keyboard support (Enter/Space keys). |
href | href | string | undefined | — | Renders 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). |
target | target | string | undefined | — | Anchor target — only meaningful when href is set. _blank auto-adds rel="noreferrer noopener". |
disabled | disabled | boolean | false | Disables the card, preventing all interaction and applying disabled styling. Takes precedence over all other states. |
elevated | elevated | boolean | false | Applies a resting shadow to lift the card off the surface. Opt-in — flat by default. |
emphasis | emphasis | 'solid' | 'outlined' | 'tinted' | 'plain' | 'solid' | Visual chrome weight on the card surface. |
orientation | orientation | '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. |
imagePosition | image-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. |
hasHeader | has-header | boolean | false | SSR optimization hint indicating header slot has content. |
hasImage | has-image | boolean | false | SSR optimization hint indicating image slot has content. |
hasBody | has-body | boolean | false | SSR optimization hint indicating body slot has content. |
hasFooter | has-footer | boolean | false | SSR optimization hint indicating footer slot has content. |
headerTitle | header-title | string | '' | The primary title text displayed in the card header. Ignored when the header slot is used. |
headerSubtitle | header-subtitle | string | '' | Secondary descriptive text displayed below the header title. Ignored when the header slot is used. |
bodyTitle | body-title | string | '' | The title text displayed in the card body. Rendered above slot content when both are present. |
bodySubtitle | body-subtitle | string | '' | Secondary descriptive text displayed below the body title. Rendered above slot content when both are present. |
Events
| Event | Detail | Description |
|---|---|---|
card-click | — | Fired when a clickable card is activated via mouse or keyboard. Not fired in href mode — navigation is the activation. |
Slots
| Slot | Description |
|---|---|
default | The main body content of the card. When present, property-based body content (body-title, body-subtitle) is still rendered above slot content. |
header | Content to display in the card header section. Completely overrides all header properties when present. |
image | Image content to display at the top of the card. Uses line-height: 0 container to eliminate baseline spacing around images. |
footer | Content 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
| Part | Description |
|---|---|
base | The card's base wrapper element |
header | The header section container |
image | The image section container with line-height: 0 to prevent spacing issues |
body | The main body content container |
footer | The footer section container |
link | The stretched anchor rendered in href mode (covers the whole card) |
CSS Custom Properties
| Property | Description |
|---|---|
--hy-card-shadow-hover | Box shadow on hover/focus for clickable cards |
--hy-card-background-elevation-rest | Surface 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-stroke | Border width |
--hy-card-radius-top-start | Top-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-end | Top-end corner radius override. See top-start. |
--hy-card-radius-bottom-start | Bottom-start corner radius override. See top-start. |
--hy-card-radius-bottom-end | Bottom-end corner radius override. See top-start. |
--hy-card-image-width | Image column width in horizontal orientation. Default: minmax(120px, 40%) |
--hy-card-section-divider | Border 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-duration | Transition duration for hover and focus state feedback |
--hy-card-hover-easing | Transition easing for hover and focus state feedback |