Language rules
- Zeta Flutter
- Zeta Web
Use ZetaStatelessWidget or ZetaStatefulWidget
All components extend ZetaStatelessWidget or ZetaStatefulWidget — never plain StatelessWidget or StatefulWidget. These base classes add the rounded property and wire it to the inherited ZetaProvider, giving every component consistent shape behaviour for free.
// ✅ Good
class ZetaFoo extends ZetaStatelessWidget {
const ZetaFoo({super.key, super.rounded});
Widget build(BuildContext context) { ... }
}
// ❌ Bad — plain StatelessWidget, loses rounded and Zeta wiring
class ZetaFoo extends StatelessWidget {
const ZetaFoo({super.key});
Widget build(BuildContext context) { ... }
}
Use Dart — no dynamic
All files run under dart analyze with strict settings (implicit-casts: false, implicit-dynamic: false). dynamic defeats static analysis the same way any does in TypeScript — it silences errors that will become runtime crashes.
| Situation | Use instead |
|---|---|
| Unknown external data | Object? — forces a null-and-type check |
| Callback with unknown return | ValueChanged<T> |
| Unconstrained generic | <T extends Widget> |
// ❌ Bad
void handleTap(dynamic event) { ... }
final value = json['key']; // dynamic
// ✅ Good
void handleTap(TapUpDetails details) { ... }
final value = json['key'] as String?;
Type everything explicitly
Every constructor parameter, field, and method return type must be written out. Do not rely on var or inference at the API boundary:
// ✅ Good
class ZetaFoo extends ZetaStatelessWidget {
const ZetaFoo({
super.key,
super.rounded,
required this.label,
this.flavor = ZetaWidgetStatus.info,
this.onTap,
});
final String label;
final ZetaWidgetStatus flavor;
final VoidCallback? onTap;
}
// ❌ Bad — missing types
class ZetaFoo extends ZetaStatelessWidget {
const ZetaFoo({super.key, required this.label, this.onTap});
final label;
final onTap;
}
Organize imports
Follow Dart import ordering and use show to make the surface of an import explicit where it reduces ambiguity:
// ✅ Good — grouped: dart → flutter → package → relative
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:zeta_flutter/zeta_flutter.dart' show ZetaColors, ZetaSpacing;
import '../tokens/zeta_color_scheme.dart';
Avoid wildcard imports from internal packages — they expose private symbols and make dependency tracking harder.
Use Lit
All components are built with Lit. Do not introduce any other component framework, runtime, or abstraction layer. Vanilla HTMLElement subclasses are not desirable — they lack Lit's reactive property system, template engine, and style scoping.
// ✅ Good
import { LitElement, html } from "lit";
export class ZetaFoo extends LitElement { ... }
// ❌ Bad — plain HTMLElement
export class ZetaFoo extends HTMLElement { ... }
Use shadow DOM
Every component must render into a shadow root. Lit does this by default. Never override createRenderRoot() to return this:
// ❌ Bad — disables shadow DOM entirely
override createRenderRoot() {
return this;
}
For components that wrap a native focusable element, add delegatesFocus: true:
static override shadowRootOptions: ShadowRootInit = {
...LitElement.shadowRootOptions,
mode: "open",
delegatesFocus: true,
};
Use TypeScript — no any
All component files are .ts. The project runs with strict: true, which enforces strictNullChecks, noImplicitAny, noUnusedLocals, and noUnusedParameters. A PR that does not compile will not merge.
any defeats the type system. Use one of these instead:
| Situation | Use |
|---|---|
| Unknown external data | unknown — forces a type guard before use |
| Multiple known types | A union: string | number |
| Unknown DOM element | Element or HTMLElement |
| Unconstrained generic | <T extends LitElement> |
| Mixin super-class | Constructor<LitElement> |
// ❌ Bad
handleEvent(event: any) { ... }
const el = this.shadowRoot?.querySelector("input") as any;
// ✅ Good
handleEvent(event: Event) { ... }
const el = this.shadowRoot?.querySelector<HTMLInputElement>("input");
Type everything explicitly
Do not rely on inference for @property or @state decorators — types must appear in generated .d.ts files and the custom elements manifest:
// ✅ Good
@property({ type: String, reflect: true }) flavor: ButtonFlavor = "primary";
@property({ type: Boolean, reflect: true }) disabled: boolean = false;
@state() private _open: boolean = false;
// ❌ Bad — inferred, absent from declarations
@property({ type: String }) flavor = "primary";
Every method must have typed parameters and an explicit return type:
// ✅ Good
protected override updated(changedProperties: PropertyValues): void { ... }
private handleClick(event: MouseEvent): void { ... }
private getSelectedOption(): HTMLOptionElement | undefined { ... }
Organize imports
Follow ESM import order — external packages first, then local relative imports. Group and separate with a blank line. Use import type for type-only imports: they have zero runtime cost and make the import surface explicit:
// ✅ Good — grouped: lit → third-party → local components → types
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import type { ZetaIconName } from "@zebra-fed/zeta-icons";
import type { PropertyValues } from "lit";
import styles from "./foo.styles.js";
import "../icon/icon.js";
import type { FooFlavor } from "./foo.types.js";
// ❌ Bad — imports a type as a value, wrong order
import { ZetaIconName } from "@zebra-fed/zeta-icons";
import { html, LitElement } from "lit";
import styles from "./foo.styles.js";
Do not barrel-import from src/index.ts inside component files — it creates circular dependencies.