TallStackUI 4.0 is here! Seven new components and thousand of improvements. See what's new .

Powerful suite of Blade components for TALL Stack apps

Upgrade Guide

Three years later, here I am shipping TallStackUI 4. And you know what? I still have the same excitement I had on the first days. Version 4 comes with seven new components, and also thousands of invisible fixes. Things nobody reported, but they were there as bugs. I fixed a lot.

Before I start describing the upgrade guide, like I always do, I want to thank you for using TallStackUI and for reading this. If you are here, you have some interest in it, even if it is just to know the basics of what TallStackUI is. I created TallStackUI to be my own source of components for TALL projects, with Livewire as the main foundation, so I would not depend on third parties and would not have to pay for a component library.

Now let's talk about what matters. First of all, v4 requires Livewire 4 exclusively. And also, before you go through the actual upgrade guide, remember I currently work on this library alone. If you want to help, you are welcome to sponsor the project.

🥳 New Components:

  • - Chart : five types as inline SVG, no charting library.
  • - Editor : WYSIWYG, no extra JavaScript, HTML or Markdown.
  • - Gallery : three layouts and a shared lightbox.
  • - QrCode : ISO/IEC 18004, no dependency.
  • - Spinner : thirteen animated variations.
  • - Swap : cycle a value with buttons, drag, or the keyboard.
  • - Radio Group and Checkbox Group : a set of options from one array, in four presentations.
  • - Upload Async : chunked uploads straight to your own controller.

👀 Cross-cutting:

  • - skeleton on Card, Stats, Table, List, Step, Chart and QrCode: a placeholder for the first render.
  • - compact on Table, List and KeyValue: tighter rows. Each affected block gained a -compact twin, so if you customize both modes you have to cover both blocks.
  • - paddingless on Modal, Slide, Card, Tab and Errors.
  • - shadowless and bordered on Card, Stats, Calendar, Tab and Errors, plus shadowless alone on Alert, Accordion and Kbd.
  • - Footer slot alignment through start , center , end , between and unwrapped on Modal, Slide, Card and Errors.
  • - A neutral oklch() dark palette and a violet secondary .
  • - floating_scroll_lock , which locks page scroll while a popup is open.
  • - Three dependencies dropped: tippy.js , clipboard.js and qs .
  • - x-table renders outside Livewire, and so do Autocomplete, Calendar and Upload Async.
  • - Global defaults in config/tallstackui.php for around twenty components: the flat-look flags, the Table props, Link's navigate , Back to Top, Modal, Kbd, Accordion, Color, Number, Icon, Tooltip, Spinner, Swap and the Button spinner. The inline prop always wins.
  • - select="label:name|value:id" remapping on Autocomplete, both selects, the selection groups, Swap and the Command Palette, inline or from the configuration.
  • - Eleven size and 29 color shorthands on Icon , and a size scale up to 7xl plus gravatar on Avatar .
  • - Tooltip was rebuilt inside the package, with delay , balloon , scale , data-tooltip-disabled and global settings.
  • - --tsui-scrollbar-offset and .tsui-scrollbar-bleed , so a full-bleed element still reaches the edge while the page is locked.
  • - extend() , to change a scope that already exists instead of starting it over.

The rest is on each component page.

Read the ones that apply to you. Everything else looks the same as 3.x .

1. Card: bordered became accent

On Card, bordered never drew a border around the card. Together with color , it turned the header from a filled background into a colored top border. That name is now the wrapper border, so the header look moved to accent . If you still pass bordered , the header stops being colored and you get the neutral wrapper border instead.

<x-card color="red" bordered header="Report">...</x-card>
 
<x-card color="red" accent header="Report">...</x-card>

2. Kbd: borderless keeps the shadow

It used to strip the border and the shadow, so you could not drop one without the other. Now it only removes the border. Use shadowless if you want the shadow gone too.

<x-kbd borderless />
 
<x-kbd borderless shadowless />

3. Clipboard: icons became icon

The two attributes only worked together. icons did nothing without icon , and icon alone was how you asked for the default pair. Now icon takes the array, and passing an array turns icon mode on by itself.

<x-clipboard text="TallStackUI" icon :icons="['copy' => 'pencil', 'copied' => 'check']" />
 
<x-clipboard text="TallStackUI" :icon="['copy' => 'pencil', 'copied' => 'check']" />

