Skip to main content
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');
}
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:
ClassScope
LayoutRenderHookHead, body, header, content, dashboard, account, settings
ProductRenderHookProduct index, edit, variant
OrderRenderHookOrder index, detail, shipments, abandoned carts
CustomerRenderHookCustomer index, create, show
CollectionRenderHookCollection index, edit
CatalogRenderHookCategories, brands, tags, attributes, reviews
SalesRenderHookDiscounts, 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

ConstantClassDescription
HEAD_STARTLayoutRenderHookAfter the meta tags, before Filament styles and Shopper theme
HEAD_ENDLayoutRenderHookEnd of <head>, after all styles and scripts
BODY_STARTLayoutRenderHookStart of <body>, before any content
BODY_ENDLayoutRenderHookEnd of <body>, after Filament scripts
HEADER_STARTLayoutRenderHookStart of the header bar content area
HEADER_ENDLayoutRenderHookEnd of the header bar, before the site link and account dropdown
CONTENT_STARTLayoutRenderHookStart of the main content area, after the sidebar
CONTENT_ENDLayoutRenderHookEnd of the main content area
DASHBOARD_STARTLayoutRenderHookStart of the dashboard page
DASHBOARD_ENDLayoutRenderHookEnd of the dashboard page
ACCOUNT_STARTLayoutRenderHookBefore the account page content
ACCOUNT_ENDLayoutRenderHookAfter the account page content
SETTINGS_INDEX_STARTLayoutRenderHookBefore the settings grid
SETTINGS_INDEX_ENDLayoutRenderHookAfter the settings grid

Products

ConstantClassDescription
INDEX_TABLE_BEFOREProductRenderHookBefore the products table
INDEX_TABLE_AFTERProductRenderHookAfter the products table
EDIT_HEADER_AFTERProductRenderHookAfter the product edit page heading
EDIT_TABS_BEFOREProductRenderHookBefore the product edit tabs
EDIT_TABS_ENDProductRenderHookBefore the closing of the product edit tabs
EDIT_CONTENT_BEFOREProductRenderHookBefore the product edit tab content
EDIT_CONTENT_AFTERProductRenderHookAfter the product edit tab content
VARIANT_HEADER_AFTERProductRenderHookAfter the variant page heading
VARIANT_MAIN_AFTERProductRenderHookAfter the variant main column
VARIANT_SIDEBAR_AFTERProductRenderHookAfter the variant sidebar column

Orders

ConstantClassDescription
INDEX_TABLE_BEFOREOrderRenderHookBefore the orders table
INDEX_TABLE_AFTEROrderRenderHookAfter the orders table
DETAIL_HEADER_AFTEROrderRenderHookAfter the order detail sticky header
DETAIL_MAIN_BEFOREOrderRenderHookBefore the order detail main content
DETAIL_MAIN_AFTEROrderRenderHookAfter the order detail main column
DETAIL_SIDEBAR_BEFOREOrderRenderHookBefore the order detail sidebar
DETAIL_SIDEBAR_AFTEROrderRenderHookAfter the order detail sidebar
SHIPMENTS_TABLE_BEFOREOrderRenderHookBefore the shipments table
SHIPMENTS_TABLE_AFTEROrderRenderHookAfter the shipments table
ABANDONED_CARTS_TABLE_BEFOREOrderRenderHookBefore the abandoned carts table
ABANDONED_CARTS_TABLE_AFTEROrderRenderHookAfter the abandoned carts table

Customers

ConstantClassDescription
INDEX_TABLE_BEFORECustomerRenderHookBefore the customers table
INDEX_TABLE_AFTERCustomerRenderHookAfter the customers table
CREATE_FORM_BEFORECustomerRenderHookBefore the create customer form
CREATE_FORM_AFTERCustomerRenderHookAfter the create customer form
SHOW_HEADER_AFTERCustomerRenderHookAfter the customer show page header
SHOW_TABS_BEFORECustomerRenderHookBefore the customer show tabs
SHOW_TABS_ENDCustomerRenderHookBefore the closing of the customer show tabs
SHOW_CONTENT_BEFORECustomerRenderHookBefore the customer show tab content
SHOW_CONTENT_AFTERCustomerRenderHookAfter the customer show tab content

Collections

ConstantClassDescription
INDEX_TABLE_BEFORECollectionRenderHookBefore the collections table
INDEX_TABLE_AFTERCollectionRenderHookAfter the collections table
EDIT_FORM_BEFORECollectionRenderHookBefore the collection edit form
EDIT_FORM_AFTERCollectionRenderHookAfter the collection edit form

Catalog

ConstantClassDescription
CATEGORIES_TABLE_BEFORECatalogRenderHookBefore the categories table
CATEGORIES_TABLE_AFTERCatalogRenderHookAfter the categories table
BRANDS_TABLE_BEFORECatalogRenderHookBefore the brands table
BRANDS_TABLE_AFTERCatalogRenderHookAfter the brands table
TAGS_TABLE_BEFORECatalogRenderHookBefore the tags table
TAGS_TABLE_AFTERCatalogRenderHookAfter the tags table
ATTRIBUTES_TABLE_BEFORECatalogRenderHookBefore the attributes table
ATTRIBUTES_TABLE_AFTERCatalogRenderHookAfter the attributes table
REVIEWS_TABLE_BEFORECatalogRenderHookBefore the reviews table
REVIEWS_TABLE_AFTERCatalogRenderHookAfter the reviews table

Sales

ConstantClassDescription
DISCOUNTS_TABLE_BEFORESalesRenderHookBefore the discounts table
DISCOUNTS_TABLE_AFTERSalesRenderHookAfter the discounts table
SUPPLIERS_TABLE_BEFORESalesRenderHookBefore the suppliers table
SUPPLIERS_TABLE_AFTERSalesRenderHookAfter 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:
  1. Create a routes file. Name it whatever you want, for example: routes/shopper.php
  2. Then add this to your shopper/routes.php file so that all routes are dynamically loaded:
     'custom_file' => base_path('routes/shopper.php'),
    
  3. 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',
    ],