Skip to content

LayrzAlert

Kenny Mochizuki Escalona edited this page Aug 20, 2026 · 7 revisions

LayrzAlert

A Material-free inline status callout that communicates information, success, warnings, or errors with semantic color, icons, and flexible visual styles.

Metadata
Mirrors: ThemedAlert (layrz_theme)
Phase: M2 (Core primitives)
Domain: Feedback
Primitive: Hand-rolled (Container + BoxDecoration + Row + Column + Icon)
Status: Confirmed scope.


Overview

LayrzAlert is an inline status callout that displays a semantic message via color, icon, title text, and body text. It supports multiple visual styles and semantic types to fit different contexts — from informational hints to critical error callouts. The alert can be either non-interactive (default, read-only) or interactive (with optional tap handling and hover/press feedback).

Design Principles

  • Semantic type mapping — the type parameter maps to a semantic color (info, success, warning, danger, context, or custom), automatically selecting both the icon and color without explicit parameters.
  • Fixed content structuretitle and description are both required; there are no optional variants. This enforces a consistent information hierarchy and prevents incomplete callouts.
  • Two visual styleslayrz (default, tonal) and filledIcon (high-emphasis, solid), both using split-panel layout with colored left panel and surface right panel.
  • Responsive layout — no fixed heights on text-bearing elements, supporting WCAG 1.4.4 (Text Scaling, AAA) — text scales with the system font size without causing layout issues.
  • Optional interactivity — alerts are read-only by default (onTap: null). When onTap is provided, the alert becomes interactive with hover, press, and focus feedback. A containing view manages the alert's lifecycle.
  • Geometry consistency — per decision D15, only color and shadow change; size, padding, and radius remain constant across all interaction states. The lift during hover/focus is paint-only (via transform), never affecting layout or hit-testing.

API Structure

Core Constructor

class LayrzAlert extends StatelessWidget {
  /// The semantic type of the alert.
  ///
  /// Determines the default icon and colour. Defaults to [LayrzAlertType.info].
  /// If [type] is [LayrzAlertType.custom], the [color] and [icon] parameters
  /// control appearance; otherwise they are ignored.
  final LayrzAlertType type;

  /// The title text of the alert.
  ///
  /// Required. Displays as bold title text. Scales with system text scale factor.
  final String title;

  /// The description text of the alert.
  ///
  /// Required. Displays as body text. Limited to [maxLines] lines before ellipsis.
  /// Scales with system text scale factor.
  final String description;

  /// The maximum number of lines for [description] text.
  ///
  /// If the description exceeds this many lines, it is truncated with an ellipsis.
  /// Defaults to 3.
  final int maxLines;

  /// The visual style of the alert.
  ///
  /// Determines background, border, icon chip, and text colours.
  /// Defaults to [LayrzAlertStyle.layrz].
  final LayrzAlertStyle style;

  /// The custom colour of the alert, used only when [type] is [LayrzAlertType.custom].
  ///
  /// If [type] is not custom, this parameter is ignored.
  /// If null and [type] is custom, defaults to [LayrzTokens.colors.primary].
  final Color? color;

  /// The custom icon glyph, used only when [type] is [LayrzAlertType.custom].
  ///
  /// If [type] is not custom, this parameter is ignored.
  /// If null and [type] is custom, defaults to [MdiIcons.informationBoxOutline].
  final IconData? icon;

  /// The size of the icon glyph.
  ///
  /// If null, defaults to [kLayrzAlertFilledIconSize] for [LayrzAlertStyle.filledIcon],
  /// or [kLayrzAlertIconSize] for all other styles.
  final double? iconSize;

  /// Called when the user taps the alert.
  ///
  /// When null (the default), the alert is not interactive: no cursor change,
  /// no hover/press feedback, not focusable, and not announced as a button to
  /// assistive technology. The alert renders exactly as it did before, with no
  /// state changes during interaction.
  ///
  /// When non-null, the alert becomes interactive: on hover or focus, the surface
  /// lifts by [kLayrzAlertHoverLift] and the shadow steps up one level; on press,
  /// it settles back down. The lift is paint-only (via transform), so geometry
  /// remains constant. The alert is focusable by Tab and activatable by Enter or
  /// Space, and announces as a button to assistive technology.
  final VoidCallback? onTap;

