All widgets implement Crumbls\Layup\Contracts\Widget. This interface defines the full API that the builder, registry, and renderer depend on. The interface is rendering-agnostic -- it specifies what a widget is and how it's edited, not how it produces HTML on the frontend. Two base classes implement the interface in concrete form:
Crumbls\Layup\View\BaseBladeWidget-- renders through a Blade view component (the default).Crumbls\Layup\View\BaseLivewireWidget-- renders through a Livewire component (opt-in, requireslivewire/livewire).
Crumbls\Layup\View\BaseWidget is an abstract alias for BaseBladeWidget retained for backwards compatibility. New code may extend either base directly. See Livewire-rendered widgets for when each base applies.
Interface methods
namespace Crumbls\Layup\Contracts;
use Crumbls\Layup\Support\WidgetContext;
interface Widget
{
// Identity
public static function getType(): string;
public static function getLabel(): string;
public static function getIcon(): string;
public static function getCategory(): string;
// Form & data
public static function getFormSchema(): array;
public static function getDefaultData(): array;
public static function getValidationRules(): array;
// Rendering
public static function getPreview(array $data): string;
public static function prepareForRender(array $data): array;
// Discovery
public static function getSearchTerms(): array;
public static function isDeprecated(): bool;
public static function getDeprecationMessage(): string;
// Lifecycle
public static function onSave(array $data, ?WidgetContext $context = null): array;
public static function onCreate(array $data, ?WidgetContext $context = null): array;
public static function onDelete(array $data, ?WidgetContext $context = null): void;
public static function onDuplicate(array $data, ?WidgetContext $context = null): array;
// Assets & serialization
public static function getAssets(): array;
public static function toArray(): array;
}
The interface deliberately omits render(). Each base class adds it with the appropriate return type (View|Htmlable|string for Blade, string for Livewire). The frontend call site stringifies whatever comes back, so any compliant return value works.
BaseWidget / BaseBladeWidget
Crumbls\Layup\View\BaseBladeWidget provides the default Blade rendering path. Custom widgets may extend it directly, or extend BaseWidget (an abstract alias retained for backwards compatibility -- existing widgets and stubs need no changes).
Minimum required overrides:
getType()-- unique string identifiergetLabel()-- display name
Common overrides:
getContentFormSchema()-- content tab fieldsgetDefaultData()-- initial widget datagetPreview()-- canvas preview text
Render resolution:
- Default:
view('layup::components.{type}'). The{type}segment comes fromgetType(). - Override
getViewName()to point at a different view name.
BaseLivewireWidget
Crumbls\Layup\View\BaseLivewireWidget renders through a Livewire component instead of a Blade view component. It implements the same Widget contract as BaseBladeWidget and shares the same metadata defaults via the HasWidgetDefaults trait.
Minimum required overrides:
getType()-- unique string identifiergetLabel()-- display namegetLivewireComponent()-- Livewire alias (kebab-case) or fully-qualified class name
Render mechanism:
The base mounts the named Livewire component via <livewire:dynamic-component>, passing the widget's $data as a data prop and the recursively-rendered children as the default slot. See Livewire-rendered widgets for the full architecture, children-handling model, and migration patterns.
HasWidgetDefaults trait
Crumbls\Layup\View\Concerns\HasWidgetDefaults holds the default implementations of every Widget interface method that doesn't depend on rendering tech: getIcon(), getCategory(), getDefaultData(), getPreview(), prepareForRender(), getValidationRules(), getSearchTerms(), isDeprecated(), getDeprecationMessage(), onSave(), onCreate(), onDelete(), onDuplicate(), getAssets(), toArray(). Used by both BaseBladeWidget and BaseLivewireWidget. You will not normally use this trait directly -- it's an implementation detail.
Identity traits
Crumbls\Layup\View\Concerns\Identity\ ships per-widget identity traits for select widgets that may have multiple render flavours. Each trait holds the static metadata for a widget -- type, label, icon, category, form schema, defaults, preview -- with no rendering logic. The traits are consumed by both the built-in Blade widget class and any downstream Livewire flavour:
class HeadingWidget extends BaseWidget
{
use HeadingIdentity; // sets type, label, icon, category, form, defaults, preview
}
// Downstream package, same trait, different render path:
class HeadingLivewireWidget extends BaseLivewireWidget
{
use HeadingIdentity;
public static function getLivewireComponent(): string
{
return 'my-package.heading';
}
}
Identity traits currently shipped:
HeadingIdentityButtonIdentityNumberCounterIdentityNewsletterIdentity
For one-off widgets that will only ever ship in one render flavour, define the static methods directly on the widget class -- the trait pattern is overkill.
BaseView
Crumbls\Layup\View\BaseView is the parent of BaseBladeWidget, BaseLivewireWidget, Row, Column, and Section. It provides:
- The three-tab form structure (Content, Design, Advanced)
- Shared Design tab fields (colors, borders, spacing, shadows, opacity)
- Shared Advanced tab fields (ID, classes, animations, visibility)
$dataand$childrenstorage with fluent constructors and child-management methods- Static helper methods:
// Build Tailwind classes for responsive hiding
BaseView::visibilityClasses(['sm', 'lg']);
// Build inline CSS string from design data
BaseView::buildInlineStyles($data);
// Build Alpine.js animation attributes
BaseView::animationAttributes($data);
- Instance helper used by Livewire-rendered widgets and any custom render path:
// Recursively render children to a single HTML string
$widget->renderChildrenToHtml();
- The abstract
render()method whose return type isView|Htmlable|string. Subclasses may narrow the return type (Section/Row/Column returnView;BaseBladeWidgetreturnsView|Htmlable|string;BaseLivewireWidgetreturnsstring). The wide parent type lets both rendering paths satisfy the contract while preserving covariance.
Type-checking widgets at runtime
When code inspects widgets at runtime (custom registries, content walkers, exporters, test assertions), prefer instanceof Widget (the interface) over instanceof BaseWidget (a class). The interface matches both Blade and Livewire bases; the class only matches the Blade lineage. Layup's own internals (RegistersWidgets, LayupContent, LayupAssertions, WidgetDefaultCompletenessTest) were switched to interface-based checks alongside the introduction of BaseLivewireWidget so that all rendering paths are equally discoverable.
Contributors
Thank you to everyone who has contributed to this package. Every pull request, bug report, and idea makes a difference.