Props System
Props are the atomic unit of the Tagixo builder schema. Every field that appears in a module's properties drawer — a text input, a color picker, a toggle, a repeater of slides — is backed by a PHP class that extends AbstractProp. Props carry their own key, label, default value, widget identifier, and any widget-specific configuration. The builder serializes the full set of Props for a module into a JSON metadata payload that the Vue frontend uses to render the drawer. When a user saves, the values flow back as a JSON object keyed by prop key, and those values are what your module's Blade template receives at render time.
Architecture: Prop vs PropType
Before diving into individual classes, it is worth being clear on the boundary:
- A Prop is one field.
TextProp,SelectProp,ToggleProp— each maps to a single widget in the drawer. - A PropType is a reusable group of Props (often with CSS generation logic).
TypographyPropType,SpacingPropType,BackgroundPropTypebundle multiple Props under a named section.
You compose both in a ModuleDefinition. The content() method takes Props directly. The design() and advanced() methods typically take PropTypes. You can also use Props inside a custom PropType's schema() array.
ModuleDefinition
├── content(TextProp, ToggleProp, RepeaterProp, ...) ← raw Props
├── design(TypographyPropType, SpacingPropType, ...) ← PropTypes
└── advanced(ToggleProp, SelectProp, ...) ← Props or PropTypes
The core base class for all Props is Ccast\Tagixo\Core\Props\AbstractProp.
Built-in Props Reference
The table below lists every built-in Prop, its JSON storage type, and the widget identifier the Vue frontend dispatches on. These are available in the Ccast\Tagixo\Core\Props\ namespace unless noted otherwise.
| Class | Widget ID | Stored as | Primary use |
|---|---|---|---|
TextProp |
text |
string |
Single-line text, labels, URLs |
TextareaProp |
textarea |
string |
Multi-line plain text |
EditorProp |
editor |
string (HTML) |
Rich text via Tiptap |
NumberProp |
number |
int|float |
Numeric inputs with optional min/max/step |
SliderProp |
slider |
int|float |
Range slider with configurable bounds and unit |
SizeProp |
size |
{value, unit} |
Dimension fields: px, em, rem, % |
ColorProp |
color |
string (CSS color) |
Color picker, supports hex, rgb, rgba, CSS variables |
SelectProp |
select |
string |
Dropdown from a fixed option set |
MultiSelectProp |
multi-select |
string[] |
Multi-select from a fixed option set |
ToggleProp |
toggle |
bool |
Boolean on/off switch |
MediaProp |
media |
{id, url, alt} |
Image/video from the media gallery |
IconProp |
icon |
string (icon key) |
Icon picker from registered Blade Icons packs |
FontPickerProp |
font-picker |
string (font family) |
Google Fonts or system font picker |
RepeaterProp |
repeater |
array of objects |
Ordered list of structured items with sub-fields |
ModelSelectProp |
model-select |
int|string (ID) |
Select from a registered Eloquent model's records |
CodeEditorProp |
code-editor |
string |
Raw code with syntax highlighting |
LinkProp |
link |
{href, target, rel} |
URL + target + rel as a structured object |
Specialized internal Props exist for complex built-in PropTypes — FourWaysProp (spacing), BoxShadowProp, CssDeclarationsProp, TabsProp — but these are consumed by their owning PropType and rarely referenced directly in module definitions.
TextProp
Renders a single-line <input type="text">. The value is stored as a plain string. Use for titles, button labels, URLs as plain strings, or any short text that should not parse to HTML.
TextProp::make('cta_label')
->setLabel('Button label')
->default('Get started')
->placeholder('e.g. Sign up free')
->helpText('Appears on the primary button.');
TextareaProp
Multi-line plain text. Does not parse HTML. Use for descriptions, alt text, or any content where newlines matter but rich formatting is not needed.
TextareaProp::make('summary')
->setLabel('Summary')
->rows(4);
EditorProp
Embeds the Tiptap rich-text editor. The value is an HTML string. Content tokens ({{year}}, {{site.name}}) and inline links work inside editor output. Do not mix rich editor output with additional CSS string manipulation in PHP — treat it as opaque HTML.
EditorProp::make('body')
->setLabel('Body content')
->default('<p>Describe your product here.</p>');
NumberProp
Integer or float input. Supports min(), max(), step(), and unit() display suffix (display only — the unit is not stored in the value).
NumberProp::make('columns')
->setLabel('Column count')
->min(1)->max(6)->step(1)
->default(3);
SliderProp
A drag slider for numeric values. Identical storage format to NumberProp. Use when a continuous range is more intuitive than a text input. Chain min(), max(), step(), unit().
SliderProp::make('opacity')
->setLabel('Opacity')
->min(0)->max(100)->step(1)->unit('%')
->default(100);
SizeProp
A combined number + unit control. Stored as {value: float, unit: string}. Built-in units: px, %, em, rem, vw, vh. Use allowedUnits() to restrict choices. Call ->none() to add a "none" option that sets the CSS to nothing.
SizeProp::make('max_width')
->setLabel('Max width')
->allowedUnits(['px', '%', 'rem'])
->default(['value' => 800, 'unit' => 'px']);
When rendering, convert with your own helper or destructure directly:
$maxWidth = $props['max_width']['value'] . $props['max_width']['unit'];
ColorProp
A color picker with hex, rgb, rgba, and CSS variable support. The stored value is always a CSS-ready string (#1a2b3c, rgba(0,0,0,0.5), var(--tgx-primary)). Pair with variablePicker('color') to allow selection from global color variables.
ColorProp::make('accent')
->setLabel('Accent color')
->variablePicker('color')
->default('#0ea5e9');
SelectProp
A dropdown backed by a static options array. Options are set via options() as an associative array [value => label].
SelectProp::make('layout')
->setLabel('Layout')
->options([
'grid' => 'Grid',
'list' => 'List',
'masonry' => 'Masonry',
])
->default('grid');
ToggleProp
A boolean switch. Default is false unless overridden. Often paired with showWhen() on dependent Props.
ToggleProp::make('show_badge')
->setLabel('Show badge')
->default(false);
MediaProp
Opens the media gallery picker. The value is a structured object: {id: int, url: string, alt: string}. Use type() to constrain to image or video. At render time, use $props['hero_image']['url'] directly in Blade.
MediaProp::make('hero_image')
->setLabel('Hero image')
->type('image');
IconProp
A searchable icon picker drawing from all registered Blade Icons packs. The stored value is the icon key as a string (e.g. heroicon-o-star). Render with <x-dynamic-component :component="$props['icon']" /> or resolve via the icon registry.
IconProp::make('feature_icon')
->setLabel('Icon')
->default('heroicon-o-check-circle');
RepeaterProp
A sortable list of structured sub-items. Each item stores the complete set of child prop values as an object. See the Repeater deep-dive section below.
ModelSelectProp
Populates a dropdown from an Eloquent model's records. Pass the model class and optionally configure label/value columns.
ModelSelectProp::make('category_id')
->setLabel('Category')
->model(\App\Models\Category::class)
->labelColumn('name')
->valueColumn('id');
The model must be reachable at schema-build time. Dynamic filtering (e.g. "only categories where type = blog") is not supported out of the box — use a custom Prop backed by a Vue component that accepts an API endpoint if you need filtered lazy-loaded options.
The Fluent API
Every Prop built with AbstractProp shares the same base fluent interface. Method chains return $this so they compose naturally.
Core setters
| Method | Type | Description |
|---|---|---|
make(string $key) |
static | Construct the Prop and bind its storage key. The key is the field name in the saved JSON. |
setLabel(string $label) |
fluent | Human-readable label shown above the widget in the drawer. |
default(mixed $value) |
fluent | Initial value when the module is first inserted or when the field is reset. |
placeholder(string $text) |
fluent | Ghost text shown when the field is empty (text-family Props only). |
helpText(string $text) |
fluent | Small description shown below the field. Plain text; do not use {{ }} syntax — it will be evaluated by Vue and crash on undefined vars. |
required() |
fluent | Marks the field as required in the drawer UI (visual only — not enforced server-side). |
Conditional visibility
| Method | Signature | Description |
|---|---|---|
showWhen(field, value, op?) |
string, mixed, string |
Show this Prop only when field matches value. Optional operator: = (default), !=, >, <, >=, <=. |
hideWhen(field, value, op?) |
string, mixed, string |
Inverse of showWhen. |
showWhen and hideWhen are drawer UX features only. The values are still stored and passed to the module renderer regardless of visibility. Never rely on them for security or business-rule enforcement.
ToggleProp::make('has_badge')->setLabel('Show badge')->default(false),
TextProp::make('badge_text')
->setLabel('Badge text')
->showWhen('has_badge', true),
ColorProp::make('badge_color')
->setLabel('Badge color')
->showWhen('has_badge', true),
Variable picker
->variablePicker(string $type)
Adds a variable-picker button next to the field. When the user selects a global variable, the stored value becomes a token reference (e.g. var(--tgx-primary) for a color). Supported types: color, spacing, text, link.
ColorProp::make('brand_color')
->variablePicker('color')
->default('#0ea5e9');
sub() — inline sub-props
Some Props accept nested sub-level props via ->sub([...]). This is used internally by composite Props like SizeProp. For custom Props, prefer RepeaterProp with explicit child fields over inventing nested sub() structures.
withConfig() — raw widget config merge
Escape hatch for sending arbitrary keys into the widget config payload:
->withConfig(['rows' => 6, 'monospace' => true])
Use this when a built-in widget supports options that aren't exposed as named fluent methods.
Organizing Props: Groups and Sections
Within the content drawer, Props appear in declaration order. To group them under labeled collapsible sections, wrap them with a PropGroup:
use Ccast\Tagixo\Core\Props\PropGroup;
ModuleDefinition::make()->content(
PropGroup::make('Content')
->props([
TextProp::make('title')->setLabel('Title'),
TextareaProp::make('description')->setLabel('Description'),
]),
PropGroup::make('Call to action')
->props([
ToggleProp::make('show_cta')->setLabel('Show CTA')->default(true),
TextProp::make('cta_text')->setLabel('Button text')->showWhen('show_cta', true),
LinkProp::make('cta_link')->setLabel('Button link')->showWhen('show_cta', true),
]),
);
Groups collapse independently. Collapsed state is persisted per-module-instance in the drawer. Keep groups focused — five to eight Props per group is a reasonable upper bound before the UX degrades.
Use
PropGroupfor logical groupings of content Props, not for design tabs. Design tab organization is controlled by PropTypes and theirtab()return value, not by groups.
RepeaterProp In Depth
RepeaterProp stores an ordered array of objects. Each item in the array has a _id key (generated at insertion, used for VNode keying) plus whatever keys your child Props define. Items are sortable by drag-and-drop in the drawer.
Defining child fields
Pass child Props to fields(). These are the same Prop classes used everywhere else.
RepeaterProp::make('features')
->setLabel('Feature rows')
->fields([
IconProp::make('icon')->setLabel('Icon')->default('heroicon-o-check'),
TextProp::make('text')->setLabel('Text'),
ColorProp::make('icon_color')->setLabel('Icon color')->default('#16a34a'),
]);
Constraints
->minItems(int $n) // Enforce a minimum count (prevents deletion below n)
->maxItems(int $n) // Disable add-item button above n
->sortable(bool) // Default true; pass false to remove drag handles
->addLabel(string) // Text for the "Add item" button
JSON shape
{
"features": [
{ "_id": "abc123", "icon": "heroicon-o-check", "text": "Fast deploy", "icon_color": "#16a34a" },
{ "_id": "def456", "icon": "heroicon-o-shield-check", "text": "Secure by default", "icon_color": "#2563eb" }
]
}
Do not reference _id in your Blade template logic. It is a Vue-layer concern for diff keying. Use it only as a loop key if you need one.
Nested repeaters
RepeaterProp fields can include another RepeaterProp — for example, a tab list where each tab has a list of bullet points. Keep nesting depth to two levels maximum; three-deep repeaters become impractical in the drawer and fragile to JSON migrations.
Rendering a repeater
@foreach($props['features'] as $feature)
<div class="feature-row">
<x-dynamic-component :component="'{{ $feature['icon'] }}'" />
<span>{{ $feature['text'] }}</span>
</div>
@endforeach
Never change repeater item key names in production without a database migration that rewrites the stored JSON. The key name is the sole link between stored data and rendered output. Renames silently produce empty values.
Props at Render Time
When the PageRenderer renders a module, it resolves the stored JSON for that module instance and merges in the declared defaults for any missing keys. The merged result is what your Blade template receives as $props.
This means:
- A newly inserted module always has the declared
default()values, even if the user never opened the drawer. - A module saved before a new Prop was added gets that Prop's
default()value on next render. - Removing a Prop from a module definition does not delete its stored value — the key is silently ignored.
{{-- resources/views/tagixo/modules/my-card.blade.php --}}
<div class="card" style="{{ $props['bg_color'] ? 'background:' . $props['bg_color'] : '' }}">
<h3>{{ $props['title'] }}</h3>
@if($props['show_body'])
<div class="card-body">{!! $props['body'] !!}</div>
@endif
</div>
Always use
{!! !!}(unescaped) when renderingEditorPropvalues — the stored value is HTML. Use{{ }}(escaped) forTextPropandTextareaPropvalues, which are plain strings.
Custom Props
Build a custom Prop when none of the built-in widget types fit. A typical trigger: a dual-handle range picker, a structured JSON editor, a gradient stop builder, or any widget with its own Vue component.
Scaffold
php artisan make:tagixo-prop GradientBuilderProp
Default output: app/Tagixo/Props/GradientBuilderProp.php. Relocate via config('tagixo.scaffolding.path').
Required overrides
<?php
namespace App\Tagixo\Props;
use Ccast\Tagixo\Core\Props\AbstractProp;
class GradientBuilderProp extends AbstractProp
{
protected string $direction = 'to right';
public function widget(): string
{
// Must match the key registered in your Vue widget dispatcher
return 'gradient-builder';
}
public function widgetConfig(): array
{
return array_merge($this->baseConfig(), [
'direction' => $this->direction,
]);
}
// Custom fluent setter
public function direction(string $direction): static
{
$this->direction = $direction;
return $this;
}
}
baseConfig() populates the standard keys all widgets expect: key, label, default, helpText, variablePicker, showWhen. Always spread it first and merge your widget-specific keys on top.
Vue widget contract
The widget key returned from widget() must be registered in the drawer's widget dispatcher (DynamicPropType.vue routes by this key). If using the custom-props distribution pattern from your plugin bundle:
- Build a Vue component that receives
config(thewidgetConfig()payload) and emitsupdate:modelValuewith the new value. - Register it under the same key string in your plugin's frontend entry point.
- Include your plugin's compiled JS in the page via the Tagixo plugin asset pipeline.
If your custom Prop can reuse an existing widget identifier (e.g. slider, select, number) with different defaults or constraints, return that existing identifier and you need zero Vue code.
Registration
Props are not registered individually. They travel with the PropType or module definition that instantiates them. If your custom Prop is shared across many PropTypes, those PropTypes go into your plugin's service provider — the Prop classes come along for the ride.
Prop Design Checklist
Keep these rules in mind when composing module schemas:
- One Prop, one value. Do not encode multiple logical fields into a single
TextPropvalue (e.g."199|349|Annual"). Each logical value gets its own Prop. - Keys are permanent. Once a module is in production, renaming a prop key requires a data migration against stored JSON.
- Defaults must render safely. The
default()value is what a freshly inserted module shows. It must produce valid, non-broken output with zero further configuration. showWhenis not validation. All prop values reach the server; conditional visibility is display-only. Validate server-side if a value's presence is conditional on another field.- Prefer composable schemas over monoliths. A 20-field content section is a sign the module is doing too much. Consider splitting into two modules or using a repeater.
- Variable picker for design tokens. Any Prop that controls a color or size should offer
->variablePicker('color')/->variablePicker('spacing')so pages can participate in the site's design system. - RepeaterProp for collections, not JSON blobs. If you find yourself storing
json_encode(...)in aTextProp, switch toRepeaterPropwith typed child Props.
Example: Full Module Content Schema
use Ccast\Tagixo\Core\Props\TextProp;
use Ccast\Tagixo\Core\Props\EditorProp;
use Ccast\Tagixo\Core\Props\MediaProp;
use Ccast\Tagixo\Core\Props\SelectProp;
use Ccast\Tagixo\Core\Props\ToggleProp;
use Ccast\Tagixo\Core\Props\LinkProp;
use Ccast\Tagixo\Core\Props\ColorProp;
use Ccast\Tagixo\Core\Props\RepeaterProp;
use Ccast\Tagixo\Core\Props\IconProp;
ModuleDefinition::make()
->content(
// Hero block
MediaProp::make('image')->setLabel('Hero image')->type('image'),
SelectProp::make('image_position')
->setLabel('Image position')
->options(['left' => 'Left', 'right' => 'Right'])
->default('right'),
TextProp::make('eyebrow')
->setLabel('Eyebrow label')
->placeholder('e.g. New release'),
TextProp::make('headline')
->setLabel('Headline')
->default('Build faster with Tagixo'),
EditorProp::make('body')
->setLabel('Body text'),
// CTA group
ToggleProp::make('show_cta')
->setLabel('Show call to action')
->default(true),
TextProp::make('cta_text')
->setLabel('Button label')
->default('Get started')
->showWhen('show_cta', true),
LinkProp::make('cta_href')
->setLabel('Button link')
->showWhen('show_cta', true),
// Feature bullets
RepeaterProp::make('bullets')
->setLabel('Feature points')
->minItems(0)
->maxItems(6)
->addLabel('Add feature')
->fields([
IconProp::make('icon')
->setLabel('Icon')
->default('heroicon-o-check-circle'),
TextProp::make('text')
->setLabel('Feature text'),
ColorProp::make('icon_color')
->setLabel('Icon color')
->variablePicker('color')
->default('#16a34a'),
]),
);
See Also
- PropTypes System — Grouping Props into reusable CSS-generating sections
- Custom Props — Scaffolding a custom Prop with a custom Vue widget
- Custom PropTypes — Building schema-driven design control groups
- Module System and Catalog — Where
ModuleDefinitionfits in the module lifecycle - Sub-elements and States — Exposing stylable parts of a module beyond the root node
- Storage Model and Data Shape — How prop values are persisted and retrieved from
tgx_pages - Data Models and Global Variables — How
variablePicker()tokens are resolved at render time