Filament Infolist: Create Custom Components with Tailwind CSS

Filament v3 has an awesome Infolist feature, but the components are pretty limited, like TextEntry or ImageEntry. What if you want to create your own custom entry? This tutorial will teach you how to make a custom Filament Infolist Component with custom CSS styling.

As an example, we will have Course and Lesson Models. Our main goals are:

  • To display a list of all lessons on the view Course page
  • Re-use the same component and display a list of all lessons on the view Lesson page
  • Highlight the current Lesson on the view Lesson page
  • Style list using TailwindCSS by compiling a custom theme

View Course

View Lesson

View Lesson Dark

First, let's quickly set up our models and data.


Migrations, Models, Factories & Seeds

Let's create Course and Lesson Models with Migrations and Factories by passing the -mf flag.

php artisan make:model Course -mf
 
php artisan make:model Lesson -mf

The database schema is as follows.

database/migrations/XX_create_courses_table.php

Schema::create('courses', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->longText('content')->nullable();
$table->timestamps();
});

database/migrations/XX_create_lessons_table.php

use App\Models\Course;
 
// ...
 
Schema::create('lessons', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Course::class);
$table->string('name');
$table->longText('content')->nullable();
$table->timestamps();
});

Now let's define our fillable fields and relationships for both Models.

app/Models/Course.php

namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Course extends Model
{
use HasFactory;
 
protected $fillable = ['name', 'content'];
 
public function lessons(): HasMany
{
return $this->hasMany(Lesson::class);
}
}

app/Models/Lesson.php

namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Lesson extends Model
{
use HasFactory;
 
protected $fillable = ['course_id', 'name', 'content'];
 
public function course(): BelongsTo
{
return $this->belongsTo(Course::class);
}
}

Now that everything is in place, we need data to populate the database. It is time to define factories for both models.

database/factories/CourseFactory.php

public function definition(): array
{
return [
'name' => rtrim(fake()->sentence(), '.'),
'content' => fake()->realText(),
];
}

database/factories/LessonFactory.php

public function definition(): array
{
return [
'name' => rtrim(fake()->sentence(), '.'),
'content' => fake()->realText(),
];
}

And finally, update the DatabaseSeeder class as follows.

database/seeders/DatabaseSeeder.php

namespace Database\Seeders;
 
use App\Models\Course;
use App\Models\User;
use Illuminate\Database\Seeder;
 
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()->create([
'name' => 'Admin',
'email' => '[email protected]',
]);
 
Course::factory(5)->hasLessons(10)->create();
}
}

Now you should have five Courses with ten Lessons each in your database.


Filament Resources

Before we go any further with creating the Infolist Component, we first need to have a Filament admin panel.

Install the Filament Panel Builder by running the following commands in your Laravel project directory.

composer require filament/filament:"^3.0-stable" -W
 
php artisan filament:install --panels

By default, Filament Resources does not have a View page for models and opens the Edit page instead. You can use the'- view' flag to create a new resource with a View page. Do this for both Models.

php artisan make:filament-resource Course --view
 
php artisan make:filament-resource Lesson --view

Let's update those resources as follows to have a working panel quickly.

app/Filament/Resources/CourseResource.php

namespace App\Filament\Resources;
 
use App\Filament\Resources\CourseResource\Pages;
use App\Models\Course;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
 
class CourseResource extends Resource
{
protected static ?string $model = Course::class;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->required()
->columnSpanFull(),
RichEditor::make('content')
->columnSpanFull(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('#'),
TextColumn::make('name'),
TextColumn::make('lessons_count')
->label('Lessons')
->counts('lessons'),
])
->actions([
Action::make('Lessons')
->icon('heroicon-m-academic-cap')
->url(fn (Course $record): string => LessonResource::getUrl('index', [
'tableFilters[course][value]' => $record,
])),
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListCourses::route('/'),
'create' => Pages\CreateCourse::route('/create'),
'view' => Pages\ViewCourse::route('/{record}'),
'edit' => Pages\EditCourse::route('/{record}/edit'),
];
}
}

app/Filament/Resources/LessonResource.php

namespace App\Filament\Resources;
 
use App\Filament\Resources\LessonResource\Pages;
use App\Models\Lesson;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
 