icons is gone, not deprecated. The attribute bag drops an array attribute the component does not declare, so a leftover :icons does nothing and does not error. A key other than copy and copied now raises instead of falling back to the default icon.

4. Theme: secondary is violet, dark is neutral

--color-secondary-* is Tailwind Violet now, and components no longer use it. --color-dark-* left Slate for a neutral oklch() scale. If you override either in your own @theme , your values still win.

<!-- Was slate-gray, is violet now -->
<x-badge color="secondary" text="Draft" />
 
<x-badge color="slate" text="Draft" />

If you replace() one of the old dark-700 / dark-600 values, or a *-secondary-* class inside a component block, point it at the new step and the gray-* equivalent. Twelve hardcoded dark:*-gray-* / dark:*-slate-* classes moved to the matching dark-* shade (Progress, Upload, Number, Color, Step, Timeline, Gallery and Carousel), so a replace() aimed at the old class no longer finds it. Details are on the color page .

5. Icon: a bare icon has a size, and 40 names are reserved

<x-icon name="users" /> used to reach the browser with no width and no height. It now falls back to md (20px). Only calls with no class at all are affected, and those were already rendering at an unusable size. Eleven sizes and 29 colors are taken off the attribute bag, so they cannot be forwarded to the svg for something else. Passing class turns both shorthands off, including class="" . That is why the 190 internal icon usages in the package still look the same.

6. No more external JS dependencies

tippy.js , clipboard.js and qs left package.json . Tooltip, Clipboard and Select Styled stay the same from the outside, and @tallStackUiScript needs no change. An application importing clipboard or qs directly has to install it on its own.

Anything that reached for $el._tippy to enable or disable a tooltip has to move to the attribute. A balloon styled through tippy's theme classes has to be restyled through [data-tsui-tooltip] .

x-effect="$el._tippy && ($store['tsui.side-bar'].open ? $el._tippy.disable() : $el._tippy.enable())"
 
x-bind:data-tooltip-disabled="$store['tsui.side-bar'].open"

7. Soft customization is stricter

Two chains on the same block did not stack. The second one silently dropped the first. They accumulate now, so a package and an app can customize the same block. If you were relying on the last chain winning, collapse them into one chain.

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'

remove() ran a plain str_replace , so it also ate every longer class that contained the name you gave it.

// 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'

Three calls that used to pass silently now throw, and one global changed how it accumulates.

// AppServiceProvider, "boot" method.
 
// 3.x: quietly built a scope named "main"; 4.x: throws
TallStackUi::customize('badge.main')->block('wrapper', '...');
 
// 3.x: wrote nowhere; 4.x: throws
TallStackUi::customize('alert')->append('foo-bar');
 
// 3.x: registered twice, so the narrowing never took effect; 4.x: the last call wins
TallStackUi::customize()->globals()->colorful();
TallStackUi::customize()->globals()->colorful(toast: false);

A scope also layers over the global customization instead of replacing it, so <x-card scope="card-shadowless" /> keeps whatever you customized on Card globally. And the square global matches whole tokens now: rounded-[10px] is removed cleanly, while not-rounded and unrounded-md stay.

8. Button: radius blocks and spinner animation

wrapper.border.radius.rounded and wrapper.border.radius.circle were a two-entry map for a two-state prop. The radius blocks are a size map now, in the same place Badge and Environment keep theirs.

// AppServiceProvider, "boot" method.
 
TallStackUi::customize()->button()->block('wrapper.border.radius.rounded', 'rounded-2xl');
 
TallStackUi::customize()->button()->block('wrapper.border.radius.circle', '...');
 
TallStackUi::customize()->button()->block('border.radius.md', 'rounded-2xl');
 
TallStackUi::customize()->button()->block('border.radius.full', '...');

icon.spinner-animation is gone from both buttons. Each spinner variant has its own animation under the new spinner.* blocks. The loading indicator no longer reads icon.sizes.* . That block still applies to regular icons.

// AppServiceProvider, "boot" method.
 
TallStackUi::customize()->button()->block('icon.spinner-animation', 'animate-pulse');
 
TallStackUi::customize()->button()->block('spinner.gradient.base', 'inline-block animate-pulse');

round is validated now. The button had no validate() before, so a value outside the six sizes ( 2xl and circle among them) throws at render time instead of being ignored.

9. Table: paginator view split, scope removed

components/table/paginators.blade.php no longer exists. It became paginators/simple.blade.php , and the directory has one file per variation.

