Skip to content

LayrzCard

Kenny Mochizuki Escalona edited this page Aug 21, 2026 · 5 revisions

LayrzCard

A Material-free card widget providing an elevated surface container with optional interactive behavior, supporting five discrete elevation levels and customizable background color.

Metadata
Domain: Display
Phase: M2 (Core primitives)
Primitive: Hand-rolled (Container + FocusableActionDetector + MouseRegion + Listener + GestureDetector + AnimatedContainer)
Status: Confirmed scope.


Overview

LayrzCard is a simple, elevated surface container that holds child content. It supports five discrete elevation levels (1–5), an optional background color, and optional interactive behavior via onTap.

Design Principles

  • No outer margin — inter-child spacing is owned by LayrzRow and LayrzConstrainedView through their spacing parameter, so a card margin would double-count. Place the card inside a spacing container if needed.
  • Fixed padding and radius — padding is fixed at 16 logical pixels on all sides; radius is fixed at 12 logical pixels. Neither is exposed as a parameter, preventing arbitrary content shrinking and promoting consistent spacing.
  • Elevation as discrete levels — five specific shadow ramps (1–5) for visual hierarchy. Chosen at construction time; elevation does not change based on state.
  • Optional interactivity — when onTap is non-null, the card gains interactive feedback: cursor change, shadow elevation on hover/focus, shadow reduction on press, and keyboard support. When onTap is null, the card is inert.
  • Geometry constant during interaction — per decision D15, hover and press states vary shadow and colour only; size, padding, and radius are unchanged, preventing flicker and reflow.

API Structure

Core Constructor

class LayrzCard extends StatefulWidget {
  /// The widget displayed inside the card.
  final Widget child;

  /// The elevation level of the card (1–5).
  ///
  /// Selects a discrete shadow level from the elevation ramp. Higher values
  /// produce a larger drop shadow. Defaults to 1.
  final int elevation;

  /// The background fill color of the card.
  ///
  /// When null, defaults to the `sf1` token color (page canvas background).
  /// When provided, overrides the token color entirely.
  final Color? backgroundColor;

  /// Called when the user taps the card.
  ///
  /// When null, the card is not interactive (no cursor change, no hover/press
  /// feedback, no keyboard activation).
  final VoidCallback? onTap;

  /// Creates a new [LayrzCard].
  ///
  /// The [elevation] must be between 1 and 5 inclusive. [child] is required.
  /// When [backgroundColor] is null, the card defaults to the surface token color.
  /// When [onTap] is null, the card is not interactive.
  const LayrzCard({
    super.key,
    required this.child,
    this.elevation = 1,
    this.backgroundColor,
    this.onTap,
  }) : assert(
         elevation >= 1 && elevation <= 5,
         'elevation must be between 1 and 5, got $elevation',
       );
}

Interaction Behavior

Non-Interactive (onTap: null)

When onTap is null, the card is inert:

  • Default cursor (no pointer change)
  • No hover response
  • No press response
  • No keyboard focus
  • Not announced as a button to assistive technology
  • Fixed shadow at the specified elevation

Interactive (onTap: non-null)

When onTap is non-null, the card is interactive:

State Behavior
Default Static shadow at the specified elevation
Hovered Shadow steps UP one level (clamped at elevation 5)
Focused Shadow steps UP one level (clamped at elevation 5); keyboard-visible focus indicator
Pressed Shadow steps DOWN one level (clamped at elevation 1)
Disabled (Never occurs; only onTap: null disables)
  • Cursor becomes SystemMouseCursors.click (pointer)
  • Geometry (size, padding, radius) remains constant across all states
  • Focusable by Tab navigation
  • Activatable by Enter or Space keys
  • Announced to assistive technology as an interactive button

Elevation Levels

The card uses the discrete elevation ramp from LayrzTokens.shadow. Each level is a preset shadow configuration:

Elevation Usage Shadow Appearance
1 Subtle elevation, cards at rest Smallest drop shadow (default)
2 Standard elevation, popovers Medium drop shadow
3 Medium elevation, overlays Medium-large drop shadow
4 High elevation, modal dialogs Large drop shadow
5 Highest elevation, top-most overlays Largest drop shadow

