Tagixo Docs

Developer Documentation for Laravel, SDK integrations, and extensibility

Extending Tagixo

Sub-elements and States

Style internal parts of modules explicitly, including repeatable elements and state-specific selectors.

Sub-elements and States

Sub-elements let a module expose stylable internal parts without relying on undocumented CSS selectors.

This is one of the most important features for high-quality custom modules.

Core concept

A module can declare internal elements such as:

  • title
  • description
  • button
  • badge
  • wrapper
  • repeater items

These elements are declared through subElements(...) and serialized into the builder metadata.

Value object

Core class:

  • Ccast\Tagixo\Core\SubElement

Main factories:

  • SubElement::structural(...)
  • SubElement::repeatable(...)

What sub-elements can describe

A sub-element definition can include:

  • key
  • label
  • selector
  • design prop groups
  • advanced prop groups
  • linked content field keys
  • repeater key
  • per-item styles
  • state definitions
  • defaults

Why use them

Without sub-elements, a module has only one styling surface: the root node.

That is not enough for modules such as:

  • tabs
  • accordions
  • pricing tables
  • cards with badges and actions
  • counters with multiple text layers
  • form fields with label, input, helper, and error parts

Structural sub-element example

use Ccast\Tagixo\Core\SubElement;

ModuleDefinition::make()->subElements(
    SubElement::structural(
        key: 'title',
        label: __('Title'),
        selector: '.alert-box__title',
        styles: ['typography', 'spacing']
    ),
    SubElement::structural(
        key: 'body',
        label: __('Body'),
        selector: '.alert-box__body',
        styles: ['typography', 'spacing']
    ),
);

Repeatable sub-element example

Repeatable sub-elements are for structures backed by repeaters:

SubElement::repeatable(
    key: 'tab',
    label: __('Tab'),
    selector: '.tabs__trigger',
    styles: ['typography', 'spacing', 'border'],
    repeaterKey: 'tabs',
    itemStyles: ['typography', 'spacing', 'background', 'border']
)

The repeaterKey must match the real content schema. If it does not, the drawer will expose configuration that cannot map back to the saved payload correctly.

States

Sub-elements can also expose state-specific styling, such as:

  • active
  • hover
  • selected
  • current

A state is declared inside states[] on the SubElement and contains four keys:

Key Meaning
label Human-readable name shown in the drawer (e.g. "Active state")
elementKey Internal key under which the state's PropType values are persisted in the saved JSON. Use a stable kebab/snake identifier — once content exists in production, do not rename it without a migration.
selector CSS selector applied to the base selector when this state is active (e.g. .is-active, [aria-current="page"], .vb-tab-button--active). The renderer concatenates it onto the parent selector at CSS generation time.
designProps The list of PropType keys allowed when styling this state. Usually a SUBSET of the base sub-element's styles — you rarely want full design control on a hover/active state.

Example with explicit fields:

SubElement::repeatable(
    key: 'tab',
    label: __('Tab navigation'),
    selector: '.vb-tab-button',
    styles: ['typography', 'spacing', 'border'],
    repeaterKey: 'tabs',
    itemStyles: ['typography', 'spacing', 'background', 'border'],
    states: [
        'active' => [
            'label'       => __('Active state'),
            'elementKey'  => 'tab_active',
            'selector'    => '.vb-tab-button--active',
            'designProps' => ['typography', 'background', 'border'],
        ],
        'hover' => [
            'label'       => __('Hover state'),
            'elementKey'  => 'tab_hover',
            'selector'    => ':hover',
            'designProps' => ['typography', 'background'],
        ],
    ],
)

Linked elements

If two sub-elements should share their style buffer (i.e., one edits affects both, no duplicate state on the saved JSON), declare them via linked. This is useful for, e.g., a "title in card" sub-element that should remain consistent with a "title in detail view" sub-element.

