defineElement
defineElement declares a Zeus Web Component. The same source is analyzed into a component manifest, Web Component output, React/Vue wrappers, and type declarations.
Signature
defineElement<TProps, THost, TEmits>(
tag: string,
options: DefineElementOptions<TProps, TEmits>,
setup: (
props: Readonly<TProps>,
context: DefineElementContext<THost, TEmits>,
) => JSX.Element,
): CustomElementConstructorMinimal Input Example
import { Host, Slot, defineElement, event, prop } from '@zeus-js/zeus'
import type { DefineElementContext, EventDefinition } from '@zeus-js/zeus'
export interface InputProps {
value?: string
placeholder?: string
disabled?: boolean
invalid?: boolean
}
type InputEmits = {
valueChange: EventDefinition<{ value: string; nativeEvent: Event }>
}
type InputHost = HTMLElement & {
value?: string
focus(): void
}
function setup(
props: InputProps,
ctx: DefineElementContext<InputHost, InputEmits>,
) {
let control!: HTMLInputElement
ctx.expose({
focus() {
control.focus()
},
})
return (
<Host data-invalid={() => (props.invalid ? '' : undefined)}>
<span part="prefix">
<Slot name="prefix" />
</span>
<input
ref={(el: HTMLInputElement | null) => {
if (el) control = el
}}
part="control"
prop:value={() => props.value ?? ''}
placeholder={() => props.placeholder}
disabled={() => Boolean(props.disabled)}
onInput={nativeEvent => {
const value = control.value
ctx.host.value = value
ctx.emit.valueChange({ value, nativeEvent })
}}
/>
</Host>
)
}
export const ZInput = defineElement<InputProps, InputHost, InputEmits>(
'z-input',
{
shadow: false,
props: {
value: {
type: String,
default: '',
reflect: true,
},
placeholder: String,
disabled: prop(Boolean),
invalid: prop(Boolean),
},
emits: {
valueChange: event<{ value: string; nativeEvent: Event }>(),
},
meta: {
description: 'Input primitive.',
},
},
setup,
)Options
interface DefineElementOptions<TProps, TEmits> {
shadow?: boolean | ShadowRootInit
formAssociated?: boolean
props?: PropOptions<TProps>
emits?: TEmits
form?: FormAssociatedOptions<TProps>
styles?: string | string[]
models?: readonly ElementModelDefinition<TProps>[]
cssVars?: Record<string, { description?: string }>
meta?: DefineElementMeta
}The author-facing surface should stay small. Prefer source inference over metadata:
modelsis inferred from<prop>+<prop>Change+detail.<prop>.slotsis inferred from<Slot>and native<slot>.cssPartsis inferred from staticpart="...".cssVarsis optional documentation for public styling tokens only.
Props
Supported constructors:
StringNumberBooleanObjectArrayFunction
Useful shorthand:
props: {
disabled: prop(Boolean),
}prop(Boolean) is equivalent to:
{
type: Boolean,
default: false,
reflect: true,
}String literal unions use prop(values):
props: {
size: prop(['sm', 'md', 'lg'], {
default: 'md',
reflect: true,
}),
}Complex attribute serialization must be explicit:
props: {
tokens: {
type: Array,
attr: 'tokens',
reflect: true,
default: () => [],
serialize: value => (value?.length ? value.join('|') : null),
deserialize: value => (value ? value.split('|') : []),
},
}Events
Events are declared in emits:
emits: {
valueChange: event<{ value: string }>(),
press: event<{ nativeEvent: MouseEvent }>('press'),
}Default event mapping:
valueChangebecomes DOM eventvalue-change.- React wrapper exposes
onValueChange. - Vue wrapper emits
value-change. - Default event options are
bubbles: true,composed: true,cancelable: false.
Emit from setup:
ctx.emit.valueChange({ value: 'next' })Undeclared ctx.emit.* calls are not part of the public event surface.
Vue Models
The analyzer infers Vue model metadata for common controlled props:
props: {
value: String,
},
emits: {
valueChange: event<{ value: string }>(),
},This produces:
models: [
{
prop: 'value',
event: 'value-change',
eventPath: 'detail.value',
},
]Vue users can write:
<ZInput v-model:value="email" />Use explicit models only for non-standard event names or detail paths. Use models: [] to disable inference for a component.
Slots And Parts
Do not duplicate common slots and parts in metadata:
<Slot name="prefix" />
<input part="control" />The analyzer emits:
- slot
prefix - css part
control
Use meta.slots or meta.cssParts only when the information cannot be statically inferred or needs extra documentation.
Exposed Methods
Expose host instance methods with ctx.expose():
ctx.expose({
focus() {
control.focus()
},
})The analyzer extracts method names and common TypeScript signatures. Lazy Web Component output creates method proxies that wait for the real component chunk before invoking the exposed method.