Skip to main content
VoiceNoteRecorderField ← Back to Table of Contents

Summary

In-browser voice recorder with real-time frequency visualizer, inline playback (waveform + play/pause), and Filament FileUpload storage integration. Records audio from the microphone, previews locally, then uploads to Livewire temporary storage and persists to disk on form save.
ClassBjanczak\FilamentFlexFields\Filament\Forms\Components\VoiceNoteRecorderField
State typestring|null — stored path on disk after save; keyed TemporaryUploadedFile object during upload
FieldType— (use the PHP class directly; not mapped via FieldType)
ExtendsFlexFileUploadFilament\Forms\Components\FileUpload

Basic usage

Default — upload on form submit

Recording stays in the browser until the form is submitted. A loader is shown while the file uploads before save.
use Bjanczak\FilamentFlexFields\Filament\Forms\Components\VoiceNoteRecorderField;

VoiceNoteRecorderField::make('voice_note')
    ->label('Voice message')
    ->required()
    ->disk('public')
    ->directory('voice-notes')
    ->maxDuration(120);

Immediate upload after recording

Uploads to Livewire temporary storage right after recording stops. Delete removes the file from storage (when deleteFileOnRemove() is enabled — default in setUp()).
VoiceNoteRecorderField::make('voice_note')
    ->uploadImmediately()
    ->disk('public')
    ->directory('voice-notes');
Equivalent explicit defer:
VoiceNoteRecorderField::make('voice_note')
    ->uploadOnSubmit(); // default behaviour

Upload flow

StageWhat happens
RecordAudio captured in the browser (MediaRecorder); playback uses a local blob URL
JS upload$wire.upload('{statePath}.{uuid}', file) — file lands in Livewire temp (config/livewire.phptemporary_file_upload)
Form saveFilament beforeStateDehydratedsaveUploadedFiles() moves/stores the file to disk() + directory()
Persisted stateRelative path string, e.g. voice-notes/01H….webm
Temporary path is configured globally for all Livewire uploads (not per field):
// config/livewire.php
'temporary_file_upload' => [
    'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'),
    'directory' => 'livewire-tmp', // default when null
],
Final path is configured per field with inherited FileUpload API:
->disk('public')
->directory('voice-notes/recordings')
->visibility('public')
->moveFiles() // optional: move instead of copy when temp and target share the same disk

State format

PhaseState shapeExample
Emptynull
After JS upload (before save)Associative array keyed by UUID['a1b2…' => TemporaryUploadedFile]
After form dehydrate / savestring — path on disk()'voice-notes/01H….webm'
Existing record (edit)string pathLoaded for playback via getInitialAudioUrl()

Validation

