-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
-
No outer margin — inter-child spacing is owned by
LayrzRowandLayrzConstrainedViewthrough theirspacingparameter, 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
onTapis non-null, the card gains interactive feedback: cursor change, shadow elevation on hover/focus, shadow reduction on press, and keyboard support. WhenonTapis 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.
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',
);
}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
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
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.
| Condition | Color |
|---|---|
backgroundColor == null |
tokens.colors.sf1 (page canvas background) |
backgroundColor != null |
The provided color (overrides the token entirely) |
Fixed at tokens.spacing.sp3 (16 logical pixels) on all sides. This matches the design system's standard spacing and is not configurable.
Fixed at tokens.radius.r3 (16 logical pixels). This produces gently rounded corners suitable for card containers.
LayrzCard is built entirely without Material or Cupertino imports, using only package:flutter/widgets.dart primitives:
- Container — base surface rendering
- FocusableActionDetector — focus handling and keyboard navigation
- MouseRegion — cursor and hover detection
- Listener — pointer down/up/cancel for press state tracking
- GestureDetector — tap gesture handling
- AnimatedContainer — smooth state transitions (shadow and colour changes)
- Semantics — accessibility annotations (button role when interactive)
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).
LayrzCard(
elevation: 1,
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Card content'),
),
)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'),
],
),
)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'),
),
),
),
],
)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'),
),
)-
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) — frompackage:flutter/widgets.dart
- 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
onTapcallback 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
Made with ❤️ by Golden M, Inc.
- LayrzTextInput
- LayrzSelectableAction
- LayrzSelectionToolbar
- LayrzTextSelectionControls
- LayrzSelectionMagnifier
- LayrzSelectionHandlePainter
- LayrzTextAreaInput
- LayrzComboBoxInput
- LayrzNumberInput
- LayrzPasswordInput
- LayrzCheckboxInput
- LayrzRadioInput
- LayrzSelectInput
- LayrzMultiSelectInput
- LayrzSearchInput
- LayrzDualListInput
- LayrzDurationInput