Elevation is asserted at construction time to be an integer between 1 and 5 inclusive. Values outside this range trigger an assertion error during development.


Styling

Background Color

Condition Color
backgroundColor == null tokens.colors.sf1 (page canvas background)
backgroundColor != null The provided color (overrides the token entirely)

Padding

Fixed at tokens.spacing.sp3 (16 logical pixels) on all sides. This matches the design system's standard spacing and is not configurable.

Border Radius

Fixed at tokens.radius.r3 (16 logical pixels). This produces gently rounded corners suitable for card containers.


Material-Free Construction

LayrzCard is built entirely without Material or Cupertino imports, using only package:flutter/widgets.dart primitives:

  1. Container — base surface rendering
  2. FocusableActionDetector — focus handling and keyboard navigation
  3. MouseRegion — cursor and hover detection
  4. Listener — pointer down/up/cancel for press state tracking
  5. GestureDetector — tap gesture handling
  6. AnimatedContainer — smooth state transitions (shadow and colour changes)
  7. Semantics — accessibility annotations (button role when interactive)

Four-State Interaction Model

The card implements a four-state model with a shared elevation ladder:

State Elevation Offset Meaning
Default 0 (use card elevation) Idle, no interaction
Hovered / Focused +1 (clamped ≤ 5) Pointer over or keyboard focus
Pressed −1 (clamped ≥ 1) Pointer/finger held down

Keyboard focus renders identically to mouse hover, satisfying WCAG 2.4.7 (Focus Visible, AA).


Usage Examples

Basic Non-Interactive Card

LayrzCard(
  elevation: 1,
  child: Padding(
    padding: EdgeInsets.all(16),
    child: Text('Card content'),
  ),
)

Interactive Card with Custom Background

LayrzCard(
  elevation: 2,
  backgroundColor: context.tokens.colors.sf2,  // Nested surface (optional custom background)
  onTap: () => Navigator.push(...),
  child: Column(
    children: [
      Text('Tap me'),
      SizedBox(height: 8),
      Text('Navigate on tap'),
    ],
  ),
)

Card Inside Spacing Container

LayrzRow(
  spacing: 16,  // Inter-card spacing
  children: [
    LayrzCol(
      xs: 12,
      md: 6,
      child: LayrzCard(
        elevation: 1,
        onTap: () => _navigateToDetail(),
        child: SizedBox(
          height: 150,
          child: Text('Card 1'),
        ),
      ),
    ),
    LayrzCol(
      xs: 12,
      md: 6,
      child: LayrzCard(
        elevation: 1,
        onTap: () => _navigateToOther(),
        child: SizedBox(
          height: 150,
          child: Text('Card 2'),
        ),
      ),
    ),
  ],
)

Elevated Card in a Dialog

LayrzCard(
  elevation: 4,  // Higher elevation in a modal context
  backgroundColor: context.tokens.colors.sf1,  // Default canvas background (or omit for default)
  onTap: () => _selectItem(),
  child: ListTile(
    title: Text('Item'),
    subtitle: Text('Tap to select'),
  ),
)

Dependencies

  • M1 Theme System (LayrzTheme, LayrzThemeData, LayrzTokens) — colors, spacing, and shadow tokens
  • M1 State resolution (WidgetState, WidgetStatesController) — for interaction state handling
  • Flutter primitives (Container, FocusableActionDetector, MouseRegion, GestureDetector, AnimatedContainer) — from package:flutter/widgets.dart

Accessibility

  • Non-interactive cards are not announced to assistive technology; they are opaque containers.
  • Interactive cards are announced as enabled buttons and are keyboard-focusable (Tab navigation).
  • Keyboard activation is via Enter or Space keys; the onTap callback is invoked identically to a tap.
  • Focus indicator is conveyed through shadow elevation change, not a visible outline (following decision D15).

Last updated: 2026-08-16
Related documents: Milestone 2, Design Tokens, Architecture, Decisions (D15), Roadmap

Clone this wiki locally