Skip to main content

Component structure

File structure

packages/zeta_flutter/lib/src/components/foo/
└── foo.dart

test/src/components/foo/
└── foo_test.dart

widgetbook/lib/src/components/
└── foo.widgetbook.dart

packages/zeta_flutter/lib/src/examples/components/
└── foo_example.dart

For components with sub-components, keep them in the same directory.

Skeleton

packages/zeta_flutter/lib/src/components/foo/foo.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../zeta_flutter.dart';

/// One-line description matching the Figma component name.
///
/// Optional Flutter-specific clarification.
///
/// Figma: https://www.figma.com/design/JesXQFLaPJLc1BdBM4sisI/...
/// Widgetbook: https://design.zebra.com/flutter/widgetbook/index.html#/?path=components/foo/zetafoo
class ZetaFoo extends ZetaStatelessWidget {
/// Creates a [ZetaFoo].
const ZetaFoo({
super.key,
super.rounded,
required this.label,
this.flavor = ZetaWidgetStatus.info,
this.size = ZetaWidgetSize.medium,
this.onTap,
});

/// The text label displayed by this component.
final String label;

/// The semantic color variant of this component.
final ZetaWidgetStatus flavor;

/// The size of this component.
final ZetaWidgetSize size;

/// Called when this component is tapped.
final VoidCallback? onTap;


Widget build(BuildContext context) {
final colors = Zeta.of(context).colors;
return GestureDetector(
onTap: onTap,
child: Text(label, style: ZetaTextStyles.bodyMedium),
);
}


void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(StringProperty('label', label))
..add(EnumProperty<ZetaWidgetStatus>('flavor', flavor))
..add(EnumProperty<ZetaWidgetSize>('size', size))
..add(ObjectFlagProperty<VoidCallback?>.has('onTap', onTap));
}
}

Naming

ThingConventionExample
Class namePascalCase, Zeta prefixZetaFoo
File namesnake_case matching classfoo.dart
Directory namesnake_case matching filenamefoo/
Parameter namecamelCaseleadingIcon, onTap

Required in every file

  • /// One-line description dart doc comment on the class (matching the Figma component name)
  • /// Figma: and /// Widgetbook: links in the class doc comment
  • /// Description dart doc on every public parameter
  • debugFillProperties override that adds every parameter
  • const constructor wherever possible

Exports and registration

Add an export line to packages/zeta_flutter/lib/src/zeta_components.dart. Keep entries alphabetical:

packages/zeta_flutter/lib/src/zeta_components.dart
// ...
export 'src/components/foo/foo.dart';
// ...

Only export the main component class. Internal helpers, private state classes, and sub-widgets that are not intended for direct consumer use must stay unexported.

Interactive elements

Always use the right interaction primitive

Never implement tap detection with raw GestureDetector when InkWell is appropriate, and never build focus handling from scratch. Flutter provides the correct primitives — use them.

Interaction neededWidget to use
Tap / long-press with ink splashInkWell (inside a Material)
Tap without ink (icon buttons)GestureDetector
Keyboard focus + navigationFocus or FocusableActionDetector
Form field participationFormField<T> with FormFieldState<T>
foo.dart
// ✅ Good — InkWell gives ink splash, keyboard activation, and semantics
InkWell(
onTap: onTap,
borderRadius: _borderRadius,
child: Padding(
padding: EdgeInsets.all(ZetaSpacing.medium),
child: Text(label),
),
)

// ❌ Bad — GestureDetector has no ink, no keyboard activation
GestureDetector(
onTap: onTap,
child: Text(label),
)

Wrap InkWell in a Material widget when there is no Material ancestor (e.g. inside a Card content area that clips overflow).


Mixins and composition

Use ZetaStatelessWidget for components with no internal state, and ZetaStatefulWidget for components that own mutable UI state. Prefer stateless — push state up to the parent when possible.

ClassWhat it providesWhen to use
ZetaStatelessWidgetrounded prop, Zeta.of(context) convenienceNo internal state
ZetaStatefulWidgetSame as above plus createState()Component owns mutable state
ZetaWidgetStatusinfo, positive, warning, negative enumSemantic colour variants
ZetaWidgetSizesmall, medium, large enumMultiple size variants

When computing visual properties inside build(), always derive colors from Zeta.of(context).colors. Never set a color independently of the token system:

foo.dart
// ✅ Good
final colors = Zeta.of(context).colors;
final bgColor = disabled ? colors.surfaceDisabled : colors.surfacePrimary;

// ❌ Bad — hardcodes a color, breaks theming
final bgColor = disabled ? const Color(0xFFCCCCCC) : const Color(0xFF0000FF);

Always dispose controllers and focus nodes in dispose().