Docs
Authoring a custom element
Define a props interface with SignalValue, extend Node, wire @initial and @signal, add children in the constructor, then implement animateIn and animate.
A custom element is a class. It extends Node or a closer built-in. It takes a props interface. It builds a child tree in the constructor. It exposes generator methods for animation.
Start from a catalog template when one already covers the design. This page is the from-scratch path.
Props
Every custom prop is wrapped in SignalValue. Extend NodeProps (or LayoutProps / Txt props) so position, opacity, and scale stay available.
import { Node, NodeProps } from '@vidova/2d';
import { SignalValue, PossibleColor } from '@vidova/core';
export interface HelloTitleProps extends NodeProps {
label?: SignalValue<string>;
textColor?: SignalValue<PossibleColor>;
textSize?: SignalValue<number>;
}Use PossibleColor for color props so callers can pass a hex string. Extend Layout and LayoutProps when the element is itself a flex container.
Class
The class must extend Node or one of its subclasses. Pick the closest built-in. A title that is only text can extend Txt. A chip with a background extends Node and adds a Rect plus a Txt.
export class HelloTitle extends Node {
// implementation
}The exported class name is componentName on the component create call.
Signals
Each prop on the interface needs a class field of the same name. Decorate it with @initial and @signal. Colors use @colorSignal and ColorSignal.
export class HelloTitle extends Node {
@initial('Hello')
@signal()
public declare readonly label: SimpleSignal<string, this>;
@initial('#ffffff')
@colorSignal()
public declare readonly textColor: ColorSignal<this>;
@initial(48)
@signal()
public declare readonly textSize: SimpleSignal<number, this>;
}Fields use public, declare, and readonly. @signal is required for every prop you accept. @initial sets the value when the caller omits it.
Do not name a field after a Node member. draw, size, scale, opacity, position, and the rest of that list overwrite the real method or property. The element then draws nothing, with no compile error. Full list: Layout.
How signals update: Signals.
Constructor
Pass props to super. Then this.add() the child tree, the same way a scene adds to its view.
public constructor(props?: HelloTitleProps) {
super({ ...props });
this.add(
<Txt
text={() => this.label()}
fill={() => this.textColor()}
fontSize={() => this.textSize()}
fontFamily="Inter Variable"
fontWeight={600}
/>,
);
}Bind child props to functions that read the class signals. A one-shot text assignment that calls label() will not update when label changes.
You can pin a built-in prop in super when it must always be on:
super({
layout: true,
...props,
});Animation methods
Generator methods on the class are how you animate the element. The timeline calls animate(). Thumbnails play animateIn(). Keep them as two methods. Details: Animation.
public *animateIn(duration: number = 0.4): ThreadGenerator {
this.opacity(0);
yield* this.opacity(1, duration, easeOutCubic);
}
public *animate(duration?: number): ThreadGenerator {
yield* this.animateIn(duration ?? 0.4);
}Full source
import { Node, NodeProps, Txt, signal, initial, colorSignal } from '@vidova/2d';
import {
SignalValue,
SimpleSignal,
ColorSignal,
PossibleColor,
easeOutCubic,
type ThreadGenerator,
} from '@vidova/core';
export interface HelloTitleProps extends NodeProps {
label?: SignalValue<string>;
textColor?: SignalValue<PossibleColor>;
textSize?: SignalValue<number>;
}
export class HelloTitle extends Node {
@initial('Hello')
@signal()
public declare readonly label: SimpleSignal<string, this>;
@initial('#ffffff')
@colorSignal()
public declare readonly textColor: ColorSignal<this>;
@initial(48)
@signal()
public declare readonly textSize: SimpleSignal<number, this>;
public constructor(props?: HelloTitleProps) {
super({ ...props });
this.add(
<Txt
text={() => this.label()}
fill={() => this.textColor()}
fontSize={() => this.textSize()}
fontFamily="Inter Variable"
fontWeight={600}
/>,
);
}
public *animateIn(duration: number = 0.4): ThreadGenerator {
this.opacity(0);
yield* this.opacity(1, duration, easeOutCubic);
}
public *animate(duration?: number): ThreadGenerator {
yield* this.animateIn(duration ?? 0.4);
}
}Place it with component create and timeline_edit addClip. Walkthrough: Quickstart. Asset slots and inputDefs: Inputs. SkSL: Shaders.