  /// Creates a [LayrzAlert].
  const LayrzAlert({
    super.key,
    this.type = LayrzAlertType.info,
    required this.title,
    required this.description,
    this.maxLines = 3,
    this.style = LayrzAlertStyle.layrz,
    this.color,
    this.icon,
    this.iconSize,
    this.onTap,
  });
}

LayrzAlertType

Semantic type classification that determines the alert's accent color and icon:

enum LayrzAlertType {
  /// Informational semantic type — use `tokens.colors.info` for neutral messages.
  ///
  /// Associated icon: [MdiIcons.informationBoxOutline].
  info,

  /// Success semantic type — use `tokens.colors.success` for positive/affirmative messages.
  ///
  /// Associated icon: [MdiIcons.checkboxOutline].
  success,

  /// Warning semantic type — use `tokens.colors.warning` for cautionary messages.
  ///
  /// Associated icon: [MdiIcons.alertBoxOutline].
  warning,

  /// Danger semantic type — use `tokens.colors.danger` for destructive/critical messages.
  ///
  /// Associated icon: [MdiIcons.closeCircleOutline].
  danger,

  /// Contextual semantic type — use `tokens.colors.contextual` for context-dependent messages.
  ///
  /// Associated icon: [MdiIcons.dotsSquare].
  context,

  /// Custom type — use explicit `icon` and `color` values from the [LayrzAlert] constructor.
  ///
  /// The `color` and `icon` parameters are only honoured when `type == custom`.
  /// Defaults to `tokens.colors.primary` and [MdiIcons.informationBoxOutline] if
  /// `color` and `icon` are null, respectively.
  custom,
}
Type Token Color Icon Use Case
info tokens.colors.info (blue) informationBoxOutline Neutral, informational messages
success tokens.colors.success (green) checkboxOutline Confirmation, successful operations
warning tokens.colors.warning (orange) alertBoxOutline Cautionary, attention-needed messages
danger tokens.colors.danger (red) closeCircleOutline Critical, destructive, error messages
context tokens.colors.contextual dotsSquare Context-dependent, custom application logic
custom Provided via color parameter Provided via icon parameter Any other semantic meaning

LayrzAlertStyle

Visual style classification that determines appearance. All styles render in a split-panel layout:

enum LayrzAlertStyle {
  /// Default Layrz style with tonal accent on left, neutral surface on right, severity-tinted border.
  layrz,

  /// Solid accent on left panel, neutral surface on right, solid accent border.
  filledIcon,
}

Style Rendering Guide

All styles use a split-panel layout. The table below shows how each style handles color rendering:

Style Left Panel Right Panel Border Title Color Body Color Icon Color
layrz Tonal accent (20% opacity) surface Solid accent fg1 fg2 Accent
filledIcon Solid accent surface Solid accent fg1 fg2 Contrast

Tonal opacity: The tonal accent is computed as accent.withOpacityValue(tokens.colors.tonalOpacity).
Contrast color: For solid-colored backgrounds, the text and icon use contrastColor to ensure readability (white or black depending on accent brightness).

.layrz — The Default Style

The .layrz style is the default and most versatile. All alerts use a split-panel layout that combines clarity with visual softness:

  • Left panel: tonal accent background (at tokens.colors.tonalOpacity, typically 20% opacity) with the semantic icon centered
  • Right panel: neutral surface background with title and description
  • Border: solid accent at tokens.border.base, defining the alert's perimeter
  • Icon: accent color at full strength for semantic visibility
  • Text: neutral foreground colors (fg1 for title, fg2 for body), ensuring strong readability

This style creates visual hierarchy through the tonal left panel while maintaining readability via the neutral right panel. It works universally on neutral backgrounds and is the safe choice for inline alerts in forms, cards, and dialogs. The tonal left panel softens the accent emphasis compared to the more aggressive filledIcon style, making .layrz suitable for general-purpose status messages.


Layout: Split-Panel