class LessonResource extends Resource
{
protected static ?string $model = Lesson::class;
 
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
 
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->required(),
Select::make('course_id')
->label('Course')
->relationship('course', 'name')
->required(),
RichEditor::make('content')
->columnSpanFull(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name'),
TextColumn::make('course.name'),
])
->filters([
SelectFilter::make('course')
->relationship('course', 'name')
->searchable()
->preload(),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
 
public static function getPages(): array
{
return [
'index' => Pages\ListLessons::route('/'),
'create' => Pages\CreateLesson::route('/create'),
'view' => Pages\ViewLesson::route('/{record}'),
'edit' => Pages\EditLesson::route('/{record}/edit'),
];
}
}

Infolist Component

You may use the following command to create a custom Infolist component class and view.

php artisan make:infolist-layout LessonList

We have added two setter methods:

  • course() - let's set a course to display lessons from
  • current() - set a lesson we are currently viewing; it is helpful in some cases, for example, if you want to highlight the current lesson in the list.

Two getter methods, getCourse() and getCurrent(), allow retrieving those values in Blade view. Getters will enable us to inject values such as record the Filament way, as we do with any other field components.

app/Infolists/Components/LessonList.php

namespace App\Infolists\Components;
 
use App\Models\Course;
use App\Models\Lesson;
use Closure;
use Filament\Infolists\Components\Component;
 
class LessonList extends Component
{
protected string $view = 'infolists.components.lesson-list';
 
protected null | Course | Closure $course = null;
 
protected null | Lesson | Closure $current = null;
 
public static function make(): static
{
return app(static::class);
}
 
public function course($course): self
{
$this->course = $course;
 
return $this;
}
 
public function getCourse(): ?Course
{
$course = $this->evaluate($this->course);
 
if (! $course instanceof Course) {
return null;
}
 
return $course;
}
 
public function current($lesson): self
{
$this->current = $lesson;
 
return $this;
}
 
public function getCurrent(): ?Lesson
{
$lesson = $this->evaluate($this->current);
 
if (! $lesson instanceof Lesson) {
return null;
}
 
return $lesson;
}
}

The LessonList Blade view looks like this.

resources/views/infolists/components/lesson-list.blade.php

<div {{ $attributes }}>
@foreach($getCourse()->lessons as $index => $lesson)
<div>
<a
href="{{ route('filament.admin.resources.lessons.view', $lesson) }}"
@class(['font-semibold' => $getCurrent()?->id === $lesson->id])
>
{{ $index + 1 }}: {{ $lesson->name }}
</a>
</div>
@endforeach
</div>

We do not call getter methods like this $this->getCourse() because $this does not refer to the LessonList Component but the page component itself. Getters are reachable via callable variables like $getCourse().

The View page will default display a disabled form with the record's data. To display Infolist and have a custom layout, we can define an infolist() method on the resource class.

Let's update both resource classes for each Model.

app/Filament/Resources/CourseResource.php

use App\Infolists\Components\LessonList;
use Filament\Infolists\Components\Grid;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
 
// ...
 
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->columns(3)
->schema([
Grid::make()
->columns(1)
->columnSpan(2)
->schema([
TextEntry::make('name'),
TextEntry::make('content')
->html(),
]),
Grid::make()
->columns(1)
->columnSpan(1)
->schema([
LessonList::make()
->course(fn (Course $record) => $record),
]),
]);
}

app/Filament/Resources/LessonResource.php

use App\Infolists\Components\LessonList;
use Filament\Infolists\Components\Grid;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Infolist;
 
// ...
 
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->columns(3)
->schema([
Grid::make()
->columns(1)
->columnSpan(2)
->schema([
TextEntry::make('name'),
TextEntry::make('content')
->html(),
]),
Grid::make()
->columns(1)
->columnSpan(1)
->schema([
LessonList::make()
->course(fn (Lesson $record) => $record->course)
->current(fn (Lesson $record) => $record),
]),
]);
}

Custom Component Design

We used the font-semibold CSS class to highlight the current lesson in the list. It works because Filament already has this class compiled, but if we try to customize it using other TailwindCSS classes, it won't work.

Let's add a custom stylesheet to define our own CSS classes. We can use this command to create a custom theme for a panel.

php artisan make:filament-theme

It will create some configuration files and add TailwindCSS to your project.

Then, add a new item to the input array of vite.config.js.

vite.config.js

export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/css/filament/admin/theme.css'
],
refresh: true,
}),
],
});

Next, register the theme in the admin panel provider.

app/Providers/Filament/AdminPanelProvider.php

class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->colors([
'primary' => Color::Amber,
])
->viteTheme('resources/css/filament/admin/theme.css')

Let's add our custom styling for the LessonList Component.

resources/css/filament/admin/theme.css

@import '/vendor/filament/filament/resources/css/theme.css';
 
@config 'tailwind.config.js';
 
.card {
@apply bg-white dark:bg-gray-800 p-3 shadow rounded;
}
 
.lesson-list {
@apply space-y-0.5
}
 
.lesson-list a {
@apply flex flex-row hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-900 dark:text-white rounded p-1 text-sm gap-2;
}
 
.lesson-list a.active {
@apply bg-primary-600 dark:bg-primary-500 text-white font-semibold;
}

And update the LessonList layout.

resources/views/infolists/components/lesson-list.blade.php

<div {{ $attributes->merge(['class' => 'card lesson-list']) }}>
@foreach($getCourse()->lessons as $index => $lesson)
<a
href="{{ route('filament.admin.resources.lessons.view', $lesson) }}"
@class(['active' => $getCurrent()?->id === $lesson->id])
>
<div class="w-6 text-right shrink-0">{{ $index + 1 }}</div>
<div>{{ $lesson->name }}</div>
</a>
@endforeach
</div>

Finally, run npm run build to compile the theme and see the final result.

View Lesson


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.

avatar

Wow another great tutorial, learned a lot! I do have a few questions:

$this->evaluate($this->course); what does this do exactly? Where does "evaluate" come from?

Second question as far as my understanding is with tailwind is that the encourage the use of just their styling components and not class names. Wouldnt it be easier to also do that here? Or wouldn't it work if you just made all the css inline?

Third question, in other projects on filament examples (out of memory i think the repair salon) you use for the statuses just a different view. When would you advise creating an infolist and when is a simple view sufficient?

avatar
You can use Markdown
avatar

Ok, I have reproduced this code. All your code works perfectly fine. When I added class text-red-800 into lesson-list.blade.php (on lesson name) it dosent work.

I have same issue on my project - could not make text-color classes to work as expected. Any solution?

avatar

Did you re-compile the assets by running npm run build?

avatar

Sure. It looks like Tailwind dosen't see this class in custom column template. I have to dive deeper into configs I think

avatar
You can use Markdown
avatar
You can use Markdown

Recent New Courses