<x-table :$headers :$rows paginate paginator="components.table.paginators" />
 
<x-table :$headers :$rows paginate paginator="components.table.paginators.simple" />

paginator is no longer only a view path. It also names a look: simple , minimal , or compact . A value with a . or :: is still a view, so your own paginator still works. Anything else raises a validation exception listing the bundled names.

The data a custom paginator view receives also changed. scrollTo and simplePagination became scroll , simple , name , dusk and fragment . The paginator markup changed class by class, so CSS aimed at the old classes needs updating.

The table-shadowless scope had nothing left to remove, so it was dropped. Remove scope="table-shadowless" from tables. Rendering stays the same, but extending that scope now throws.

10. Step: previous and next are slot names

A stray bare previous or next used to fall through to the attribute bag. It now hits a slot prop and throws.

<!-- previous and next are slot names now: a bare attribute throws -->
<x-step selected="1" helpers previous>
 
<x-step selected="1" helpers navigate-previous>

The navigation buttons also left soft customization. button.base , button.icon and button.icon-spacing.* are gone. Restyle the bar by picking a variation, replacing the buttons through the slots, or pointing helpers at your own view.

helpers itself went from a flag to a variation name, the same way Table's paginator works: default , minimal , compact , or a view path. A string that names none of them throws. Before, any truthy value was enough.

11. List: dividers are keyed on a visibility marker

Hidden rows still take part in CSS sibling matching, which painted a phantom divider above the first visible row after a search. If you override items.wrapper , key the dividers on data-list-on , not data-list-row , or that line comes back.

// AppServiceProvider, "boot" method.
 
// Dividers must be keyed on data-list-on, not data-list-row
TallStackUi::customize()
->list()
->block('items.wrapper', '[&>[data-list-on]~[data-list-on]]:border-t');

The row also gained a wrapper. When action and/or menu are present, both sit inside a new content.aside block, so a row that used to render only a menu now has one extra div . If you select the menu wrapper by DOM position instead of by class, you will need to adjust that.

12. Colorful: three blocks became color classes

Everything that changes by notification type now lives in the color classes.

// AppServiceProvider, "boot" method.
 
// These three block no longer exist and now throw
TallStackUi::customize()->dialog()->block('colorful.confirm', '...');
 
TallStackUi::customize()->toast()->block('colorful.confirm', '...');
 
TallStackUi::customize()->toast()->block('colorful.cancel', '...');
 
// Publish the color classes and override colorfulColors() instead
php artisan tallstackui:setup-color

The question type also follows primary instead of a grayscale palette, and the two buttons no longer share the same translucent background.

13. Footer slots gained a wrapper

Modal, Slide, Card and Errors now read footer alignment from the slot. That split their blocks and nested one extra div .

Component 3.x 4.x
Modal footer footer.wrapper + footer.base
Slide footer.base footer.wrapper + footer.base
Card footer.text footer.base
Errors slots.footer slots.footer.wrapper + slots.footer.base

Two behaviours moved with them. A Slide footer with no attribute now defaults to the end, not the start. A Card footer passed as a slot is aligned instead of falling through raw. If you were relying on that raw fall-through, use unwrapped .

14. Blocks that no longer exist

Customizing a block that is gone throws, so these are the ones to grep for. Button, Step and Colorful are already covered above.

Component 3.x 4.x
Date floating.expanded box.picker.expanded, and h-[17rem] became min-h-[17rem]
Calendar floating.default, floating.class, box.picker.button-label-wrapper, box.picker.navigate-wrapper removed with the floating panel
Input input.addon.button.left and .right moved into input.addon.button.base
Carousel images.rounded images.rounded.*: a default key plus one per size

The calendar's month and year pickers are now a copy of the date picker, rendered in place instead of as a popover, so the calendar.floating internal scope has nothing left to point at. box.picker.wrapper.second and third carry the date picker values, and box.picker.today is just cursor-pointer .

// The calendar renders no floating anymore, so the internal scope is gone
TallStackUi::customize('floating', scope: 'calendar.floating')->block('default', '...');

15. Blocks that changed shape

These kept their names but hold something different now. If you customized them, you will need to revisit those customizations.

