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.
PropertySchema
Section titled “PropertySchema”The full definition of a single property:
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}Field reference
Section titled “Field reference”| Field | Purpose |
|---|---|
key | Stable identifier used in bindings, CEL expressions, and compile values map. Snake_case by convention. |
name | Human-readable label for property editors. |
property_type | Constrains the value space. Also drives auto-generated validation rules. |
required | When true, the Pending trait treats a missing value as incomplete. |
is_generator | When true, the property expands into multiple cards (e.g., cloze deletions, multi-select items). |
default_value | Pre-populated value. Components often set sensible defaults for optional properties. |
description | Shown in property editors and included in AI prompts via Describe. |
is_system | System properties are not shown in the editor UI. Used for internal bookkeeping. |
is_title | At most one property per component can be the title. Used as the display name in placement lists. |
Declaring properties
Section titled “Declaring properties”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> },}Type details
Section titled “Type details”| Type | Value shape | Constraints |
|---|---|---|
Text | String(s) | Optional max_length → auto-generates size(value) <= N |
Number | Number(f64) | Optional min/max → auto-generates range checks |
Boolean | Bool(b) | No extra constraints |
Color | String(hex) | Hex color string (e.g., "#FF5733") |
Date | String(iso) | ISO 8601 date string |
Select | String(id) or Array(ids) | multiple: false → single ID, multiple: true → array of IDs |
Relation | String(id) or Array(ids) | References to other entities (notes, cards, decks) |
File | String(url) or Array(urls) | File attachment URLs |
Cloze | String(text) | Text with {{c1::...}} cloze deletions |
Url | String(url) | URL string |
Formula | Computed | CEL expression that derives a value from other properties |
Json | String(json) | Arbitrary JSON with optional JSON Schema validation |
SelectOption
Section titled “SelectOption”Options for Select type properties:
pub struct SelectOption { pub id: String, // lowerCamelCase identifier pub name: String, // Display name pub color: Option<u32>, // Optional ARGB color}PropertyValue
Section titled “PropertyValue”The runtime representation of a property’s data:
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.
Typed extraction in compile
Section titled “Typed extraction in compile”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.
Building bindings in reducers
Section titled “Building bindings in reducers”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.
Property
Section titled “Property”Combines a schema with its current value — used in AI contexts:
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.
Generator properties
Section titled “Generator properties”When is_generator is true, a property can expand a single component into multiple cards. The expansion depends on the property type:
| Type | Expansion |
|---|---|
| Multi-select / Relation / File | One card per array item |
| Cloze | One card per {{cN::...}} deletion |
| Others | Always one card |
impl PropertySchema { pub fn card_count(&self, value: &PropertyValue) -> usize { /* ... */ } pub fn expand_value(&self, value: &PropertyValue) -> Vec<(PropertyValue, String)> { /* ... */ }}Related
Section titled “Related”- Bindings — how property values flow from parent to child
- Validation — rules and the Pending trait
- CEL Environment — expression evaluation context
- Component — the
Component::properties()declaration