PropTypes System (Advanced)
The term "PropType" is used in three related but distinct senses throughout the codebase and documentation. (1) The system/abstract layer: the PropTypeContract interface and AbstractPropType base class that define the shared contract all PropTypes fulfill. (2) The built-in PropTypes shipped by the package — TypographyPropType, SpacingPropType, BackgroundPropType, and so on — which implement that contract and are registered automatically. (3) Custom PropTypes created by developers to extend the system with application-specific design groups, registered via config or at runtime. When reading API docs or error messages, check which sense is meant.
A PropType is a reusable group of design fields that together produce a cohesive chunk of CSS. Where a single Prop represents one field (a color picker, a number input, a toggle), a PropType wraps a related set of Prop instances under a named key, exposes them as a panel section in the drawer, stores its values under a namespaced key in the node payload, and knows how to convert those stored values back into CSS at render time. The canonical example is TypographyPropType: it groups font-family, font-size, font-weight, line-height, letter-spacing, text-transform, text-align, and color into a single resettable block that applies consistently across every module that declares it.
PropTypes power three things:
- Properties panel sections — each PropType becomes a collapsible section in the builder drawer, scoped to its tab.
- Defaults and reset —
defaults()defines the baseline values used when a module is first dropped onto the canvas, and reset buttons restore to these values. - CSS generation — at render time, the registry walks every PropType declared on a node and calls
toCss()ortoCssWithSelectors()to build the scoped style block for that element.
Registry
The PropTypeRegistry is the central authority for all PropType operations. It is bound as a singleton in the Tagixo service container:
$registry = app(\Ccast\Tagixo\Core\PropTypeRegistry::class);
Responsibilities:
- Register and resolve PropTypes by key.
- Generate CSS from a node's stored props, iterating over every active PropType and accumulating output.
- Serialize metadata to the frontend — the registry converts registered PropTypes into a JSON manifest that the builder Vue app uses to render the correct drawer controls and know which keys are design keys vs. content keys.
Every PropType must be registered before it can be referenced by a module definition. Built-in PropTypes are registered during the package's boot() phase. Custom PropTypes registered via config are registered immediately after; those registered programmatically (e.g. in a plugin's boot()) follow the same order.
Ordering matters for CSS generation. The registry generates CSS in registration order for each node. If two PropTypes emit conflicting properties (e.g. both emit
filter:), the one registered later wins. Register custom PropTypes after the defaults to predictably override built-ins.
Contract and Base Class
Every PropType implements Ccast\Tagixo\Core\PropTypes\PropTypeContract. The interface is intentionally minimal — the contract is about behavior, not internal structure. Always extend AbstractPropType instead of implementing the interface directly; the base class provides sensible defaults for optional methods and handles internal wiring with the registry.
namespace App\Tagixo\PropTypes;
use Ccast\Tagixo\Core\PropTypes\AbstractPropType;
class GlassmorphismPropType extends AbstractPropType
{
// Required: key(), label(), tab(), schema()
// Optional: hasCss(), toCss(), toCssWithSelectors(), defaults(), features()
}
Method Reference
| Method | Signature | Required | Description |
|---|---|---|---|
key() |
(): string |
yes | Unique string identifier. Used as the storage key under props. Never change in production. |
label() |
(): string |
yes | Human-readable label shown as the panel section heading. Use __() for translatability. |
tab() |
(): string |
yes | Target tab in the drawer: 'content', 'design', or 'advanced'. |
schema() |
(): array |
yes | Array of Prop instances that define the fields for this PropType. |
hasCss() |
(): bool |
no | Return true if this PropType generates CSS. Default: false. |
toCss() |
(array $values): string |
no | Generate CSS declarations for the element's own selector. Return empty string to emit nothing. |
toCssWithSelectors() |
(array $values, string $selector): array |
no | Return an associative array of ['selector' => 'css declarations'] for nested rules. |
defaults() |
(): array |
no | Baseline values used on first drop and for reset. Must mirror the keys in schema(). |
features() |
(): array |
no | Feature flags passed to the frontend component as extra context. Rarely needed for custom PropTypes. |
vueComponent() |
(): ?string |
no | Custom Vue component name. Internal — built-in PropTypes only. Do not use in custom PropTypes; schema-driven rendering handles all standard field types. |
Never override
vueComponent()in a custom PropType. The method exists for the handful of built-in PropTypes that pre-date the schema-driven renderer (e.g.BackgroundPropType). For everything else, defineschema()and rely on the generic drawer renderer — it is maintained, tested, and consistent with the rest of the UI.
Storage Format
PropType values live inside the node's _styles object, not inside props. The props key holds content/data (text, URLs, media IDs); _styles holds all design values, keyed by PropType key and optionally by state.
Default State
{
"_styles": {
"spacing": {
"padding": { "top": "16px", "right": "16px", "bottom": "16px", "left": "16px", "linked": true },
"margin": { "top": "", "right": "auto", "bottom": "", "left": "auto", "linked": false }
},
"border": {
"width": "1px",
"style": "solid",
"color": "#e2e8f0",
"radius": { "topLeft": "8px", "topRight": "8px", "bottomRight": "8px", "bottomLeft": "8px", "linked": true }
}
}
}
State Namespacing
Hover, mobile, and other named states are stored as sibling keys inside _styles, each containing the same PropType keys with only the overridden values:
{
"_styles": {
"border": {
"color": "#e2e8f0"
},
"_hover": {
"border": {
"color": "#94a3b8"
}
},
"_mobile": {
"spacing": {
"padding": { "top": "8px", "right": "8px", "bottom": "8px", "left": "8px", "linked": true }
}
}
}
}
At render time the CSS generator emits the default state first, then each named state wrapped in its appropriate selector (:hover for hover, a breakpoint media query for responsive states). State values are sparse — only what differs from the default needs to be stored.
JSON stability is sacred. The
key()you return is the storage key for every page ever saved with this PropType. Renaming a key after you go to production silently drops all saved values for that PropType. If you must rename, write a migration that walkstgx_pages,tgx_layouts, andtgx_global_variablesJSON columns and renames the key in every stored payload.
toCss() vs toCssWithSelectors()
Use toCss(array $values): string when the PropType styles the element itself. The return value is a block of CSS declarations (no selector, no braces) that the CSS generator wraps with the node's unique scoped selector.
public function toCss(array $values): string
{
$lines = [];
if ($opacity = ($values['opacity'] ?? null)) {
$lines[] = "opacity: {$opacity};";
}
return implode(' ', $lines);
}
Use toCssWithSelectors(array $values, string $selector): array when the PropType needs to target nested elements. Return an associative array where keys are CSS selectors and values are declaration blocks. The $selector argument is the node's own scoped selector — use it as a prefix.
public function toCssWithSelectors(array $values, string $selector): array
{
$rules = [];
if ($headingColor = ($values['h1_color'] ?? null)) {
$rules["{$selector} h1"] = "color: {$headingColor};";
}
if ($linkColor = ($values['link_color'] ?? null)) {
$rules["{$selector} a"] = "color: {$linkColor};";
$rules["{$selector} a:hover"] = "color: {$linkColor}; opacity: 0.8;";
}
return $rules;
}
Both methods can coexist on the same PropType — implement toCss() for declarations that apply to the element itself and toCssWithSelectors() for nested rules. The CSS generator calls both and merges the output.
State-Aware CSS Generation
PropTypes do not need to implement state-awareness themselves. The CSS generator handles it automatically: it calls toCss() / toCssWithSelectors() once for the default state values, then again for each named state with that state's sparse values merged over the defaults, wrapping the output in the appropriate CSS selector or media query.
For hover:
[data-vb-id="abc123"] { color: #1e293b; }
[data-vb-id="abc123"]:hover { color: #0f172a; }
For mobile (below the configured breakpoint):
[data-vb-id="abc123"] { padding: 16px; }
@media (max-width: 768px) {
[data-vb-id="abc123"] { padding: 8px; }
}
Your toCss() receives whichever values are active for the current pass — default values for the default pass, merged hover values for the hover pass. No branching needed inside toCss().
Do not hard-code
:hoveror media queries insidetoCss(). Emitting raw:hoverselectors fromtoCss()bypasses the state system and will not merge with user-configured state transitions. Always return plain declarations and let the generator apply selectors.
Sub-Element PropTypes
A sub-element is a named part of a module that can be styled independently — for example, the label, icon, and description of a feature card, or the image and caption of a media module. Sub-elements are declared in the module class via subElements() and each sub-element stores its own _styles block.
PropTypes assigned to sub-elements work identically to those on the parent node. The CSS generator scopes the output to the sub-element's own selector (usually [data-vb-id="abc123"] [data-vb-sub="label"]) and uses the same toCss() / toCssWithSelectors() methods.
To indicate that a PropType is intended for sub-element use (rather than the main element), no special marking is needed — any registered PropType can be used on any sub-element. However, some PropTypes like SizingPropType are meaningful on most elements, while others like SectionDividerPropType are not. Pick deliberately.
// In your module definition
public function subElements(): array
{
return [
SubElement::make('label')
->label(__('Label'))
->design('typography', 'spacing', 'border'),
SubElement::make('icon')
->label(__('Icon'))
->design('sizing', 'filter'),
];
}
Built-In PropType Catalog
The package ships 22 PropTypes out of the box, grouped by the tab they target in the drawer. Compose them per module via ->design(...) or ->content(...) in your module definition. The 18 general-purpose ones are below; four context-specific ones (form and slider) follow.
Content Tab
| PropType class | Key | CSS | What it provides |
|---|---|---|---|
LinkPropType |
link |
no | URL, new-tab toggle, rel attribute, link title |
AdminPropType |
admin |
no | Editorial label visible only in the canvas tree — not rendered |
LayoutPropType |
layout |
no | Structural layout options specific to section/row/column containers |
Design Tab
TypographyPropType (typography) — 5 nested tabs (text, link, ul, ol, quote), each offering: font-family (Google Fonts or system stack), font-size (px/rem/em), font-weight, line-height, letter-spacing, text-transform, text-align, color. Generates base typography declarations on the element plus nested rules for a, ul li, ol li, blockquote.
HeadingTypographyPropType (heading_typography) — Per-level cascade for H1–H6. Each level inherits from the level above if left unset, so you can define H1 and only override H2 where needed. Generates h1, h2, …, h6 nested rules via toCssWithSelectors().
SpacingPropType (spacing) — 4-way padding and margin using FourWaysProp with linked/unlinked toggle. Accepts any CSS length unit. Generates padding and margin shorthand or longhand depending on whether sides are linked.
BackgroundPropType (background) — 6 composable layers: solid color, gradient (linear/radial with color stops), image (with position/size/repeat), video (URL + fallback poster), pattern (repeating SVG tile), shape-mask (SVG clip path). Multiple layers compose with mix-blend-mode. This is the most complex built-in PropType; its drawer uses a custom Vue component.
BorderPropType (border) — Border width (linked or per-side), style (solid/dashed/dotted/none), color, and corner radius (linked or per-corner). Generates border, border-radius.
SizingPropType (sizing) — Width and height (each with min/max variants), aspect-ratio, object-fit. Useful on image sub-elements and constrained wrappers.
BoxShadowPropType (box_shadow) — Unlimited shadow layers. Each layer: x-offset, y-offset, blur, spread, color (with opacity), inset toggle. Generates a single box-shadow declaration with comma-separated values.
FilterPropType (filter) — CSS filter property controls: brightness, contrast, saturation, hue-rotate, blur, sepia, grayscale, invert. Generates filter: brightness(…) contrast(…) … combining only enabled adjustments.
TransformPropType (transform) — Translate (x/y), rotate, scale (x/y), skew (x/y), with transform-origin. Generates transform: translate(…) rotate(…) scale(…) skew(…).
AnimationPropType (animation) — Entry animation triggered by IntersectionObserver when the element scrolls into view. Presets: fade, slide-up/down/left/right, zoom-in/out. Controls: duration, delay, easing, stagger (for children). Does not use CSS keyframe animation directly — the JS driver manages class toggling.
TextAnimationPropType (text_animation) — Text-specific motion effects: gradient shimmer, typewriter (with optional blinking cursor), reveal in sequence (word or line), rotating words (cycles a list in place of a {word} marker). Emits a data-vb-text-animate attribute; the JS driver in frontend.js handles all rendering. No CSS is generated by this PropType. Page context only — gated out of mail, PDF, and slider contexts.
SectionDividerPropType (section_divider) — SVG shape divider at the top and/or bottom of a section. Shape presets include wave, curve, triangle, tilt, zigzag. Controls: height, color, flip (mirror), layering (above/below content).
Advanced Tab
| PropType class | Key | What it provides |
|---|---|---|
CssSelectorsPropType |
css_selectors |
Custom CSS id and additional class attributes on the node wrapper |
CustomCssPropType |
custom_css |
Raw CSS textarea with .this-element as a placeholder for the node's own scoped selector |
VisibilityPropType |
visibility |
Per-breakpoint hide toggles and per-axis overflow control |
DisplayPropType |
display |
Position mode (static/relative/absolute/fixed/sticky), top/right/bottom/left offsets, z-index |
StateTransitionPropType |
state_transition |
CSS transition to smooth property changes between states. Controls: enabled, duration (ms), delay (ms), timing function. Emits transition: all …. Not an entry animation — only animates the delta between two states already rendered. |
Context-Specific (Form and Slider)
| PropType class | Key | Tab | Context |
|---|---|---|---|
FormLayoutPropType |
grid |
design | Form builder only — column span and grid placement for form fields |
SubmitPropType |
submit |
design | Form builder only — submit button label, loading state, success/error behavior |
CarouselPropType |
carousel |
content | Slider builder only — slides collection, autoplay, navigation arrows, pagination dots |
TransitionPropType |
transition |
content | Slider builder only — slide transition type, duration, easing |
A module enables whichever set fits its semantics:
->design('typography', 'spacing', 'border', 'box_shadow', 'animation')
PropTypes not listed simply do not appear in the drawer for that module.
Registration
Config-Based (Recommended)
Add your PropType class to config/tagixo.php. The package resolves and registers it during boot, before any module definitions are processed:
// config/tagixo.php
return [
'prop_types' => [
App\Tagixo\PropTypes\GlassmorphismPropType::class,
App\Tagixo\PropTypes\AudienceTargetPropType::class,
],
];
Programmatic (Plugin Boot)
Register from a service provider or plugin boot() method when you need to resolve dependencies first or when you are distributing a package:
use Ccast\Tagixo\Facades\Tagixo;
public function boot(): void
{
Tagixo::registerPropType(new GlassmorphismPropType());
}
Both methods are equivalent in effect. Config-based registration is preferable for app-level PropTypes because it is declarative and automatically included in the registry's metadata export.
Custom PropType Tutorial: GlassmorphismPropType
This example builds a production-ready custom PropType that generates backdrop-filter + semi-transparent background CSS — the glassmorphism effect. It covers schema definition, CSS generation, defaults, and registration end to end.
The Class
<?php
namespace App\Tagixo\PropTypes;
use Ccast\Tagixo\Core\PropTypes\AbstractPropType;
use Ccast\Tagixo\Core\Props\ColorProp;
use Ccast\Tagixo\Core\Props\NumberProp;
use Ccast\Tagixo\Core\Props\ToggleProp;
class GlassmorphismPropType extends AbstractPropType
{
public function key(): string
{
return 'glassmorphism';
}
public function label(): string
{
return __('Glassmorphism');
}
public function tab(): string
{
return 'design';
}
public function hasCss(): bool
{
return true;
}
public function schema(): array
{
return [
ToggleProp::make('enabled')
->label(__('Enable'))
->default(false),
NumberProp::make('blur')
->label(__('Blur (px)'))
->min(0)
->max(40)
->step(1)
->default(12)
->unit('px'),
ColorProp::make('tint_color')
->label(__('Tint color'))
->default('#ffffff'),
NumberProp::make('tint_opacity')
->label(__('Tint opacity (%)'))
->min(0)
->max(100)
->step(1)
->default(20),
NumberProp::make('saturation')
->label(__('Saturation (%)'))
->min(0)
->max(300)
->step(10)
->default(100),
NumberProp::make('border_opacity')
->label(__('Border opacity (%)'))
->min(0)
->max(100)
->step(1)
->default(30),
];
}
public function defaults(): array
{
return [
'enabled' => false,
'blur' => 12,
'tint_color' => '#ffffff',
'tint_opacity' => 20,
'saturation' => 100,
'border_opacity' => 30,
];
}
public function toCss(array $values): string
{
if (! ($values['enabled'] ?? false)) {
return '';
}
$blur = max(0, (int) ($values['blur'] ?? 12));
$tintColor = $values['tint_color'] ?? '#ffffff';
$tintOpacity = max(0, min(100, (int) ($values['tint_opacity'] ?? 20)));
$saturation = max(0, min(300, (int) ($values['saturation'] ?? 100)));
$borderOpacity = max(0, min(100, (int) ($values['border_opacity'] ?? 30)));
// Convert hex color to rgba for the background
$bg = $this->hexToRgba($tintColor, $tintOpacity / 100);
// Parse border color at reduced opacity for the subtle glass edge
$border = $this->hexToRgba($tintColor, $borderOpacity / 100);
return implode(' ', [
"backdrop-filter: blur({$blur}px) saturate({$saturation}%);",
"-webkit-backdrop-filter: blur({$blur}px) saturate({$saturation}%);",
"background-color: {$bg};",
"border: 1px solid {$border};",
]);
}
private function hexToRgba(string $hex, float $alpha): string
{
$hex = ltrim($hex, '#');
if (strlen($hex) === 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
return "rgba({$r}, {$g}, {$b}, {$alpha})";
}
}
Registration
// config/tagixo.php
'prop_types' => [
App\Tagixo\PropTypes\GlassmorphismPropType::class,
],
Using it in a Module
public function design(): array
{
return ['background', 'glassmorphism', 'border', 'spacing', 'box_shadow'];
}
When the user enables the PropType in the drawer, the generated CSS block for that node will include the backdrop-filter and background declarations. The border PropType's own border declarations will render after, giving the user the option to override the glassmorphism border independently.
Stored Payload
{
"_styles": {
"glassmorphism": {
"enabled": true,
"blur": 16,
"tint_color": "#ffffff",
"tint_opacity": 15,
"saturation": 150,
"border_opacity": 25
}
}
}
When to Create a PropType Instead of a Prop
| Situation | Use |
|---|---|
| Single field needed on one module | Prop added to schema() in the module definition |
| Same group of fields reused across 2+ modules | Custom PropType |
| The fields together produce CSS | Custom PropType with hasCss(): true |
| A panel section with reset behavior | Custom PropType — resets are PropType-scoped |
| Just one field but it needs CSS | Either — a schema-only PropType with one Prop is valid |
Common Pitfalls
Duplicate keys — Two registered PropTypes with the same key() cause the second to silently overwrite the first in the registry. Registration order is the only thing that determines which one survives. Make keys specific: prefer acme_glassmorphism over glassmorphism in distributed packages.
Key mutation after production — Changing key() renames the storage key. Every page that had saved values for the old key will silently revert to defaults. There is no automatic migration. Treat PropType keys as permanent identifiers.
Missing defaults — If defaults() does not cover every key returned by schema(), the drawer may render controls without initial values, which can cause null to reach toCss(). Always keep defaults() in sync with schema().
Emitting selectors inside toCss() — Returning a string like ":hover { color: red; }" from toCss() bypasses the state system. The CSS generator wraps toCss() output with the node's own scoped selector; adding raw selectors inside creates invalid or double-scoped rules. Use toCssWithSelectors() for nested rules and never emit :hover manually.
Mixing content data with design data — _styles is for visual presentation only. If a value affects business logic (e.g. which variant of a component to render), it belongs in props as a regular Prop on the module, not inside a PropType.
Schema/data shape mismatch — If you add a new field to schema() on an existing PropType, existing saved payloads will not have that key. Your toCss() must use ?? default null coalescing throughout and never assume a key is present.
Overriding vueComponent() — This method is for internal built-in PropTypes with hand-crafted Vue drawers. Custom PropTypes that override it will reference a Vue component that does not exist in the consumer app's build, resulting in a broken drawer section. Use schema() instead.
See Also
- Props System — individual field types available for
schema() - Module System and Catalog — how modules declare PropTypes via
design() - Sub-Elements and States — sub-element declarations and state-scoped styling
- Storage Model and Data Shape — full payload structure for pages and layouts
- Custom Prop Types — additional examples and patterns
- Contexts and Builders — how context (page/form/mail/pdf) gates which PropTypes render