Component Blocks
KeyValue wrapper, header.wrapper, button.add and list.divider changed; header.neutral and button.neutral are new
Layout the padding transition left wrapper.second.expanded / .collapsed for wrapper.second.transition; wrapper.second.footer and main.grow are new; the header wrapper traded shadow-sm and border-gray-300/10 for a solid border-gray-200
SideBar item.state.base and group.button lost their gap; item.badge and group.badge now style the wrapper around the badge; simple.wrapper lost its padding; item.state.gap, group.button.gap, group.button.collapsed, simple.wrapper.visible / .hidden and the group.flyout.* set are new
Step panels-shape carries the frame, wrapper.panels carries the scroll only; the inactive rings, bars and titles were recolored, so a customization replacing the old values has to target the new ones
Stats wrapper.second-no-slot moved into wrapper.second; header and footer split into .text and .wrapper
Toast wrapper.position gained x-center and top-on-mobile; a stack.* group is new

16. Smaller behaviour changes

  • Currency is 12px tighter on each side, because the slot paddings became !important and now actually apply.
  • Currency no longer re-emits name on the visible input, so a native form receives the raw value instead of the formatted one.
  • Radio, Checkbox and Toggle suffix the generated id with the option value, so a group renders plan-basic instead of three inputs sharing one id.
  • Timeline items in the slot inherit horizontal , alternate , compact , color and style from the container. You no longer have to repeat them.
  • Select Styled qualifies a selected grouped item with its group name. A test that asserts the exact text has to expect that qualified form.
  • simple-pagination implies paginate . Passing both is just redundant.
  • A published config list of scalars is taken as published instead of being merged entry by entry, so it can be shorter than the default.
  • A nested wire:model reads correctly on the server now. wire:model="form.files" and any other dotted binding used to resolve to null . That is why KeyValue threw and Upload listed nothing.
  • Reaction's panel markup changed shape. Anything that selected into the old structure (a browser test walking an XPath, especially) has to be pointed at the new one.
  • Modal's center accepts a breakpoint now, and it is validated. center="true" written as a quoted attribute throws, because Blade hands it over as a string and no positions.center-true block exists.
  • Avatar reads its eleven size names from the attribute bag and strips them, the same way Icon does, and two at once throws. A medium avatar renders its initials at text-base instead of inheriting the container's size, because the old text-md is not a Tailwind class. A published avatar view now reads $scale and $src .
  • <x-loading> locks the body scroll for real. Livewire.hook('commit.prepare') does not exist in Livewire 4 and failed silently, so only the unlocking half ever ran.
  • Escape closes the topmost popup first and the overlay behind it on the second press, instead of both at once. Closing a popup also hands focus back to its anchor.
  • Radio and Checkbox print the validation message once per property instead of once per option.
  • Stats throws when increase and decrease are combined, colors the number through the color prop rather than a hardcoded text-primary-500 , and renders a clickable card without href as a div .
  • The custom palette of <x-color> is read from colors in the configuration. The documented custom key was never read, so anything set under it has to be renamed.

1. Read the breaking changes above

Only the ones that apply to you. If you write no soft customization and pass none of the renamed attributes, there is nothing to do here.

2. Republish the configuration file

config/tallstackui.php gained the floating_scroll_lock key, per-component settings for the new components, and a global default for most of the props that used to be repeated at every call site. Compare your file with the new one and sync them. Keys your file does not mention keep their default, so nothing is lost if you leave it alone.

Two notes while you are in there. A published list of scalars is taken as published now, instead of being merged entry by entry, so table.quantity or editor.allowed_tags can be shorter than the default. And the custom palette of <x-color> moved from custom to colors .

3. Redo deep customization

If you published TallStackUI Blade files or extended component classes, back up your files, republish the new ones, and reapply your adjustments. If you do not use Deep Customization, skip this step.

4. Clear the application cache

Clear cached files before updating Composer:

php artisan optimize:clear

5. Update Composer

Edit your composer.json . Livewire ^4.3 is required now, so if you are still on Livewire 3, upgrade that first:

"require": {
"laravel/framework": "^13.0",
"tallstackui/tallstackui": "^3.0",
"tallstackui/tallstackui": "^4.0",
"livewire/livewire": "^4.0",
 
// ...
},

After that, remove the vendor folder and run:

composer install

6. Rebuild your assets

The palette moved, so you have to regenerate the CSS. If you override --color-secondary-* or --color-dark-* in your own @theme , this is also where you check that your values still win.

If you notice anything wrong with this guide, please open an issue.

Code highlighting provided by Torchlight