Props helpers

Props are the data attached to nodes: attributes, classes, styles, and events.

Props are just data

Props are structured data attached to a node. They are not magic, and they are not special-cased outside the IR. When you use props helpers, you are simply describing how a node should look and behave. The runtime reads those props and patches the DOM accordingly.

The helpers exist to keep intent clear. They tell you where attributes live, how class names are built, how style maps are applied, and how events are attached. This makes your UI tree predictable and easy to reason about.

The core helpers: attrs, cls, style

These helpers cover the most common prop categories. They are used on almost every element node.

  • attrs([...]) attaches HTML attributes such as id, title, data-*, and aria-* properties.
  • cls(...) controls class names. It accepts strings or expressions so you can derive classes from state.
  • style([...]) applies inline styles as a map. Useful for computed values or quick demos.

Use style() for dynamic values or small adjustments, but keep most layout in CSS classes. Classes scale better as a project grows.

Events are props too

Events are attached via the on() helper. This means behavior is declared in the same prop list as attributes and classes.

An event handler is an action or a composed task, so you can treat behavior as data. This keeps structure, style, and behavior unified in the IR tree.

Reactive props and expressions

Props can be static values or expressions. When a prop depends on state, use expressions such as read(), concat(), and cond() to keep it reactive.

  • read($atom) feeds state into props
  • concat(...) builds attribute strings
  • cond(...) chooses between class strings or style maps

A good rule is to keep props static unless they truly depend on state. It makes the IR smaller and the UI easier to debug.

Example: reactive attributes

This example shows how attrs(), cls(), style(), and on() work together. The button title updates as the count changes, which demonstrates that attributes can be driven by expressions just like text nodes.

The click handler uses inc() to update the atom, and the count display reads from the same atom, so you can see the reactive loop end to end.

PHP

<?php
$cnt 
state(0);
$app el('div', [ cls('container') ], [
    
el('h2', [], [ text('Reactive attribute demo') ]),
    
el('button', [ 
            
attrs(['title' => concat('Clicked 'read($cnt), ' times')]), 
            
cls('btn btn-primary'),
            
style(['padding'=>'8px 12px','margin'=>'6px','border'=>'1px solid #ccc']),
            
on('click'inc($cnt1)) 
        ], [ 
text('Hover me, then click') ]
    ),
    
el('div', [], [ text(concat('Count: 'read($cnt))) ]),
]);

Example: class strings and style maps

This component example passes class and style values as props. The template reads prop('class') and prop('style'), so the parent can fully control presentation without changing the component internals.

Notice how cond() is used to swap between class strings and style maps. This lets you build a clean, reactive styling model without mixing logic into the template.

PHP

<?php
// ---------- State ----------
$i        state(0);          // drives label "Card #i"
$isHeavy  state(false);      // drives fontWeight via style map
$isPrimarystate(false);       // drives class string: adds/removes "card-primary"

/**
 * Component template (Card)
 * - Applies class from prop('class') as string
 * - Applies style from prop('style') as map (shallow diff expected in runtime)
 * - Shows label text to verify propÒ†’text flow
 */
$CardTpl fragment([
    
el('div', [ 
        
cls(prop('class')), 
        
style(prop('style')),
        
attrs(['id' => 'card']),
    ], [
        
el('div', [ cls('card-body') ], [
            
el('h3', [ cls('card-title') ], [ text(prop('label')) ]),
            
slot() // default slot
        
]),
    ])
]);

$card component($CardTpl, [
    
'label' => concat('Card #'read($i)),
    
'class' => cond(         // STRING usage
        
eq(read($isPrimary), val(true)),
        
'card bg-warning',
        
'card'
    
),
    
'style' => cond(           // MAP usage
        
eq(read($isHeavy), val(true)),
        
// heavy
        
val(['color' => 'tomato''fontWeight' => 700]),
        
// light
        
val(['color' => 'black''fontWeight' => 400])
    ),
], [
    
// default slot content
    
'default' => [
        
el('div', [ cls('row') ], [ 
            
el('p',[cls('my-1')], [text('From the forest the fairies were heard, stepping on leaves of 
            moonlight.'
)]),
            
el('p',[cls('my-1')], [text('The water grew afraid and turned into many mirrors, so it could 
            look back.'
)]),
            
el('p',[cls('my-1')], [text('An old shepherd, with a wire staff and a heavy heart, said he 
            had seen them -- not with his eyes, but with fear.'
)]),
            
el('p',[cls('my-1')], [text('And in that moment his shadow faded, for the fairies do not 
            spare those who understand.'
)]),
        ])
    ],
]);


// ---------- UI ----------
$app el('div', [ cls('container py-3') ], [
    
el('h2', [], [ text('Components: class (string) + style (map) - live updates') ]),

    
// Controls
    
el('div', [ cls('my-3 btn-group') ], [
        
el('button', [ 
            
cls('btn btn-primary'), 
            
on('click'inc($i1)) 
        ], [ 
            
text('Label++'
        ]),
        
el('button', [ 
            
cls('btn btn-warning'), 
            
on('click'set($isPrimarycond(read($isPrimary), val(false), val(true)))) 
        ], [ 
            
text('Toggle Warning Class'
        ]),
        
el('button', [ 
            
cls('btn btn-outline-danger'), 
            
on('click'set($isHeavycond(read($isHeavy), val(false), val(true)))) 
        ], [ 
            
text('Toggle Heavy Style'
        ]),
    ]),

    
// Demo card
    
$card,
]);

Practical guidance

  • Use attrs() for semantics and accessibility first, styling second. aria-* and data-* belong here.
  • Prefer cls() over style() for layout and theming. Reserve style() for computed values or rare overrides.
  • Keep reactive props focused. If a prop never changes, keep it static for clarity and performance.
  • Components scale props. Pass class/style as props when you want flexible presentation without forking templates.

Props are the bridge between structure and behavior. Once you are comfortable composing them, most UI patterns become straightforward.