Skip to content

Using Raw Filament Components

FilamentCraft's setting types are a curated DSL — a stable, version-agnostic surface that compiles to Filament form components and wires each one into the live-preview bus. But the DSL is not a wall. Any Filament v4/v5 form component can be dropped straight into a section's settings() array, alongside (or instead of) the DSL types, with no registration, adapter, or wrapper.

This is the escape hatch: reach for it whenever you need a control the DSL doesn't ship — a Repeater, KeyValue, Tabs layout, Builder, or your own custom field.

The one rule

Return the component from settings() like any setting. That's it.

php
use Filament\Forms\Components\TextInput;
use FilamentCraft\Sections\AbstractSection;
use FilamentCraft\Sections\Data\SectionData;
use FilamentCraft\Settings\Types\Text;

final class HeroSection extends AbstractSection
{
    public static function settings(): array
    {
        return [
            Text::make('heading')->default('Hello'),     // FilamentCraft DSL
            TextInput::make('subheading')->default('World'), // raw Filament — both work
        ];
    }

    public static function renderToHtml(SectionData $data): string
    {
        return '<p>'.$data->settings->get('heading').' '.$data->settings->get('subheading').'</p>';
    }
}

Both values are read the same way in the view — $data->settings->get('id'). The compiler indexes raw components into the same field-descriptor map the DSL uses, so defaults, value resolution, and image routing all behave identically.

What you get for free

When you pass a raw component, the SchemaCompiler does the wiring for you:

Live preview is automatic

Every raw leaf Field is opted into live updates so the iframe refreshes as the user edits — you don't call ->live() yourself. The mode is chosen by widget kind:

  • Discrete, one-click widgetsToggle, Checkbox, Select, Radio, ToggleButtons, ColorPicker, FileUpload — get instant ->live(). They commit on the DOM change event, so a single click round-trips immediately.
  • Typing widgetsTextInput, Textarea, RichEditor — get ->live(onBlur: true), so a half-typed value doesn't morph the DOM out from under the cursor mid-keystroke.

If you set your own live mode, FilamentCraft leaves it alone:

php
TextInput::make('blurb')->live(debounce: 500); // preserved — not overridden

Nested layouts are walked

Layout containers (Grid, Fieldset, Section, …) are recursed into, so their leaf fields get live mode and contribute descriptors just like top-level fields:

php
use Filament\Schemas\Components\Grid;
use Filament\Forms\Components\TextInput;

Grid::make(2)->schema([
    TextInput::make('accent'),
    TextInput::make('background'),
]);

Repeater, Builder, and KeyValue are treated as leaf fields — their inner schemas are nested state, not top-level siblings, so they are not flattened into separate descriptors. The whole repeater value is stored under its own id:

php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;

Repeater::make('logos')->schema([
    TextInput::make('name'),
    TextInput::make('url')->url(),
]);
// $data->settings->get('logos') === [['name' => …, 'url' => …], …]

File uploads route correctly

A raw FileUpload works out of the box. Its disk, directory, visibility, and accepted types are read off the component; anything you omit falls back to your config('filamentcraft.uploads.*') defaults — the same routing the DSL Image type uses.

php
use Filament\Forms\Components\FileUpload;

// Explicit disk/dir:
FileUpload::make('logo')->image()->disk('s3')->directory('logos')->visibility('public');

// Omit them → falls back to filamentcraft.uploads config:
FileUpload::make('logo')->image();

The stored upload reads back as an ImageValueStringable (its string form is the public URL), with ->url, ->path, and the srcset() / small() / medium() / large() variant helpers — or null when nothing is uploaded:

blade
<img src="{{ $data->settings->get('logo') }}" />
{{-- or, explicitly: $data->settings->get('logo')?->url --}}

Grouping: keep using Group

The collapsible headers in the editor's settings panel are FilamentCraft sentinels — use Group (not Filament's Section) to open one. Everything after a Group — DSL setting or raw component — belongs to it until the next Group. Raw components mix freely inside groups:

php
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\FileUpload;
use FilamentCraft\Settings\Types\Group;
use FilamentCraft\Settings\Types\Text;

public static function settings(): array
{
    return [
        Group::make('content_group')->label('Content'),
        Text::make('heading'),                 // DSL
        TextInput::make('subtitle'),           // raw — same group

        Group::make('media_group')->label('Media')->collapsed(),
        FileUpload::make('hero_image')->image(), // raw — second group
    ];
}

When to use which

Use the DSL when…Reach for a raw component when…
You want the curated, version-stable APIYou need a control the DSL doesn't ship (Repeater, KeyValue, Tabs, Builder)
You want a structured value object — Spacing/Gradient ->css(), curated Font catalog, ColorSchemeYou're integrating a custom Filament field of your own
You want insulation from Filament v4 ↔ v5 API driftYou already know the exact Filament API you want

Most raw fields return plain values — with three exceptions

A raw TextInput gives you a plain string, and a Repeater / KeyValue a plain array. But three raw component types are recognised by the compiler and get the same value objects as their DSL twins: a raw FileUpload reads back as an ImageValue, a raw RichEditor as a RichTextValue, and a raw ColorPicker as a ColorValue. Everything else passes through untransformed. The remaining structured value objects — SpacingValue and GradientValue with their ->css() helpers, the curated Bunny Fonts catalog, color-scheme resolution — only come from their Setting types.

Gotchas

  • Ids must be unique across the whole section, exactly like DSL fields. The compiler throws a LogicException on a duplicate id (DSL or raw).
  • Use Group, not Section, for collapsible headers — a raw Filament Section won't slot into the editor's grouping model.
  • Value transforms apply to three raw types. FileUpload, RichEditor, and ColorPicker read back as ImageValue, RichTextValue, and ColorValue. For every other raw field (TextInput, Repeater, KeyValue, …) what Filament stores is what your view reads.

See also