Custom PropTypes
A PropType is a named, reusable group of design properties (props) that knows how to render itself to CSS. Instead of repeating the same three sliders and colour picker across a dozen modules, you register a PropType once and reference it by key in any module's ->design() call.
When to use a PropType
Use a PropType (rather than individual props) when:
- Two or more props always appear and work together (e.g.
border-radius+border-width+border-coloralways combine into a singlebordershorthand). - The CSS output requires conditional logic across values (e.g. "only emit the gradient if at least one stop colour is set").
- The same group is needed on more than one module.
- The group has state-layer overrides (hover, sticky) that must follow a consistent pattern.
Single-prop changes that are truly isolated belong as individual Prop definitions, not a PropType.
Scaffold
Generate the boilerplate with the Artisan generator:
php artisan make:tagixo-prop-type GradientBorder
Optional flags:
| Flag | Default | Purpose |
|---|---|---|
--path=app/Tagixo/PropTypes |
app/Tagixo/PropTypes |
Where to write the class |
--namespace=App\Tagixo\PropTypes |
derived from path | PHP namespace |
--package |
— | Emit as a distributable (adds a static key() helper) |
The generator creates a single PHP class. No view or JS file is needed — CSS is emitted server-side.
The Contract
Every PropType extends Ccast\Tagixo\PropTypes\AbstractPropType.
Minimal skeleton
namespace App\Tagixo\PropTypes;
use Ccast\Tagixo\PropTypes\AbstractPropType;
class GradientBorderPropType extends AbstractPropType
{
public static function key(): string
{
return 'gradient_border';
}
public function label(): string
{
return 'Gradient Border';
}
public function schema(): array
{
return [];
}
public function toCss(array $values, string $selector): string
{
return '';
}
}
Required methods
| Method | Return type | Purpose |
|---|---|---|
key() |
string |
Unique snake_case identifier used in ->design() and config registration. Must be globally unique across all registered PropTypes. |
label() |
string |
Human-readable name shown in the design drawer group header. |
schema() |
array |
Returns an array of Prop instances that define the drawer UI. Order determines drawer render order. |
toCss(array $values, string $selector) |
string |
Converts stored values to CSS for the base state. $selector is the fully-qualified CSS selector for the element. Return an empty string to emit nothing. |
Optional overrides
| Method | Default | When to override |
|---|---|---|
toCssForStateLayer(array $values, string $selector, string $state) |
Delegates to toCss() |
When the hover or sticky state needs different CSS output (e.g. only emit the colour delta, not re-emit static geometry). |
toCssWithSelectors(array $values, string $baseSelector, array $subSelectors) |
Delegates to toCss() |
When the PropType must target sub-elements (e.g. child <a> tags) rather than the root element selector. |
defaultValues() |
[] |
Provide hardcoded fallbacks so $values is never sparse on first render. |
panelIcon() |
null |
Return a Heroicon or Blade Icons name to show next to the drawer group label. |
Schema: Defining Props
schema() returns an ordered array of Prop objects. These are the same Prop classes used elsewhere in the builder.
use Ccast\Tagixo\Props\ColorProp;
use Ccast\Tagixo\Props\SelectProp;
use Ccast\Tagixo\Props\SliderProp;
use Ccast\Tagixo\Props\ToggleProp;
public function schema(): array
{
return [
ToggleProp::make('enabled')
->label('Enable Gradient Border')
->default(false),
SliderProp::make('width')
->label('Border Width')
->min(1)->max(20)->step(1)
->unit('px')
->default(2),
ColorProp::make('color_start')
->label('Start Colour')
->variablePicker() // shows the global-variables colour picker
->default('#6366f1'),
ColorProp::make('color_end')
->label('End Colour')
->variablePicker()
->default('#ec4899'),
SelectProp::make('direction')
->label('Direction')
->options([
'to right' => 'Left → Right',
'to bottom' => 'Top → Bottom',
'to bottom right' => 'Diagonal',
])
->default('to right'),
];
}
Notes:
->variablePicker()on aColorPropallows the user to select a CSS custom property (--my-colour) instead of a literal hex value. Tagixo resolves the variable at render time if a fallback is configured.- Prop keys must be unique within the schema. They become the keys of
$valuesintoCss(). ->default()on individual props feeds the drawer UI initial state. It does not guarantee$valueswill contain the key at render time — usedefaultValues()on the PropType itself for render-safe fallbacks.
The toCss() Contract
public function toCss(array $values, string $selector): string
What $values contains
An associative array of prop_key => stored_value for every prop in schema(), merged with defaultValues(). Values are always strings (even numbers come as "2"). A missing key means the user never set it; rely on defaultValues() or null-coalesce defensively.
What to return
A CSS string — one or more complete rules. Do not wrap in a selector; $selector is provided for you to use where needed.
// Correct
return "{$selector} { border: 2px solid red; }";
// Wrong — no selector wrapping in the return value
return "border: 2px solid red;";
Return an empty string '' to emit nothing (e.g. when the feature is disabled).
Four CSS emission idioms
1. Skip on default / disabled
if (! filter_var($values['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
return '';
}
2. Compose a multi-value shorthand
$width = $values['width'] ?? '2';
$gradient = "linear-gradient({$values['direction']}, {$values['color_start']}, {$values['color_end']})";
return "{$selector} { border: {$width}px solid transparent; background: {$gradient} border-box; }";
3. Delegate complex geometry to StyleGenerator
For props that map 1-to-1 with standard CSS properties, call the shared helper instead of re-implementing the logic:
use Ccast\Tagixo\Css\StyleGenerator;
return StyleGenerator::borderRadius($values['radius'] ?? '0', $selector);
4. No selectors in return value for sub-element targeting
If you need to target a child element, override toCssWithSelectors() instead of hardcoding a descendant selector inside toCss(). This keeps base-state and state-layer logic consistent.
resolveColorValue() helper
Use this to transparently handle both literal hex values and CSS variable tokens:
use Ccast\Tagixo\Css\CssHelpers;
$colorStart = CssHelpers::resolveColorValue($values['color_start'] ?? '#6366f1');
// Returns "var(--my-colour)" if the stored value is a variable token, otherwise the literal.
State-Aware CSS
When the user edits the hover or sticky state in the builder, Tagixo calls toCssForStateLayer() instead of toCss(). The default implementation simply delegates to toCss(), which is correct when the full CSS block can be re-emitted.
Override it when you want to emit only the delta for the state layer:
public function toCssForStateLayer(array $values, string $selector, string $state): string
{
if (! filter_var($values['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
return '';
}
// For hover: only change the gradient direction, not the border width
$gradient = "linear-gradient({$values['direction']}, {$values['color_start']}, {$values['color_end']})";
return "{$selector} { background: {$gradient} border-box; }";
}
Selector patterns by state
| State | $state value |
$selector pattern produced by Tagixo |
|---|---|---|
| Base | base |
.tgx-element-{id} |
| Hover | hover |
.tgx-element-{id}:hover |
| Sticky (ancestor) | sticky |
.is-sticky .tgx-element-{id} |
You receive $selector already resolved — never reconstruct it manually inside the PropType.
Sub-Element-Aware CSS
Some PropTypes must style a child element rather than the root element (e.g. a typography PropType targeting inner <p> tags). Override toCssWithSelectors():
public function toCssWithSelectors(array $values, string $baseSelector, array $subSelectors): string
{
$output = $this->toCss($values, $baseSelector);
foreach ($subSelectors as $sub) {
$output .= $this->toCss($values, "{$baseSelector} {$sub}");
}
return $output;
}
$subSelectors is an array of child selectors (e.g. ['p', 'li', 'blockquote']) provided by the module when it registers the PropType association. If your PropType owns the root element only, do not override this method.
Registration
A PropType must be registered before any module can reference its key.
Via config
Add the fully-qualified class name to config/tagixo.php:
// config/tagixo.php
return [
'prop_types' => [
\App\Tagixo\PropTypes\GradientBorderPropType::class,
],
];
Via facade (service provider)
use Ccast\Tagixo\Facades\Tagixo;
// Register one
Tagixo::registerPropType(\App\Tagixo\PropTypes\GradientBorderPropType::class);
// Register many
Tagixo::registerPropTypes([
\App\Tagixo\PropTypes\GradientBorderPropType::class,
\App\Tagixo\PropTypes\OverlayPropType::class,
]);
Call registerPropType() / registerPropTypes() inside your AppServiceProvider::boot() or a dedicated TagixoServiceProvider::boot(). Registration must happen before the first request resolves a module schema.
Distributable plugin pattern
When shipping a PropType inside a Composer package, register it in the package's service provider and declare it as a tagged singleton so host apps can discover it without editing config/tagixo.php:
// In your package ServiceProvider::boot()
Tagixo::registerPropType(GradientBorderPropType::class);
Enabling on a Module
Once registered, add the key to any module's ->design() call:
public function design(): array
{
return [
'spacing',
'border',
'gradient_border', // <-- your PropType key
'box_shadow',
];
}
The design drawer will render a collapsible group for each key in the order declared. If a key is listed but the corresponding PropType is not registered, Tagixo silently skips it in the drawer and emits no CSS — no exception is thrown, but the feature simply does not appear for the user.
Design Panel Ordering
The design drawer renders PropType groups in the order returned by DesignPanelOrder::order() for the module — not the order declared in ->design(). The canonical order is:
[module-specific groups] → sizing → spacing → border → box_shadow → filter → transform → animation
Module-specific groups (i.e. custom PropTypes not in the canonical list) appear before the universal panels, in the order they are declared in ->design().
Example
// Module declares:
public function design(): array
{
return ['gradient_border', 'overlay', 'spacing', 'border', 'box_shadow'];
}
// Drawer renders:
// 1. gradient_border (module-specific, declared first)
// 2. overlay (module-specific, declared second)
// 3. spacing (universal)
// 4. border (universal)
// 5. box_shadow (universal)
Universal panel guarantee: sizing, spacing, border, box_shadow, filter, transform, and animation are always appended in canonical order regardless of whether they appear in ->design(). Listing them explicitly controls their inclusion, not their position relative to each other.
Complete Example: GradientBorderPropType
<?php
namespace App\Tagixo\PropTypes;
use Ccast\Tagixo\Css\CssHelpers;
use Ccast\Tagixo\Props\ColorProp;
use Ccast\Tagixo\Props\SelectProp;
use Ccast\Tagixo\Props\SliderProp;
use Ccast\Tagixo\Props\ToggleProp;
use Ccast\Tagixo\PropTypes\AbstractPropType;
class GradientBorderPropType extends AbstractPropType
{
public static function key(): string
{
return 'gradient_border';
}
public function label(): string
{
return 'Gradient Border';
}
public function defaultValues(): array
{
return [
'enabled' => false,
'width' => '2',
'color_start' => '#6366f1',
'color_end' => '#ec4899',
'direction' => 'to right',
];
}
public function schema(): array
{
return [
ToggleProp::make('enabled')
->label('Enable Gradient Border')
->default(false),
SliderProp::make('width')
->label('Border Width')
->min(1)->max(20)->step(1)
->unit('px')
->default(2),
ColorProp::make('color_start')
->label('Start Colour')
->variablePicker()
->default('#6366f1'),
ColorProp::make('color_end')
->label('End Colour')
->variablePicker()
->default('#ec4899'),
SelectProp::make('direction')
->label('Direction')
->options([
'to right' => 'Left → Right',
'to bottom' => 'Top → Bottom',
'to bottom right' => 'Diagonal',
])
->default('to right'),
];
}
public function toCss(array $values, string $selector): string
{
if (! filter_var($values['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
return '';
}
return $this->buildGradientBorderCss($values, $selector);
}
public function toCssForStateLayer(array $values, string $selector, string $state): string
{
if (! filter_var($values['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
return '';
}
// For state layers re-emit only the gradient; border-width is stable across states.
$colorStart = CssHelpers::resolveColorValue($values['color_start']);
$colorEnd = CssHelpers::resolveColorValue($values['color_end']);
$direction = $values['direction'];
$gradient = "linear-gradient({$direction}, {$colorStart}, {$colorEnd})";
return "{$selector} { background: {$gradient} border-box; }";
}
// ------------------------------------------------------------------
// Internals
// ------------------------------------------------------------------
private function buildGradientBorderCss(array $values, string $selector): string
{
$width = $values['width'] ?? '2';
$colorStart = CssHelpers::resolveColorValue($values['color_start']);
$colorEnd = CssHelpers::resolveColorValue($values['color_end']);
$direction = $values['direction'];
$gradient = "linear-gradient({$direction}, {$colorStart}, {$colorEnd})";
return implode(' ', [
"{$selector} {",
"border: {$width}px solid transparent;",
"background-image: {$gradient};",
"background-origin: border-box;",
"background-clip: padding-box, border-box;",
'}',
]);
}
}