SubElement::structural(
    key: 'card_title',
    label: __('Card title'),
    selector: '.vb-card .vb-title',
    styles: ['typography'],
    linked: 'detail_title',  // shares props with the 'detail_title' sub-element
)

The drawer renders the secondary entry as a read-only mirror with a "linked to..." label, so editors understand they're sharing styles.

Per-item style overrides on repeatables

For SubElement::repeatable(...), the standard itemStyles array describes which PropTypes apply to each repeater item uniformly. Sometimes a single item should override (one specific tab styled differently). Tagixo exposes perItemStyles for this:

SubElement::repeatable(
    key: 'plan',
    label: __('Plan card'),
    selector: '.vb-plan',
    styles: ['typography', 'border'],
    repeaterKey: 'plans',
    itemStyles: ['typography', 'border', 'background'],
    perItemStyles: true,        // enables per-item overrides in the drawer
)

When enabled, the drawer adds a "Override per item" toggle next to each repeater item. Override values are stored under perItemStyles.{itemId}.{propTypeKey} in the content payload; the renderer applies them via [data-vb-id="{itemId}"] selectors automatically.

This pairs well with pricing tables, comparison matrices, and any "spotlight the recommended choice" pattern.

Defaults on sub-elements

Most sub-elements don't need explicit defaults — the user starts with the parent module's design propagation. When you DO want a sub-element to start with a baked-in look (e.g., a badge that should always have a soft drop-shadow until the editor overrides), declare them inline:

SubElement::structural(
    key: 'badge',
    label: __('Badge'),
    selector: '.vb-badge',
    styles: ['typography', 'background', 'border', 'box_shadow'],
    defaults: [
        'box_shadow' => DefaultsBuilder::boxShadow()->subtle(),
        'background' => DefaultsBuilder::background()->color('#fff7ed'),
    ],
)

The defaults seed the saved JSON when a user first inserts the module, but never re-apply on subsequent renders — editor overrides win.

How the builder uses them

During metadata serialization, sub-elements are exposed to the frontend as part of the component's PropType payload.

That allows the drawer to:

  • show internal element targets
  • drill down into repeatable items
  • apply style groups to the right selector
  • keep state-specific styling explicit

Selector strategy

Selectors must be:

  • stable
  • intentional
  • scoped to the module

Avoid generic selectors such as .title or .button.

Prefer selectors such as:

  • .pricing-card__title
  • .tabs__trigger
  • .hero-banner__cta

If selector quality is poor, the sub-element API becomes dangerous.

Best practices

  1. Design the final HTML selectors before exposing sub-elements.
  2. Keep keys stable once content exists in production.
  3. Link content fields when contextual editing should target a specific element.
  4. Define defaults for any stylable element that needs a product baseline.
  5. Test repeaters and states in save, reload, preview, and public render flows.

Pricing table example

The pricing table module is a concrete example of when sub-elements are worth the extra surface area.

It exposes internal parts such as:

  • card shell
  • featured card state
  • plan name
  • current price
  • original price
  • discount note
  • badge
  • CTA button
  • comparison title
  • comparison table
  • comparison labels and values

This matters because a pricing module usually evolves faster than simpler content blocks:

  • launch promos add crossed-out prices
  • "most popular" badges need independent styling
  • comparison matrices introduce table-level and cell-level selectors

For this kind of module, use explicit selectors and separate content keys instead of trying to infer meaning from a single text field.

Example decisions that keep the contract stable:

  • originalPrice is separate from price
  • discountExpiresText is separate from badge
  • comparison rows map values by plan index instead of storing arbitrary nested HTML

That separation keeps the editor predictable and makes homepage-specific overrides much safer.

Common mistakes

  • generic selectors that style unrelated nodes
  • repeater keys that do not match the actual content schema
  • undocumented state selectors
  • exposing too many sub-elements for simple modules
  • changing element keys after editors have already styled them

Sub-elements are powerful, but only when your module HTML contract is stable enough to support them.