MachineInput

type MachineInput<Tag, Props> = Omit<
  Marko.Input<Tag>,
  Exclude<keyof Props, "id">
> &
  Omit<Props, "id"> & { id?: string };

Input type for a Marko component wrapping a Zag machine: the native tag’s
attributes, with Props members shadowing any native attribute of the same
name.

Props always wins on a name collision — there is no case where
intersecting a declared member with a native attribute is wanted, since it
only produces redundant, lossy, or unsatisfiable types. An attr-tag title
vs. the native string | false | null is unsatisfiable; @zag-js/checkbox’s
checked: boolean | "indeterminate" intersected with the native
checked: boolean silently collapses to boolean, dropping
"indeterminate" (lossy) — shadowing fixes that regression.

The only other adjustment is id: Zag’s CommonProperties requires it, but
the <zag> tag generates a stable one automatically — so
consumers may omit it (and may still override it). id stays string | undefined even when Props declares its own id member.

Type parameters

Parameter Description
Tag The native tag name whose attributes the component forwards (e.g. "input", "div").
Props The machine module’s exported Props type (e.g. switchMachine.Props), plus any component-declared extras folded in via intersection (e.g. an <@title> attr tag): MachineInput<Tag, Props & Extras>. Every member of this combined type shadows the same-named native attribute, if any.

Example

import * as switchMachine from "@zag-js/switch";
import type { MachineInput } from "marko-zag";

export type Input = MachineInput<"input", switchMachine.Props> & {
  // Marko bind-shorthand sugar (`checked:=state.on`):
  checkedChange?: (checked: boolean) => void;
};

Shadowing a native attribute

A component’s own declared member sometimes shares a name with a native
attribute — e.g. a <@title> attr tag on a div, which collides with the
native title: AttrString attribute. Fold the colliding member into
Props via intersection so it shadows the native attribute’s type instead
of intersecting with it:

import * as dialogMachine from "@zag-js/dialog";
import type { MachineInput } from "marko-zag";

type Extras = { title?: Marko.AttrTag<{ content: Marko.Body }> };

export type Input = MachineInput<"div", dialogMachine.Props & Extras> &
  Extras;

Don’t forward a shadowed prop to the element

Shadowing widens the component’s public input, which is the point — but a
widened prop is no longer assignable to the native attribute it shadows, so
it must not be spread straight onto the rendered element.

checked is the worked example: Input["checked"] is correctly
boolean | "indeterminate", while the native <input>'s checked is
AttrBoolean. Spreading the leftover props from splitProps onto the
element therefore fails to type-check — correctly, since
checked="indeterminate" is not a valid HTML attribute value.

Such props are machine-owned state. Let the machine’s own get*Props()
supply the element’s value and drop the incoming prop from whatever leftover
object you spread:

// checkbox: api().getHiddenInputProps() already emits `checked`,
// so the incoming `checked` prop is dropped rather than forwarded.
<const/hiddenInputProps=(): Marko.NativeTags["input"]["input"] => ({
  ...stripOwnProps(splitProps(input)[1], "class", "checked"),
  ...api().getHiddenInputProps(),
})>

The same applies to any Zag prop whose type is wider than, or a different
shape from, the native attribute of that name. @zag-js/slider declares
"aria-label"?: string[] — one label per thumb — against the native
aria-label?: AttrString, so a consumer passes an array
(aria-label=["Volume"]), and the component reads it from
api().getThumbProps({ index }) rather than forwarding it to the root
element.

Remarks

Controlled-prop semantics follow Zag v1: passing a controlled prop (e.g.
open=) without wiring its change handler pins the machine to that
value — the machine reports changes through onOpenChange and expects the
owner to write the new value back. Use the default* props (defaultOpen,
defaultValue, …) for the uncontrolled, initial-value path. See
Landmines & Gotchas.