Props & PropTypes

PropTypes System

Build reusable style/content groups that define UI schema and generate optional CSS output.

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:

  1. Properties panel sections — each PropType becomes a collapsible section in the builder drawer, scoped to its tab.
  2. Defaults and resetdefaults() defines the baseline values used when a module is first dropped onto the canvas, and reset buttons restore to these values.
  3. CSS generation — at render time, the registry walks every PropType declared on a node and calls toCss() or toCssWithSelectors() 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, define schema() 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 walks tgx_pages, tgx_layouts, and tgx_global_variables JSON 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 :hover or media queries inside toCss(). Emitting raw :hover selectors from toCss() 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