Skip to content

Properties Overview

Properties are the configurable fields that components expose. A component declares its properties via PropertySchema, each schema has a PropertyType constraining what values are valid, and PropertyValue holds the runtime data.

The full definition of a single property:

crates/core/src/models/property/mod.rs [387:415]
pub struct PropertySchema {
pub key: String, // Unique key (snake_case)
pub name: String, // Display name
pub property_type: PropertyType, // Type and constraints
pub required: bool, // Must have a value
pub is_generator: bool, // Generates multiple items (cards)
pub default_value: Option<PropertyValue>, // Default when unset
pub description: Option<String>, // UI/doc description
pub icon: Option<String>, // Icon identifier
pub is_system: bool, // Not user-editable
pub is_title: bool, // Used as display title
}
FieldPurpose
keyStable identifier used in bindings, CEL expressions, and compile values map. Snake_case by convention.
nameHuman-readable label for property editors.
property_typeConstrains the value space. Also drives auto-generated validation rules.
requiredWhen true, the Pending trait treats a missing value as incomplete.
is_generatorWhen true, the property expands into multiple cards (e.g., cloze deletions, multi-select items).
default_valuePre-populated value. Components often set sensible defaults for optional properties.
descriptionShown in property editors and included in AI prompts via Describe.
is_systemSystem properties are not shown in the editor UI. Used for internal bookkeeping.
is_titleAt most one property per component can be the title. Used as the display name in placement lists.

Components declare their schemas via #[derive(ComponentProperties)] on a typed props struct. The macro generates the Vec<PropertySchema> from field types and #[property(...)] attributes:

#[derive(modality_core::ComponentProperties)]
pub struct TextBoxProps {
#[property(name = "Text", description = "The text content", default = "")]
pub text: String,
#[property(key = "backgroundColor", name = "Background", description = "Background color (optional)")]
pub background_color: Option<Color>,
#[property(key = "borderColor", name = "Border Color")]
pub border_color: Option<Color>,
#[property(key = "borderWidth", name = "Border Width", default = "2")]
pub border_width: f64,
#[property(name = "Padding", default = "12")]
pub padding: f64,
}

Each Rust type implements the PropertyField trait to map to PropertyType and PropertyValue. Built-in impls: f64, i32, u32, bool, String, Option<T>, Color. Enums use #[derive(PropertyField)] to generate PropertyType::Select.

The key attribute overrides the property key string (defaults to the field name). Use for camelCase backward compatibility (e.g. background_color field → "backgroundColor" key).

## PropertyType
Each property has a type that constrains its value space and drives auto-generated [validation rules](/reference/properties/validation/).
```rust title="crates/core/src/models/property/mod.rs [122:168]"
pub enum PropertyType {
Text { max_length: Option<usize> },
Number { min: Option<f64>, max: Option<f64> },
Boolean,
Color,
Date,
Select {
options: Vec<SelectOption>,
multiple: bool,
},
Relation {
target_type: RelationTargetType,
target_deck_id: String,
multiple: bool,
is_prerequisite: bool,
},
File { multiple: bool },
Cloze,
Url,
Formula {
expression: String,
return_type: FormulaReturnType,
},
Json { schema: Option<String> },
}
TypeValue shapeConstraints
TextString(s)Optional max_length → auto-generates size(value) <= N
NumberNumber(f64)Optional min/max → auto-generates range checks
BooleanBool(b)No extra constraints
ColorString(hex)Hex color string (e.g., "#FF5733")
DateString(iso)ISO 8601 date string
SelectString(id) or Array(ids)multiple: false → single ID, multiple: true → array of IDs
RelationString(id) or Array(ids)References to other entities (notes, cards, decks)
FileString(url) or Array(urls)File attachment URLs
ClozeString(text)Text with {{c1::...}} cloze deletions
UrlString(url)URL string
FormulaComputedCEL expression that derives a value from other properties
JsonString(json)Arbitrary JSON with optional JSON Schema validation

Options for Select type properties:

crates/core/src/models/property/mod.rs [47:53]
pub struct SelectOption {
pub id: String, // lowerCamelCase identifier
pub name: String, // Display name
pub color: Option<u32>, // Optional ARGB color
}

The runtime representation of a property’s data:

crates/core/src/models/property/mod.rs [299:307]
pub enum PropertyValue {
String(String), // Text, select IDs, colors, URLs, dates
Number(f64), // Numeric values
Bool(bool), // Boolean values
Array(Vec<String>), // Multi-select, relations, files
Null, // Empty / unset
}

PropertyValue is intentionally simple — a small set of variants that cover all PropertyType cases. The PropertyType provides the semantic interpretation (is this string a color? a URL? a date?), while PropertyValue is just the storage.

Components receive a typed properties struct instead of a raw HashMap. The #[derive(ComponentProperties)] macro generates from_values() which extracts and converts all fields with defaults:

impl TypedComponent for TextBoxComponent {
type Properties = TextBoxProps;
// ...
fn compile(
&self,
props: &TextBoxProps,
meta: &WhiteboardChildMeta,
) -> Result<Vec<WhiteboardElement>, CompileError> {
// All values are already extracted and typed:
let text = &props.text; // String, defaults to ""
let bg = props.background_color; // Option<Color>
let bw = props.border_width; // f64, defaults to 2.0
// ... build output elements
}
}

The blanket impl<T: TypedComponent> Component for T calls Props::from_values(&values) before delegating to the typed compile(), so the untyped HashMap pipeline is preserved transparently.

When creating or updating placements in reducer intent handlers, use the generated methods:

// Full construction -- all fields:
let bindings = TextBoxProps {
text: "Hello".to_string(),
background_color: Some(Color(0xFFFFFFFF)),
border_color: None,
border_width: 2.0,
padding: 12.0,
}.to_bindings();
// Partial update -- only changed fields:
TextBoxPropsUpdate {
background_color: Some(Some(Color(0xFF0000FF))),
border_width: Some(4.0),
..Default::default()
}.apply(&mut placement.bindings);

For Option<T> props fields, the update struct wraps in another Option — use .map(Some) when converting from intent parameters.

Combines a schema with its current value — used in AI contexts:

crates/core/src/template/property.rs [10:13]
pub struct Property {
pub schema: PropertySchema,
pub value: PropertyValue,
}

Property implements Describe, Validate, and Pending. It’s the primary type the AI system works with when filling component values.

When is_generator is true, a property can expand a single component into multiple cards. The expansion depends on the property type:

TypeExpansion
Multi-select / Relation / FileOne card per array item
ClozeOne card per {{cN::...}} deletion
OthersAlways one card
impl PropertySchema {
pub fn card_count(&self, value: &PropertyValue) -> usize { /* ... */ }
pub fn expand_value(&self, value: &PropertyValue) -> Vec<(PropertyValue, String)> { /* ... */ }
}