The control panel may be customized in a number of different ways. You may add new pages, menus, a stylesheet, or maybe you just want to add some arbitrary Javascript.
When you need to add features to your Shopper administration, you can first set up some configurations
Adding CSS and JS Assets
Shopper can load extra stylesheets and JavaScript files in the admin panel. Register them from a service provider using the Shopper facade:
use Shopper\Facades\Shopper;
public function boot(): void
{
Shopper::styles([
'/css/admin.css',
'https://cdn.example.com/library.min.css',
])->scripts([
'https://cdn.example.com/library.min.js',
'/js/admin.js',
]);
}
Local paths (starting with /) are resolved from the public/ directory. CDN URLs are loaded directly.
Customize Shopper theme
Shopper ships with a pre-compiled CSS file (dist/shopper.css) that works out of the box. However, if you want to customize the admin panel styles using Tailwind CSS, you can build your own theme by importing Shopper’s theme.css file.
Shopper provides a dedicated theme.css entry point that handles all internal imports (Tailwind, Filament, Shopper base styles, source detection, and plugins) so you don’t have to wire them up manually.
Setting up Tailwind CSS for your project
Install Tailwind CSS v4 with the Vite plugin and the required plugins:
npm install -D tailwindcss @tailwindcss/vite @tailwindcss/forms @tailwindcss/typography
Add the @tailwindcss/vite plugin to your vite.config.js:
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
tailwindcss(),
laravel({
input: [
'resources/css/app.css',
'resources/css/shopper.css',
],
}),
],
});
Creating the theme file
Create a resources/css/shopper.css file and import Shopper’s theme entry point:
@import '../../vendor/shopper/framework/resources/css/theme.css';
The theme.css file already includes Tailwind, Filament CSS, Shopper base styles and components, source detection for all required Blade views, and the @tailwindcss/forms and @tailwindcss/typography plugins. You don’t need to configure any of that yourself.
To add your own source paths (custom Livewire components, app modules, etc.) or custom styles, add them after the import:
@import '../../vendor/shopper/framework/resources/css/theme.css';
/* Additional source paths for your project */
@source '../../resources/views/**/*.blade.php';
@source '../../app-modules/**/resources/views/**/*.blade.php';
/* Your custom styles */
Load theme
In your AppServiceProvider or any other provider, register the Vite theme in the boot method:
use Shopper\Facades\Shopper;
public function boot(): void
{
Shopper::registerViteTheme('resources/css/shopper.css');
}
Branding Logo
By default, the Shopper logo is used on the login page and in the sidebar header. You have several options to replace it with your own brand.
Via configuration
The simplest approach is to set the brand key in your config/shopper/admin.php configuration file with a path to your logo image:
/*
|--------------------------------------------------------------------------
| Brand Logo
|--------------------------------------------------------------------------
|
| This will be displayed on the login page and in the sidebar's header.
| This is your site's logo. It will be loaded directly from the public folder
| Ex: '/images/logo.svg'
|
*/
'brand' => 'images/logo.svg',
This will load the image using the Laravel asset() helper function, so the file should be in your public/ directory.
Via service provider
For more control, use the brandLogo() method on the Shopper facade. You can pass an HTML string, a closure that returns a string, or a closure that returns a View instance:
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
public function boot(): void
{
// Simple image tag
Shopper::brandLogo('<img class="size-8" src="/images/logo.svg" alt="My Store" />');
// With a Blade component
Shopper::brandLogo(fn (): string => Blade::render('<x-application-logo class="size-8" />'));
// With a Blade view
Shopper::brandLogo(fn (): \Illuminate\Contracts\View\View => view('brand.logo'));
}
This takes priority over the config value, giving you full flexibility to render any HTML — an inline SVG, a Blade component, a dedicated view, or anything else.
The priority order is: Shopper::brandLogo() > config('shopper.admin.brand') > default Shopper logo.
Render hooks
Shopper allows you to render Blade content at specific points in the admin panel. This is useful for integrating third-party packages that require Blade components in the layout, such as Flux UI, Wire Elements Modal, or Laravel Notify, and for addons that need to inject content into specific pages.
If you are familiar with Filament’s render hooks, you already know how this works. Shopper’s render hooks follow the same concept.
Render hooks are organized into scoped classes by business domain under the Shopper\View namespace:
| Class | Scope |
|---|
LayoutRenderHook | Head, body, header, content, dashboard, account, settings |
ProductRenderHook | Product index, edit, variant |
OrderRenderHook | Order index, detail, shipments, abandoned carts |
CustomerRenderHook | Customer index, create, show |
CollectionRenderHook | Collection index, edit |
CatalogRenderHook | Categories, brands, tags, attributes, reviews |
SalesRenderHook | Discounts, suppliers |
Registering a render hook
To register a render hook, call the Shopper::renderHook() method from a service provider’s boot method. The first argument is the hook constant, and the second is a closure that returns HTML:
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
use Shopper\View\LayoutRenderHook;
public function boot(): void
{
Shopper::renderHook(
LayoutRenderHook::BODY_END,
fn (): string => Blade::render('@livewire("wire-elements-modal")'),
);
}
Since all methods return the ShopperPanel instance, you can chain multiple calls:
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
use Shopper\View\LayoutRenderHook;
public function boot(): void
{
Shopper::renderHook(LayoutRenderHook::HEAD_END, fn (): string => Blade::render('@fluxStyles'))
->renderHook(LayoutRenderHook::BODY_END, fn (): string => Blade::render('<flux:modal />'))
->renderHook(LayoutRenderHook::BODY_END, fn (): string => Blade::render('<x-notify::notify />'))
->registerViteTheme('resources/css/shopper.css')
->styles(['vendor/custom/lib.css'])
->scripts(['vendor/custom/lib.js']);
}
You can also use hooks from different scoped classes to inject content into specific pages:
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
use Shopper\View\ProductRenderHook;
Shopper::renderHook(
ProductRenderHook::EDIT_HEADER_AFTER,
fn (): string => Blade::render('<livewire:my-addon.product-banner />'),
);
Available render hooks
Layout
| Constant | Class | Description |
|---|
HEAD_START | LayoutRenderHook | After the meta tags, before Filament styles and Shopper theme |
HEAD_END | LayoutRenderHook | End of <head>, after all styles and scripts |
BODY_START | LayoutRenderHook | Start of <body>, before any content |
BODY_END | LayoutRenderHook | End of <body>, after Filament scripts |
HEADER_START | LayoutRenderHook | Start of the header bar content area |
HEADER_END | LayoutRenderHook | End of the header bar, before the site link and account dropdown |
CONTENT_START | LayoutRenderHook | Start of the main content area, after the sidebar |
CONTENT_END | LayoutRenderHook | End of the main content area |
DASHBOARD_START | LayoutRenderHook | Start of the dashboard page |
DASHBOARD_END | LayoutRenderHook | End of the dashboard page |
ACCOUNT_START | LayoutRenderHook | Before the account page content |
ACCOUNT_END | LayoutRenderHook | After the account page content |
SETTINGS_INDEX_START | LayoutRenderHook | Before the settings grid |
SETTINGS_INDEX_END | LayoutRenderHook | After the settings grid |
Products
| Constant | Class | Description |
|---|
INDEX_TABLE_BEFORE | ProductRenderHook | Before the products table |
INDEX_TABLE_AFTER | ProductRenderHook | After the products table |
EDIT_HEADER_AFTER | ProductRenderHook | After the product edit page heading |
EDIT_TABS_BEFORE | ProductRenderHook | Before the product edit tabs |
EDIT_TABS_END | ProductRenderHook | Before the closing of the product edit tabs |
EDIT_CONTENT_BEFORE | ProductRenderHook | Before the product edit tab content |
EDIT_CONTENT_AFTER | ProductRenderHook | After the product edit tab content |
VARIANT_HEADER_AFTER | ProductRenderHook | After the variant page heading |
VARIANT_MAIN_AFTER | ProductRenderHook | After the variant main column |
VARIANT_SIDEBAR_AFTER | ProductRenderHook | After the variant sidebar column |
Orders
| Constant | Class | Description |
|---|
INDEX_TABLE_BEFORE | OrderRenderHook | Before the orders table |
INDEX_TABLE_AFTER | OrderRenderHook | After the orders table |
DETAIL_HEADER_AFTER | OrderRenderHook | After the order detail sticky header |
DETAIL_MAIN_BEFORE | OrderRenderHook | Before the order detail main content |
DETAIL_MAIN_AFTER | OrderRenderHook | After the order detail main column |
DETAIL_SIDEBAR_BEFORE | OrderRenderHook | Before the order detail sidebar |
DETAIL_SIDEBAR_AFTER | OrderRenderHook | After the order detail sidebar |
SHIPMENTS_TABLE_BEFORE | OrderRenderHook | Before the shipments table |
SHIPMENTS_TABLE_AFTER | OrderRenderHook | After the shipments table |
ABANDONED_CARTS_TABLE_BEFORE | OrderRenderHook | Before the abandoned carts table |
ABANDONED_CARTS_TABLE_AFTER | OrderRenderHook | After the abandoned carts table |
Customers
| Constant | Class | Description |
|---|
INDEX_TABLE_BEFORE | CustomerRenderHook | Before the customers table |
INDEX_TABLE_AFTER | CustomerRenderHook | After the customers table |
CREATE_FORM_BEFORE | CustomerRenderHook | Before the create customer form |
CREATE_FORM_AFTER | CustomerRenderHook | After the create customer form |
SHOW_HEADER_AFTER | CustomerRenderHook | After the customer show page header |
SHOW_TABS_BEFORE | CustomerRenderHook | Before the customer show tabs |
SHOW_TABS_END | CustomerRenderHook | Before the closing of the customer show tabs |
SHOW_CONTENT_BEFORE | CustomerRenderHook | Before the customer show tab content |
SHOW_CONTENT_AFTER | CustomerRenderHook | After the customer show tab content |
Collections
| Constant | Class | Description |
|---|
INDEX_TABLE_BEFORE | CollectionRenderHook | Before the collections table |
INDEX_TABLE_AFTER | CollectionRenderHook | After the collections table |
EDIT_FORM_BEFORE | CollectionRenderHook | Before the collection edit form |
EDIT_FORM_AFTER | CollectionRenderHook | After the collection edit form |
Catalog
| Constant | Class | Description |
|---|
CATEGORIES_TABLE_BEFORE | CatalogRenderHook | Before the categories table |
CATEGORIES_TABLE_AFTER | CatalogRenderHook | After the categories table |
BRANDS_TABLE_BEFORE | CatalogRenderHook | Before the brands table |
BRANDS_TABLE_AFTER | CatalogRenderHook | After the brands table |
TAGS_TABLE_BEFORE | CatalogRenderHook | Before the tags table |
TAGS_TABLE_AFTER | CatalogRenderHook | After the tags table |
ATTRIBUTES_TABLE_BEFORE | CatalogRenderHook | Before the attributes table |
ATTRIBUTES_TABLE_AFTER | CatalogRenderHook | After the attributes table |
REVIEWS_TABLE_BEFORE | CatalogRenderHook | Before the reviews table |
REVIEWS_TABLE_AFTER | CatalogRenderHook | After the reviews table |
Sales
| Constant | Class | Description |
|---|
DISCOUNTS_TABLE_BEFORE | SalesRenderHook | Before the discounts table |
DISCOUNTS_TABLE_AFTER | SalesRenderHook | After the discounts table |
SUPPLIERS_TABLE_BEFORE | SalesRenderHook | Before the suppliers table |
SUPPLIERS_TABLE_AFTER | SalesRenderHook | After the suppliers table |
Example: Wire Elements Modal
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
use Shopper\View\LayoutRenderHook;
Shopper::renderHook(
LayoutRenderHook::BODY_END,
fn (): string => Blade::render('@livewire("wire-elements-modal")'),
);
Example: Flux UI
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
use Shopper\View\LayoutRenderHook;
Shopper::renderHook(LayoutRenderHook::HEAD_END, fn (): string => Blade::render('@fluxStyles'))
->renderHook(LayoutRenderHook::BODY_END, fn (): string => Blade::render('@fluxScripts'));
Example: Laravel Notify
use Illuminate\Support\Facades\Blade;
use Shopper\Facades\Shopper;
use Shopper\View\LayoutRenderHook;
Shopper::renderHook(
LayoutRenderHook::BODY_END,
fn (): string => Blade::render('<x-notify::notify />'),
);
Adding control panel routes
If you need to have custom routes for the control panel:
-
Create a routes file. Name it whatever you want, for example:
routes/shopper.php
-
Then add this to your
shopper/routes.php file so that all routes are dynamically loaded:
'custom_file' => base_path('routes/shopper.php'),
-
If you want to add middleware to further control access to the routes available in this file you can add in the key
middleware of the shopper/routes.php file
'middleware' => [
'my-custom-middleware',
'permission:can-access',
],