All alert styles use a split-panel layout:

A row split into two vertical panels with equal heights:

  • Left panel: colored background (tonal for layrz, solid for filledIcon) with a centered icon (kLayrzAlertFilledIconSize = 25.0 logical pixels)
  • Right panel: neutral surface background with title and description
  • Border: drawn via foregroundDecoration over both panels to avoid antialiasing seams

Both panels use tokens.spacing.sp3 padding (16 logical pixels).

┌────────────┬────────────────────────┐
│  ⊕ accent  │ Title Bold             │
│  (tonal or │ Description text...    │
│   solid)   │                        │
└────────────┴────────────────────────┘

Content Requirements

Both title and description are required. Providing neither or only one triggers a constructor assertion error.

  • title (String, required): Bold, single-line header text. Displays in tokens.typography.title with fontWeight.bold.
  • description (String, required): Body text. Displays in tokens.typography.body. Limited to maxLines (default 3) before ellipsis truncation.

This strict 1:1 structure is a deliberate port of ThemedAlert — every alert has a title and body, preventing incomplete or ambiguous callouts.


Interactive Alerts

LayrzAlert supports optional interactivity via the onTap callback. Non-interactive alerts (the default, with onTap: null) remain read-only with no state changes. Interactive alerts (with onTap: non-null) respond to hover, press, and focus with visual feedback.

Interaction Model

Non-interactive (onTap: null):

  • No cursor change; uses the default pointer cursor.
  • No hover or press visual feedback.
  • Not focusable via keyboard Tab navigation.
  • Announces as a container to assistive technology, not a button.
  • Rendering is identical to the pre-interactive alert design.

Interactive (onTap: non-null):

  • Cursor: Changes to SystemMouseCursors.click on hover.
  • Hover: Surface lifts by kLayrzAlertHoverLift (4.0 logical pixels) and shadow steps up one level (via AnimatedContainer). Animation uses tokens.motion.dHover and tokens.motion.easingEnter.
  • Focus: Renders identically to hover — surface lifts and shadow steps up. Reachable via Tab navigation.
  • Press without prior hover (touch device): Surface lifts by kLayrzAlertHoverLift and shadow steps up to elevation 2 — the same treatment as hover. Touch devices cannot hover, so the alert gives immediate visual feedback on press to indicate the action is recognized.
  • Press with prior hover (desktop): Surface settles back down to its resting position; shadow steps down to elevation 1.
  • Keyboard Activation: Enter or Space key activates the alert (calls onTap). Activation is automatic inside LayrzApp via WidgetStateIntent / ActivateIntent binding; no explicit shortcut configuration is needed.
  • Accessibility: Announces as an interactive button with enabled state.

Lift Mechanism: Paint-Only Transform