RuleDetail
required()Recording must be present before submit
Inherited FileUploadmaxSize(), acceptedFileTypes(), etc.
Default accepted MIME types (set in setUp()): audio/*, audio/mpeg, audio/wav, audio/webm, audio/ogg, audio/x-m4a, audio/aac.

Configuration API

maxDuration(int|Closure $seconds)

Maximum recording length in seconds. Timer stops recording automatically. Default: 120 (2 minutes).
->maxDuration(30)

uploadImmediately(bool|Closure $condition = true)

Upload to Livewire temp storage immediately after recording. Playback stays visible; background upload progress is shown on the pill.
VoiceNoteRecorderField::make('field_name')
    ->uploadImmediately(true);

uploadOnSubmit(bool|Closure $condition = true)

Defer upload until form submit (default). Shows “Preparing voice note for save…” while uploading on submit.
VoiceNoteRecorderField::make('field_name')
    ->uploadOnSubmit(true);

Icon overrides

MethodDefault (Gravity UI)Config key (filament-flex-fields.ui)
playIcon()PlayFillaudio_play_icon
pauseIcon()PauseFillaudio_pause_icon
microphoneIcon()Microphonemicrophone_icon
stopIcon()Minusstop_icon
trashIcon()TrashBintrash_icon
checkmarkIcon()Checkcheckmark_icon
VoiceNoteRecorderField::make('voice_note')
    ->microphoneIcon('heroicon-o-microphone')
    ->maxDuration(60)
    ->size('lg');

size(string|ControlSize|Closure $size)

sm, md, lg. Inherited from FlexFileUpload / HasControlSize.
VoiceNoteRecorderField::make('field_name')
    ->size('md');

focusOutline(bool|Closure $condition = true)

Inherited from HasFieldFocusOutline via FlexFileUpload. Default: false. Adds the shared focus ring on the recorder shell when enabled.
VoiceNoteRecorderField::make('voice_note')->focusOutline();

Inherited FileUpload API

VoiceNoteRecorderField uses the standard Filament file upload pipeline (not FilePond UI). Common options:
MethodDescription
disk(string|Closure|null $name)Target filesystem disk for persisted files
directory(string|Closure|null $directory)Subdirectory on that disk
visibility(string|Closure|null $visibility)public or private
maxSize(int|Closure|null $size)Max file size in KB
required() / nullable()Validation
deleteFileOnRemove()Remove file from disk when user deletes recording (enabled by default)
storeFileNamesIn(string|Closure|null $path)Sibling state for original filenames
preserveFilenames() / moveFiles()See FlexFileUpload
acceptedFileTypes(array|Closure $types)Override MIME whitelist (defaults set in setUp())
imageEditor(bool|Closure $condition = true)Not used by voice UI — leave disabled (default)
multiple(bool|Closure $condition = true)Single recording per field — keep multiple(false)
maxFiles(int|Closure $max)Typically 1 for a single voice note
openable() / downloadable() / previewable()FileUpload flags; voice field uses custom playback UI
VoiceNoteRecorderField::make('voice_note')
    ->disk('s3')
    ->directory('support/voice-notes')
    ->visibility('private')
    ->maxSize(5120) // KB
    ->acceptedFileTypes(['audio/webm', 'audio/mp4', 'audio/mpeg'])
    ->maxFiles(1);

Flex fields context

There is no FieldType enum mapping for VoiceNoteRecorderField. Use the PHP class directly in Filament schemas, or register a custom flex-field handler if you need schema-driven forms.

Model & persistence

// Migration
$table->string('voice_note_path')->nullable();

// Model
protected $fillable = ['voice_note_path'];
Optional URL accessor for playback outside the form:
public function getVoiceNoteUrlAttribute(): ?string
{
    if (blank($this->voice_note_path)) {
        return null;
    }

    return Storage::disk('public')->url($this->voice_note_path);
}
Edit form with existing recording:
VoiceNoteRecorderField::make('voice_note_path')
    ->label('Voice note')
    ->disk('public')
    ->directory('voice-notes')
    ->default(fn (?Ticket $record): ?string => $record?->voice_note_path);
// getInitialAudioUrl() resolves playback URL from persisted path on load

Recipes

Support ticket voice note — deferred upload

VoiceNoteRecorderField::make('voice_note_path')
    ->label('Describe the issue')
    ->disk('public')
    ->directory('tickets/voice-notes')
    ->maxDuration(120)
    ->required()
    ->helperText('Record up to 2 minutes. Upload runs when you submit the form.');

Immediate upload — feedback widget

VoiceNoteRecorderField::make('feedback_audio')
    ->label('Voice feedback')
    ->uploadImmediately()
    ->disk('public')
    ->directory('feedback')
    ->maxDuration(60);

Edit form — existing file playback

VoiceNoteRecorderField::make('voice_note_path')
    ->label('Voice message')
    ->default(fn (?Message $record): ?string => $record?->voice_note_path)
    ->disk('public')
    ->directory('messages/voice')
    ->uploadOnSubmit();

Private S3 disk — short 30s limit

VoiceNoteRecorderField::make('voice_note_path')
    ->label('Quick voice reply')
    ->disk('s3')
    ->directory('voice-notes')
    ->visibility('private')
    ->maxDuration(30)
    ->maxSize(2048)
    ->uploadImmediately();

Custom control icons

use Bjanczak\FilamentFlexFields\Support\GravityIcon;

VoiceNoteRecorderField::make('voice_note_path')
    ->microphoneIcon(GravityIcon::Microphone)
    ->playIcon('heroicon-o-play')
    ->pauseIcon('heroicon-o-pause')
    ->stopIcon('heroicon-o-stop')
    ->trashIcon('heroicon-o-trash')
    ->checkmarkIcon('heroicon-o-check')
    ->size('lg');

Public helper methods

MethodReturnsDescription
getMaxDuration()intMax recording seconds
shouldUploadImmediately()boolImmediate vs deferred upload
getInitialAudioUrl()string|nullPublic URL for existing persisted file (edit forms)
getPlayIcon() / getPauseIcon() / …string|BackedEnum|HtmlableResolved control icons

CSS classes

ClassRole
fff-voice-recorderRoot wrapper
fff-voice-recorder--{sm|md|lg}Size modifier
fff-voice-recorder__record-btnStart recording
fff-voice-recorder__recordingActive recording UI + canvas visualizer
fff-voice-recorder__playback-pillPlayback bar (play, waveform, time, delete)
fff-voice-recorder__waveformScrubbable waveform bars
fff-voice-recorder__container.is-submittingDeferred upload in progress on submit
Alpine component: voice-note-recorder-field (built to resources/dist/components/voice-note-recorder-field.js).

Playground

Registered under Audio field playground (AudioFieldPlayground):
Variant keyDescription
voice_note__basicDefault recorder, deferred upload
voice_note__sm / voice_note__lgSize variants
voice_note__with_limitmaxDuration(30)
voice_note__immediateuploadImmediately()
Use Dump JSON in the playground to inspect temp upload state (livewire-file:…). Permanent disk storage requires a real form save (playground has no save action).

Implementation notes

  • Requires microphone permission and a browser with MediaRecorder (Chrome, Safari, Firefox).
  • Prefers audio/mp4 when supported (Safari), falls back to audio/webm / audio/ogg.
  • Local playback uses blob URLs; duration falls back to measured recording time when WebM metadata is missing.
  • Deferred upload hooks the parent <form> submit event (capture phase), uploads via Livewire, then calls form.requestSubmit().
  • Delete calls removeUploadedFile (temp) or deleteUploadedFile (persisted) via callSchemaComponentMethod when schemaComponentKey is available.
  • Translations: filament-flex-fields::default.audio.* (record_label, uploading_on_submit, etc.).