Skip to Content
ComponentsPrimitiveCard

PrimitiveCard

A container widget with shadow, rounded corners, and padding - all rendered using CustomPaint and Canvas. Now supports custom shadow colors and tap interactions.

Overview

PrimitiveCard provides a Material-style card appearance using only primitive Flutter components. It demonstrates how shadows, rounded corners, and elevation can be implemented from scratch without relying on Flutter’s built-in Card widget. It also includes basic tap interaction with visual feedback (elevation reduction).

Primitives Used: CustomPaint, Canvas, RenderShiftedBox, GestureDetector

Primitive Card ShowcaseOpen in new tab ↗

Basic Usage

import 'package:primitive_ui/primitive_ui.dart'; PrimitiveCard( child: Text('Hello World'), onTap: () => print('Card tapped!'), )

API Reference

Constructor

PrimitiveCard({ Key? key, required Widget child, Color color = const Color(0xFFFFFFFF), double borderRadius = 8.0, double elevation = 2.0, Color shadowColor = const Color(0xFF000000), EdgeInsets padding = const EdgeInsets.all(16.0), VoidCallback? onTap, String? semanticsLabel, Duration duration = const Duration(milliseconds: 200), Curve curve = Curves.easeInOut, })

Parameters

ParameterTypeRequiredDefaultDescription
childWidgetThe widget to display inside the card
colorColorColor(0xFFFFFFFF)Background color of the card
borderRadiusdouble8.0Corner radius in logical pixels (must be >= 0)
elevationdouble2.0Shadow depth in logical pixels (must be >= 0)
shadowColorColorColor(0xFF000000)Color of the shadow (e.g., Colors.blue.shade800)
paddingEdgeInsetsEdgeInsets.all(16.0)Internal spacing around the child widget
onTapVoidCallback?nullCallback invoked when the card is tapped. Provides visual feedback.
semanticsLabelString?nullAccessibility label for screen readers
durationDurationDuration(milliseconds: 200)Duration for implicit style animations
curveCurveCurves.easeInOutCurve for implicit style animations

Examples

Different Elevations and Custom Shadow

PrimitiveCard( elevation: 2.0, child: Text('Low elevation'), )

Tap Interaction

PrimitiveCard( onTap: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Card tapped!')), ); }, elevation: 4.0, child: Center(child: Text('Tap me to see a snackbar!')), )

Custom Styling

PrimitiveCard( color: Color(0xFFE3F2FD), // Light blue borderRadius: 16.0, elevation: 8.0, padding: EdgeInsets.symmetric( horizontal: 24.0, vertical: 16.0, ), child: Column( children: [ Text( 'Card Title', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), SizedBox(height: 8), Text('Card content goes here'), ], ), )

Animated Styles

PrimitiveCard implicitly animates changes to its style properties.

PrimitiveCard( // Animate color and elevation when selected color: _isSelected ? Colors.blue.shade50 : Colors.white, elevation: _isSelected ? 8.0 : 2.0, duration: Duration(milliseconds: 300), curve: Curves.easeOutBack, onTap: () => setState(() => _isSelected = !_isSelected), child: Padding( padding: EdgeInsets.all(8.0), child: Text(_isSelected ? 'Selected' : 'Tap to Select'), ), )

Nested Layout

PrimitiveCard( elevation: 4.0, child: VStack( spacing: 12.0, children: [ Text('Settings', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold) ), PrimitiveToggleSwitch( value: notificationsEnabled, onChanged: (value) { setState(() => notificationsEnabled = value); }, ), ], ), )

Implementation Details

Rendering Pipeline

The PrimitiveCard uses a multi-layered rendering approach:

  1. Custom Painter (_CardPainter) handles the visual rendering (including customizable shadow color).
  2. Custom Layout (_RenderCardLayout) manages child positioning with padding.
  3. Canvas Operations draw the shadow and rounded rectangle.
  4. GestureDetector handles tap events, providing visual feedback by temporarily reducing elevation.

Implicit Animations

The component uses TweenAnimationBuilder to smoothly transition between styles.

  • A private _CardStyle class aggregates all animatable properties (color, elevation, borderRadius, shadowColor).
  • When properties change, the builder interpolates between the old and new _CardStyle.
  • This provides smooth visual transitions for all style changes without requiring an explicit AnimationController.

Shadow Calculation

Shadow opacity is calculated based on elevation using the Material Design scale, and now respects the shadowColor parameter:

final double shadowOpacity = (elevation / 24.0).clamp(0.0, 0.3); final Color effectiveShadowColor = shadowColor.withOpacity(shadowOpacity);

This ensures that higher elevations produce darker shadows, with a maximum opacity of 0.3, and the shadow’s hue is determined by shadowColor.

Layout System

The card uses a custom RenderShiftedBox implementation that:

  • Calculates child constraints by subtracting padding from available space
  • Positions the child with padding offset
  • Computes intrinsic dimensions including padding

Performance Optimization

The shouldRepaint() method only returns true when visual properties change:

bool shouldRepaint(_CardPainter oldDelegate) { return oldDelegate.color != color || oldDelegate.borderRadius != borderRadius || oldDelegate.elevation != elevation || oldDelegate.shadowColor != shadowColor; }

This prevents unnecessary repaints when the card state hasn’t changed.

Common Patterns

Card Grid Layout

GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, spacing: 16.0, ), itemBuilder: (context, index) { return PrimitiveCard( elevation: 2.0, onTap: () => print('Card $index tapped!'), child: Center(child: Text('Card $index')), ); }, )

Source Code

View the complete implementation in the GitHub repository .

Last updated on