The hover/focus lift is implemented as a paint-only transform (Matrix4.translationValues(0, -_currentLift, 0)), not a layout change. This means:

  • The alert's bounding box and hit-test region remain constant across all interaction states.
  • No reflow or repositioning of surrounding widgets.
  • The pointer is not carried away by a moving surface, preventing hover oscillation loops (the same failure pattern LayrzTooltip encountered and resolved via LayrzTooltip's anchor-always-visible guarantee).

This design satisfies the spirit of decision D15 (geometry consistency during interaction) while adding lift feedback. See Decisions (D15) for the broader context, and the amendment recorded on 2026-08-16 permitting paint-only transforms under controlled conditions.

Realistic Example: Dismissible Alert

class DismissibleAlertExample extends StatefulWidget {
  @override
  State<DismissibleAlertExample> createState() => _DismissibleAlertExampleState();
}

class _DismissibleAlertExampleState extends State<DismissibleAlertExample> {
  bool showAlert = true;

  @override
  Widget build(BuildContext context) {
    if (!showAlert) {
      return SizedBox.shrink();
    }

    return LayrzAlert(
      type: LayrzAlertType.warning,
      title: 'Important Notice',
      description: 'Tap this alert to dismiss it.',
      onTap: () {
        setState(() => showAlert = false);
      },
    );
  }
}

In this example, the alert becomes interactive and provides visual feedback (lift and shadow) on hover/focus. When tapped or activated via keyboard, it calls the onTap callback, which dismisses the alert. Non-interactive alerts are the default and remain the common case for informational callouts that do not require user action.


Sizing and Spacing

Property Value
Container padding tokens.spacing.sp3 (16 logical pixels all sides)
Border radius tokens.radius.r3 (16 logical pixels)
Icon glyph size kLayrzAlertFilledIconSize = 25.0 logical pixels
Gap between panels Integrated into split-panel layout
Gap between title and description tokens.spacing.sp1 (4 logical pixels)
Max description lines maxLines parameter (default 3)

No fixed heights on text: Title and description elements have no explicit height constraints, allowing them to scale with the system font size (WCAG 1.4.4, AAA).


Implementation Notes

Split-Panel Border Painting

For the split-panel styles (.layrz and .filledIcon), the border is painted via foregroundDecoration over the clipped content, rather than in a surrounding BoxDecoration. This prevents visible antialiasing seams at the boundary between the left and right panels. The outer ClipRRect applies the corner radius, and the border is painted on top at the same radius.

Interactive Alerts and Translucent Fills

When an alert is interactive (onTap is non-null), its translucent background colours are flattened to opaque equivalents using the flattenOn extension method. This is necessary because BoxDecoration paints its shadow behind the decorated box, and any translucent fill lets the shadow show through as a smudge inside the box instead of beneath it. By compositing translucent colours onto the surface they will be painted over, the result is pixel-identical but fully opaque — so shadows render correctly.

Inert alerts (the default, with onTap: null) preserve true translucency and are unchanged.


LayrzAlertIcon

A standalone circular icon chip widget that renders a semantic alert icon. It is NOT directly used by LayrzAlert — instead, LayrzAlert builds its own icon chips internally. This widget is provided as a reusable building block for other contexts.

API

class LayrzAlertIcon extends StatelessWidget {
  /// The semantic type of the alert icon.
  ///
  /// Determines the default icon and colour if [icon] or [color] are not provided.
  /// Defaults to [LayrzAlertType.info].
  final LayrzAlertType type;

  /// The outer size of the icon chip container (width and height in logical pixels).
  ///
  /// Defaults to [kLayrzAlertIconWidgetSize].
  final double size;

  /// The size of the icon glyph inside the chip.
  ///
  /// Defaults to [kLayrzAlertIconWidgetIconSize].
  final double iconSize;

  /// Padding between the chip edge and the icon glyph.
  final EdgeInsetsGeometry? padding;

  /// The colour of the icon chip, used only when [type] is [LayrzAlertType.custom].
  ///
  /// If null and [type] is [LayrzAlertType.custom], defaults to
  /// [LayrzTokens.colors.primary]. Ignored for non-custom types.
  final Color? color;

  /// The icon glyph, used only when [type] is [LayrzAlertType.custom].
  ///
  /// If null and [type] is [LayrzAlertType.custom], defaults to
  /// [MdiIcons.informationBoxOutline]. Ignored for non-custom types.
  final IconData? icon;

  /// Creates a [LayrzAlertIcon].
  const LayrzAlertIcon({
    super.key,
    this.type = LayrzAlertType.info,
    this.size = kLayrzAlertIconWidgetSize,
    this.iconSize = kLayrzAlertIconWidgetIconSize,
    this.padding,
    this.color,
    this.icon,
  });
}

Standalone usage:

// Info icon chip
LayrzAlertIcon(
  type: LayrzAlertType.info,
  size: 40,
  iconSize: 24,
)

// Custom icon chip
LayrzAlertIcon(
  type: LayrzAlertType.custom,
  color: Colors.purple,
  icon: MdiIcons.heart,
)

Material-Free Construction

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

  1. Container — background and border rendering
  2. Row — horizontal layout for icon and text
  3. Column — vertical layout for title and description
  4. Icon — semantic icon rendering
  5. Text — text rendering with styling
  6. ClipRRect — corner radius clipping for filledIcon style
  7. IntrinsicHeight — height matching between panels in filledIcon style

Usage Examples

Basic Info Alert

LayrzAlert(
  type: LayrzAlertType.info,
  title: 'Information',
  description: 'This is an informational message for the user.',
)

Success Alert

LayrzAlert(
  type: LayrzAlertType.success,
  title: 'Operation Successful',
  description: 'Your changes have been saved.',
  style: LayrzAlertStyle.layrz, // Default; omit for the same effect
)

Warning Alert with Custom Max Lines

LayrzAlert(
  type: LayrzAlertType.warning,
  title: 'Warning',
  description: 'This action requires your confirmation. Please review carefully before proceeding.',
  maxLines: 4,
  style: LayrzAlertStyle.layrz,
)

Danger (Error) Alert with High Emphasis

LayrzAlert(
  type: LayrzAlertType.danger,
  title: 'Error',
  description: 'An unexpected error occurred. Please try again later.',
  style: LayrzAlertStyle.filledIcon, // High-emphasis solid accent
)

Context (Custom Semantic) Alert

LayrzAlert(
  type: LayrzAlertType.context,
  title: 'Context-Dependent',
  description: 'This alert\'s meaning depends on the surrounding application state.',
  style: LayrzAlertStyle.filledIcon,
)

Custom Type with Explicit Color and Icon

LayrzAlert(
  type: LayrzAlertType.custom,
  title: 'Custom Alert',
  description: 'Using a custom color and icon.',
  color: Colors.purple,
  icon: MdiIcons.heart,
)

Inside a Form or Container

Container(
  padding: EdgeInsets.all(16),
  child: Column(
    children: [
      LayrzAlert(
        type: LayrzAlertType.info,
        title: 'Form Instructions',
        description: 'Please fill in all required fields marked with an asterisk (*).',
      ),
      SizedBox(height: 24),
      // Form fields follow...
    ],
  ),
)

Interactive Alert with Action Handler

LayrzAlert(
  type: LayrzAlertType.success,
  title: 'Update Available',
  description: 'A new version is ready. Tap to install.',
  style: LayrzAlertStyle.layrz,
  onTap: () {
    // Handle the tap — e.g., initiate download/installation
    handleUpdateInstall();
  },
)

When onTap is provided, the alert lifts on hover and announces as a button. Non-interactive alerts (onTap: null) remain the default and recommended pattern for informational callouts that require no user action.


Styling by Context

Choose the alert style based on the context where it appears:

Context Recommended Style Rationale
Default / most contexts .layrz (default) Tonal left panel provides semantic emphasis without overwhelming; split-panel layout creates visual hierarchy and works universally
High-emphasis / critical message .filledIcon Solid accent left panel with icon commands stronger attention; use when the message requires immediate action

Dependencies

  • M1 Theme System (LayrzTheme, LayrzThemeData, LayrzTokens) — colors, typography, spacing, radius, and border tokens
  • Flutter primitives (Container, Row, Column, Icon, Text, ClipRRect) — from package:flutter/widgets.dart
  • flutter_material_design_icons (^3.1.0) — icon constants (bare IconData, not wrapped)

Accessibility

  • Semantics:
    • Non-interactive alerts (onTap: null) are wrapped in Semantics(container: true, label: '<title>. <description>'), announcing as a container.
    • Interactive alerts (onTap: non-null) are wrapped in Semantics(button: true, enabled: true, label: '<title>. <description>'), announcing as an enabled button.
  • Keyboard activation: Interactive alerts are reachable via Tab and activatable by Enter or Space. No explicit shortcut binding is needed; LayrzApp (via WidgetStateIntent routes) supplies the binding automatically.
  • Color not the sole cue: the icon is a non-colour severity cue, satisfying WCAG 1.4.1 (Use of Color, A).
  • Text scaling: no fixed heights on text elements support WCAG 1.4.4 (Text Scaling, AAA) — alerts remain usable at 2x system font size.
  • Contrast: title and body text colors are resolved with sufficient contrast against their backgrounds per WCAG 1.4.3 (Contrast, AA).
  • Focus visible: Interactive alerts show focus via shadow elevation (lifted state), satisfying WCAG 2.4.7 (Focus Visible, AA).

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

Clone this wiki locally