HyCard
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>Events
- card-click - Fired when a clickable card is activated via mouse or keyboard. Not fired in
hrefmode — navigation is the activation.
Slots
- 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.
Methods
renderHeader()
Render header section with slot and property-based content. Properties are only rendered when no header slot content is present.
renderImage()
Render image section with slot content only. No property-based content available for images.
renderBody()
Render body section with both property-based and slot content. Property-based content (title/subtitle) renders above slot content when both present.
renderFooter()
Render footer section with slot content only. No property-based content available for footers.
render()
Render method with unconditional section rendering for CSR/SSR compatibility.
All sections are always rendered to the DOM. Visibility is controlled entirely through CSS:
- SSR Mode: CSS uses
has-*attributes (set by the host framework) to show sections with content - CSR Mode: CSS uses
.has-contentclasses (added by hideSectionsWithoutContent) to show sections - Property Mode: CSS uses attribute selectors (e.g.,
[header-title]:not([header-title='']))
This approach ensures:
- Works in both SSR and CSR-only (Storybook, SPAs) environments
- No conditional rendering logic that could break when
has-*attributes are missing - CSS handles all visibility, including during SSR initial render
- Clean separation: JS detects content, CSS controls visibility
The slight trade-off of always rendering all sections is negligible compared to the robustness gained for multiple rendering environments.