Soft Customization
The soft customization.
The soft customization involves customizing components at runtime, either through a service provider like AppServiceProvider or object classes. The idea behind soft customization is to explore the building blocks of customization for each component. Even if you are starting with Laravel, with a little attention to the docs below, you will be able to fully customize the components using this concept.
Since the idea of soft customization is to apply customization through PHP object classes, the first thing you need to do is make sure that TailwindCSS tracks the classes that will be defined from your application's *.php files. To do this, you need to edit your app.css CSS file by inserting this content:
@source '../../app/Providers/*.php';
Now that you have prepared TailwindCSS to track your custom classes, let's start customizing your components. Let's take a look at an example:
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { // ... TallStackUi::customize() ->form('input') ->block('input.base', 'w-full rounded-full'); // or... TallStackUi::customize('form.input') ->block('input.base', 'w-full rounded-full'); }}
In this example we are touching and replacing all the classes in the input.base block of the input component with the content: w-full rounded-full . This means that every input component displayed on the application pages will have these classes, instead of the original component classes.
At this point you may be wondering how to "discover" the blocks of each component. To do this, when browsing the documentation of each component individually you will notice a button called Customize: {Component Name} which, when clicked, will display a modal containing all the blocks - and their names, as well as the original classes that are defined by each block, for example:
Form Checkbox, Customization Blocks
Since soft customization was created to be easy to use, just like Pest , the soft customization offers a concept of fluency when using the and like a property or method. The idea behind this approach is to customize more than one component at the same time.
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { // 1. Property TallStackUi::customize() ->form('input') ->block('input.base', 'w-full rounded-full') ->and ->avatar() ->block('wrapper.sizes.sm', 'w-8 h-8 text-xs') // Or, 2. Method TallStackUi::customize() ->form('input') ->block('input.base', 'w-full rounded-full') ->and() ->avatar() ->block('wrapper.sizes.sm', 'w-8 h-8 text-xs') }}
Since a component has several blocks that organize the classes applied to the component, you can customize one block at a time or all of them at once:
use TallStackUi\Facades\TallStackUi;use App\TallStackUi\InputCustomization; class AppServiceProvider extends ServiceProvider{ public function boot(): void { TallStackUi::customize() ->form('input') ->block('input.base', new InputCustomization()) ->block('icon.wrapper', fn (array $data) => 'px-4 py-2') ->block('icon.paddings.left', 'pl-10'); // Or ... TallStackUi::customize() ->form('input') ->block([ 'input.class' => new InputCustomization(), 'icon.wrapper' => fn (array $data) => 'px-4 py-2', 'icon.paddings.left' => 'pl-10', ]); }}
You may have noticed that in the example above we used the InputPersonalization class. This is a simple invokable object class, because soft customization also allows you to make your customization into object invokable classes. This approach is ideal if you are someone who prioritizes organization above all else. Let's take a look at an example:
Preparing:
use TallStackUi\Facades\TallStackUi;use App\TallStackUi\InputCustomization; class AppServiceProvider extends ServiceProvider{ public function boot(): void { TallStackUi::customize() ->form('input') ->block('input.base', new InputCustomization()); }}
Customizing:
// You must track this namespace in the TailwindCSS config file!namespace App\TallStackUi; class InputCustomization{ public function __invoke(array $data): string { return 'w-full rounded-full'; }}
You may have noticed that the example above there is a variable called $data . This variable is an array containing all the component's properties, including the values passed when you used the component somewhere in your application.
Using the input like this:
<x-input label="Name" hint="Your full name" />
The $data will be something like:
[ "label" => "Name" "hint" => "Your full name" "icon" => null "clearable" => null "invalidate" => null "position" => "left" "prefix" => null "suffix" => null "componentName" => "input" "attributes" => Illuminate\View\ComponentAttributeBag {...} "blade" => Illuminate\View\InvokableComponentVariable {...} "customization" => Illuminate\View\InvokableComponentVariable {...} "ignoredParameterNames" => Illuminate\View\InvokableComponentVariable {...} "classes" => TallStackUi\View\Components\Form\Input::classes(?Closure $callback = null): [...] "slot" => Illuminate\View\ComponentSlot {...} "__laravel_slots" => [...] "livewire" => true "property" => null "error" => false "id" => null]
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { TallStackUi::customize() ->form('input') ->block('input.base') ->replace('rounded-md', 'rounded-full'); // Or... TallStackUi::customize() ->form('input') ->block('input.base') ->replace([ 'rounded-md' => 'rounded-full', 'border-0' => 'border-1', ]); }}
Note that in the example above we omitted the second parameter of the block method , this way we can access four useful methods that allow us to touch the component's original classes in an easy way in order to make modifications while maintaining the rest of the original content.
All the four methods:
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { TallStackUi::customize() ->form('input') ->block('input.base') // Replace: replace parts of the original content. // Accepts: // - single: from/to replace, // - array for multiples replaces in the first parameter ->replace('rounded-md', 'rounded-full'); TallStackUi::customize() ->form('input') ->block('input.base') // Remove: // - single removal // - an array for multiple removals ->remove('w-full'); TallStackUi::customize() ->form('input') ->block('input.base') // Append: appends classes as string ->append('px-4'); TallStackUi::customize() ->form('input') ->block('input.base') // Prepend: prepend classes as string ->prepend('py-4'); }}
Now that these methods have been introduced, let's imagine that you want to transform all your inputs into a fully round style to follow the look of your application, so all the work (🥵) you need to do is:
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { TallStackUi::customize() ->form('input') ->block('input.base') ->replace('rounded-md', 'rounded-full'); }}
While soft customization is powerful and easy to use, there is a catch: all soft customization are applied to all components , and you cannot assign specific customization to a component only once. However, just like in VueJS, where we have scoped CSS - CSS applied only to the component that defined the scope, soft customization offers the same concept of scoped customization - customization that will only be applied to the components that have the scope defined. Let's take a look at an example:
Notice how one input is normal while the other is fully rounded? This was only possible thanks to scoped soft customization, which instead of turning all inputs into rounded inputs, turned only the one that was defined with the circle scope. Now let's see how to achieve the same result as in the example above:
First, let's do the same soft customization via service provider:
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { TallStackUi::customize('form.input') ->scope('circle') ->block('input.wrapper') ->replace('rounded-md', 'rounded-full'); // Or ... TallStackUi::customize(component: 'form.input', scope: 'circle') ->block('input.wrapper') ->replace('rounded-md', 'rounded-full'); }}
The difference is that we must instruct that customization to be applied to a scope - defined by a unique name , and as you can see above, there are two ways to define the scope name. Just choose one of them and use it as you wish.
You have three different ways to define scopes:
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { // 1 TallStackUi::customize('form.input') ->scope('circle') ->block('input.wrapper') ->replace('rounded-md', 'rounded-full'); // Or TallStackUi::customize(component: 'form.input', scope: 'circle') ->block('input.wrapper') ->replace('rounded-md', 'rounded-full'); // Or TallStackUi::customize() ->scope('circle') ->form('input') ->block('input.wrapper') ->replace('rounded-md', 'rounded-full'); }}
Lastly and most importantly , we must apply the use of the scope to the components that are input and that we want to receive the effects of the defined customization:
<x-input ... scope="circle" />
Scopes are awesome, but they have a single problem: they could only be created, never touched. Calling scope() with the same name did not extend the existing one, it started over from the component's original classes. To solve this problem, starting from v4 you can extend a scope with extend() :
// AppServiceProvider, "boot" method. TallStackUi::customize() ->extend(scope: 'card-shadowless') ->card() ->block('wrapper.second') ->append('ring-1 ring-gray-100');
The block keeps everything the original definition did to it and the new classes go on top. Requiring the scope to exist is the point of having a separate verb: scope() creates and silently accepts a typo, extend() refuses one.
Two chains touching one block did not stack: the second silently discarded the first. Inside a single chain it already stacked, which is what made the behaviour hard to spot.
TallStackUi::customize('alert')->block('wrapper')->append('from-a');TallStackUi::customize('alert')->block('wrapper')->append('from-b'); // 3.x: 'p-4 from-b' -- from-a lost// 4.x: 'p-4 from-a from-b'
It resumes from the compiled state now, so a package and an application can each customize the same block without one erasing the other. This is also what makes extend() work.
remove() ran a plain str_replace , so removing a class also chewed through every longer class that contained its name. It works on whitespace-separated tokens now and drops only whole classes.
// block: 'mb-2 rounded-md border border-gray-300 dark:border-dark-700'->remove('border') // 3.x: 'mb-2 rounded-md -gray-300 dark:-dark-700'// 4.x: 'mb-2 rounded-md border-gray-300 dark:border-dark-700' // Several at once, as a list or as one string->remove(['shadow-md', 'rounded-lg'])->remove('shadow-md rounded-lg')
Some TallStackUI components use other TallStackUI components internally. For example, the Color Picker renders an Input, and the Date Picker renders a Floating panel. With internal scoped customization, you can target and customize these internal instances without publishing Blade templates. The second parameter of the customize method accepts the scope name of the internal component:
use TallStackUi\Facades\TallStackUi; class AppServiceProvider extends ServiceProvider{ public function boot(): void { // Customize the input used inside the color picker TallStackUi::customize(component: 'input', scope: 'form.color.input') ->block('input.base', 'rounded-full'); // Customize the floating used inside the date picker TallStackUi::customize(component: 'floating', scope: 'form.date.floating') ->block('wrapper', 'shadow-2xl'); // Customize the badge used inside sidebar items TallStackUi::customize(component: 'badge', scope: 'sidebar.item.badge') ->block('wrapper.class', 'border-0'); }}
Below is the full reference of available scopes organized by parent component.
Wrapper Components
These scopes affect all form components that use the shared wrapper infrastructure.