- Basics
- Sorting
- Buttons
- Attributes
- Click Actions
- Sticky Header
- Column Display
- Sticky columns
- Pagination
- Async Mode
- Lazy mode
- Modifiers
Basics
In CrudResource (ModelResource) on the indexPage as well as on the DetailPage, TableBuilder is used to display the main data,
so we recommend you also study the documentation section TableBuilder.
Sorting
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:4]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\Support\Enums\SortDirection;class PostResource extends ModelResource{// Default sort fieldprotected string $sortColumn = 'created_at';// Default sort typeprotected SortDirection $sortDirection = SortDirection::DESC;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:4]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\Support\Enums\SortDirection;class PostResource extends ModelResource{// Default sort fieldprotected string $sortColumn = 'created_at';// Default sort typeprotected SortDirection $sortDirection = SortDirection::DESC;// ...}
Buttons
To add buttons to the table, you can use ActionButton and the methods indexButtons(), as well as detailButtons() for the detail page.
After the main buttons:
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->add(ActionButton::make('Link', '/endpoint'));}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->add(ActionButton::make('Link', '/endpoint'));}
Before the main buttons:
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->prepend(ActionButton::make('Link', '/endpoint'));}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->prepend(ActionButton::make('Link', '/endpoint'));}
Remove the delete button:
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->except(fn(ActionButton $btn) => $btn->getName() === 'resource-delete-button');}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->except(fn(ActionButton $btn) => $btn->getName() === 'resource-delete-button');}
Standard button names for the index view rows:
- resource-detail-button,
- resource-edit-button,
- resource-delete-button,
- mass-delete-button.
You can also globally disable any actions with the resource (see active actions.
Clear the button set and add your own:
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{parent::indexButtons()->empty()->add(ActionButton::make('Link', '/endpoint'));}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{parent::indexButtons()->empty()->add(ActionButton::make('Link', '/endpoint'));}
The same approach is used for the table on the detail page, only through the method detailButtons().
For bulk actions, you need to add the bulk() method.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->add(ActionButton::make('Link', '/endpoint')->bulk());}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:2]use MoonShine\Support\ListOf;use MoonShine\UI\Components\ActionButton;protected function indexButtons(): ListOf{return parent::indexButtons()->add(ActionButton::make('Link', '/endpoint')->bulk());}
By default, all buttons in the table are displayed in a line, but you can change the behavior and display them through a drop-down list.
To do this, change the $indexButtonsInDropdown property in the resource:
class PostResource extends ModelResource{protected bool $indexButtonsInDropdown = true;// ...}class PostResource extends ModelResource{protected bool $indexButtonsInDropdown = true;// ...}
Attributes
To add attributes for the td element of the table, you can use the customWrapperAttributes() method on the field that represents the cell you need.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\UI\Fields\Text;protected function indexFields(): iterable{return [// ...Text::make('Title')->customWrapperAttributes(['width' => '20%']);];}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\UI\Fields\Text;protected function indexFields(): iterable{return [// ...Text::make('Title')->customWrapperAttributes(['width' => '20%']);];}
You can also customize tr and td for the table with data through the resource.
To do this, you need to use the corresponding methods trAttributes() and tdAttributes(),
to which you need to pass a closure that returns an array of attributes for the table component.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:6]namespace App\MoonShine\Resources;use Closure;use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\UI\Fields\Text;class PostResource extends ModelResource{// ...protected function tdAttributes(): Closure{return fn(?DataWrapperContract $data, int $row, int $cell) => ['width' => '20%'];}protected function trAttributes(): Closure{return fn(?DataWrapperContract $data, int $row) => ['data-tr' => $row];}}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:6]namespace App\MoonShine\Resources;use Closure;use MoonShine\Contracts\Core\TypeCasts\DataWrapperContract;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\UI\Fields\Text;class PostResource extends ModelResource{// ...protected function tdAttributes(): Closure{return fn(?DataWrapperContract $data, int $row, int $cell) => ['width' => '20%'];}protected function trAttributes(): Closure{return fn(?DataWrapperContract $data, int $row) => ['data-tr' => $row];}}

Click Actions
By default, clicking on tr does nothing, but you can change the behavior to navigate to editing, selection, or to the detailed view.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\Support\Enums\ClickAction;// ClickAction::SELECT, ClickAction::DETAIL, ClickAction::EDITprotected ?ClickAction $clickAction = ClickAction::SELECT;// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\Support\Enums\ClickAction;// ClickAction::SELECT, ClickAction::DETAIL, ClickAction::EDITprotected ?ClickAction $clickAction = ClickAction::SELECT;
Sticky Header
The model resource property stickyTable allows you to fix the header when scrolling a table with a large number of elements.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravle\Resources\ModelResource;class PostResource extends ModelResource{protected bool $stickyTable = true;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravle\Resources\ModelResource;class PostResource extends ModelResource{protected bool $stickyTable = true;// ...}
Column Display
You can allow users to independently determine which columns to display in the table while retaining their selection.
To do this, you need to set the parameter $columnSelection for the resource.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $columnSelection = true;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $columnSelection = true;// ...}
If you need to exclude fields from the selection, use the columnSelection() method.
columnSelection(bool $active = true,bool $hideOnInit = false)columnSelection(bool $active = true,bool $hideOnInit = false)
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:6]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Text;use MoonShine\UI\Fields\Textarea;class PostResource extends ModelResource{protected bool $columnSelection = true;// ...protected function indexFields(): iterable{return [ID::make()->columnSelection(false),Text::make('Title'),Textarea::make('Body'),];}}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:6]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Text;use MoonShine\UI\Fields\Textarea;class PostResource extends ModelResource{protected bool $columnSelection = true;// ...protected function indexFields(): iterable{return [ID::make()->columnSelection(false),Text::make('Title'),Textarea::make('Body'),];}}
If you need to hide the default field:
->columnSelection(hideOnInit: true)->columnSelection(hideOnInit: true)
Sticky columns
You can freeze cells in large tables, suitable for ID columns and buttons.
To fix buttons in the table, switch the resource to stickyButtons mode.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $stickyButtons = true;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $stickyButtons = true;// ...}
To fix a column, call the sticky() method on the Field.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\UI\Fields\ID;protected function indexFields(): iterable{return [ID::make()->sticky(),];}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\UI\Fields\ID;protected function indexFields(): iterable{return [ID::make()->sticky(),];}
Pagination
To change the number of items per page, use the property $itemsPerPage.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected int $itemsPerPage = 25;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected int $itemsPerPage = 25;// ...}
Cursor
When dealing with a large volume of data, the best solution is to use cursor pagination.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $cursorPaginate = true;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $cursorPaginate = true;// ...}
Simple
If you do not plan to display the total number of pages, use Simple Pagination.
This avoids additional queries for the total number of records in the database.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $simplePaginate = true;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $simplePaginate = true;// ...}

Disable Pagination
If you do not plan to use pagination, it can be disabled.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $usePagination = false;// ...}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:3]namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $usePagination = false;// ...}
Async Mode
In the resource, async mode is used by default.
This mode allows for pagination, filtering, and sorting without page reloads.
However, if you want to disable async mode, you can use the property $isAsync.
namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $isAsync = false;// ...}
namespace App\MoonShine\Resources;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $isAsync = false;// ...}
Updating a row
You can asynchronously update a row in the table by triggering the event:
table-row-updated-{{componentName}}-{{row-id}}table-row-updated-{{componentName}}-{{row-id}}
{{componentName}}- the name of the component;{{row-id}}- the key of the row item.
To add an event, you can use the helper class:
AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, 'main-table-{row-id}')AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, 'main-table-{row-id}')
{row-id}- shortcode for the id of the current model record.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:8]namespace App\MoonShine\Resources;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\Support\AlpineJs;use MoonShine\Support\Enums\JsEvent;class PostResource extends ModelResource{// ...protected function indexFields(): iterable{return [ID::make(),Text::make('Title'),Switcher::make('Active')->updateOnPreview(events: [AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, $this->getListComponentNameWithRow())])];}}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:8]namespace App\MoonShine\Resources;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text;use MoonShine\Laravel\Resources\ModelResource;use MoonShine\Support\AlpineJs;use MoonShine\Support\Enums\JsEvent;class PostResource extends ModelResource{// ...protected function indexFields(): iterable{return [ID::make(),Text::make('Title'),Switcher::make('Active')->updateOnPreview(events: [AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, $this->getListComponentNameWithRow())])];}}
Also, an example of a response with an event.
public function softDelete(MoonShineRequest $request): MoonShineJsonResponse{$item = $request->getResource()->getItem();$item->delete();return MoonShineJsonResponse::make()->events([AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, $this->getListComponentNameWithRow($item->getKey()))])->toast('Success');}public function softDelete(MoonShineRequest $request): MoonShineJsonResponse{$item = $request->getResource()->getItem();$item->delete();return MoonShineJsonResponse::make()->events([AlpineJs::event(JsEvent::TABLE_ROW_UPDATED, $this->getListComponentNameWithRow($item->getKey()))])->toast('Success');}
The withUpdateRow() method is also available, which helps simplify the assignment of events.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:6]namespace App\MoonShine\Resources;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{// ...protected function indexFields(): iterable{return [ID::make(),Text::make('Title'),Switcher::make('Active')->withUpdateRow($this->getListComponentName())];}}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:6]namespace App\MoonShine\Resources;use MoonShine\UI\Fields\ID;use MoonShine\UI\Fields\Switcher;use MoonShine\UI\Fields\Text;use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{// ...protected function indexFields(): iterable{return [ID::make(),Text::make('Title'),Switcher::make('Active')->withUpdateRow($this->getListComponentName())];}}
Lazy mode
If you want to display a page without waiting for data to load, and then send a query to get the table data, use Lazy mode.
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $isLazy = true;}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:1]use MoonShine\Laravel\Resources\ModelResource;class PostResource extends ModelResource{protected bool $isLazy = true;}
Modifiers
Components
You can completely replace or modify the resource's TableBuilder for both the index and detail pages.
Use the modifyListComponent() or modifyDetailComponent() methods for this.
use MoonShine\Contracts\UI\ComponentContract;public function modifyListComponent(ComponentContract $component): ComponentContract{return parent::modifyListComponent($component)->customAttributes(['data-my-attr' => 'value']);}
use MoonShine\Contracts\UI\ComponentContract;public function modifyListComponent(ComponentContract $component): ComponentContract{return parent::modifyListComponent($component)->customAttributes(['data-my-attr' => 'value']);}
use MoonShine\Contracts\UI\ComponentContract;public function modifyDetailComponent(ComponentContract $component): ComponentContract{return parent::modifyDetailComponent($component)->customAttributes(['data-my-attr' => 'value']);}
use MoonShine\Contracts\UI\ComponentContract;public function modifyDetailComponent(ComponentContract $component): ComponentContract{return parent::modifyDetailComponent($component)->customAttributes(['data-my-attr' => 'value']);}
Elements thead, tbody, tfoot
If it is not enough to just automatically output fields in thead, tbody, and tfoot,
you can override or extend this logic based on the resource methods thead(), tbody(), tfoot().
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:5]use Closure;use MoonShine\Contracts\UI\Collection\TableRowsContract;use MoonShine\Contracts\UI\TableRowContract;use MoonShine\UI\Collections\TableCells;use MoonShine\UI\Collections\TableRows;protected function thead(): null|TableRowsContract|Closure{return static fn(TableRowContract $default) => TableRows::make([$default])->pushRow(TableCells::make()->pushCell('td content'));}protected function tbody(): null|TableRowsContract|Closure{return static fn(TableRowsContract $default) => $default->pushRow(TableCells::make()->pushCell('td content'));}protected function tfoot(): null|TableRowsContract|Closure{return static fn(?TableRowContract $default) => TableRows::make([$default])->pushRow(TableCells::make()->pushCell('td content'));}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:5]use Closure;use MoonShine\Contracts\UI\Collection\TableRowsContract;use MoonShine\Contracts\UI\TableRowContract;use MoonShine\UI\Collections\TableCells;use MoonShine\UI\Collections\TableRows;protected function thead(): null|TableRowsContract|Closure{return static fn(TableRowContract $default) => TableRows::make([$default])->pushRow(TableCells::make()->pushCell('td content'));}protected function tbody(): null|TableRowsContract|Closure{return static fn(TableRowsContract $default) => $default->pushRow(TableCells::make()->pushCell('td content'));}protected function tfoot(): null|TableRowsContract|Closure{return static fn(?TableRowContract $default) => TableRows::make([$default])->pushRow(TableCells::make()->pushCell('td content'));}
Example of adding a row in tfoot
// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:7]use Closure;use MoonShine\Contracts\UI\Collection\TableRowsContract;use MoonShine\Contracts\UI\TableRowContract;use MoonShine\UI\Collections\TableCells;use MoonShine\UI\Collections\TableRows;use MoonShine\UI\Components\Table\TableBuilder;use MoonShine\UI\Components\Table\TableRow;protected function tfoot(): null|TableRowsContract|Closure{return static function(?TableRowContract $default, TableBuilder $table) {$cells = TableCells::make();$cells->pushCell('Balance:');$cells->pushCell('$1000');return TableRows::make([TableRow::make($cells), $default]);};}// torchlight! {"summaryCollapsedIndicator": "namespaces"}// [tl! collapse:7]use Closure;use MoonShine\Contracts\UI\Collection\TableRowsContract;use MoonShine\Contracts\UI\TableRowContract;use MoonShine\UI\Collections\TableCells;use MoonShine\UI\Collections\TableRows;use MoonShine\UI\Components\Table\TableBuilder;use MoonShine\UI\Components\Table\TableRow;protected function tfoot(): null|TableRowsContract|Closure{return static function(?TableRowContract $default, TableBuilder $table) {$cells = TableCells::make();$cells->pushCell('Balance:');$cells->pushCell('$1000');return TableRows::make([TableRow::make($cells), $default]);};}
You can use a list component outside a resource, but you must understand what it is.
app(MoonShineUserResource::class)->getIndexPage()->getListComponent(withoutFragment: false)