Dropdown box component
Introduction
The Dropdown box is a reusable, universal actions menu used across entity tables (subscriptions, orders, users, products). It listens for a window event and renders the correct action set based on the provided entity data. It adheres to WCAG 2.1 AA accessibility standards and integrates seamlessly with Livewire 2 and Alpine 2.
Files
- Blade template:
resources/views/components/dropdown-box.blade.php - Alpine logic (box):
resources/js/alpine/alpine-components/dropdown-box.js - Alpine trigger (ellipsis):
resources/js/alpine/alpine-components/ellipsis-dropdown.js
Usage
1) Include the dropdown box once per page
<x-dropdown-box aria-label="{{ trans('actions.menu') }}" />
2) Add ellipsis buttons in table rows
<button
type="button"
x-data="ellipsisDropdown({{ $entity->id }}, { entityType: 'subscription', entityStatus: '{{ $entity->status }}', hasNotes: {{ $entity->hasNotes ? 'true' : 'false' }}, entityInterval: '{{ $entity->interval ?? '' }}' }, 'bottom-left')"
x-on:click="openDropdown()"
class="tw-p-2 tw-rounded hover:tw-bg-gray-100"
aria-label="Open actions menu"
>
⋯
</button>
Ellipsis trigger API
The trigger is created with ellipsisDropdown(entityId, options = {}, position = 'bottom-left') and dispatches a dropdown-open event consumed by the box.
// resources/js/alpine/alpine-components/ellipsis-dropdown.js
export default function ellipsisDropdown(entityId, options = {}, position = 'bottom-left') {
return {
entityId,
position,
options,
openDropdown() { /* ... */ },
};
}
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
entityId | number | string | yes |
options.entityType | string | yes | Entity type: subscription, order, user, product. |
options.entityStatus | string | yes | Current status (e.g. approved, paused, active, pending). |
options.hasNotes | boolean | no | Whether the entity has notes. |
options.entityInterval | string | no | Subscription interval like prepaid (used by visibility rules). |
position | string | no | One of bottom-left, bottom-right, top-left, top-right. Default: bottom-left. |
Box behavior
The dropdown box listens for dropdown-open and updates its state accordingly.
// resources/js/alpine/alpine-components/dropdown-box.js (excerpt)
openWith(detail) {
this.entityId = detail.entityId;
this.position = detail.position || 'bottom-right';
this.left = detail.left || 0;
this.top = detail.top || 0;
this.entityType = detail.options.entityType;
this.entityStatus = detail.options.entityStatus;
this.hasNotes = detail.options.hasNotes;
this.interval = detail.options.entityInterval;
// ...
}
Showing actions
Use the helper shouldShowAction(action) which routes to entity-specific guards (currently subscription).
<x-dropdown.item
role="menuitem"
x-show="shouldShowAction('pause')"
x-on:click="$wire.emit('subscriptionPause', entityId); close()"
>
{{ trans('subscriptions.actions.pause') }}
</x-dropdown.item>
Current subscription logic:
// pause only when approved and not prepaid
case 'pause':
return this.entityStatus === 'approved' && this.interval !== 'prepaid';
// resume only when paused
case 'resume':
return this.entityStatus === 'paused';
// cancel when paused or approved
case 'cancel':
return ['paused', 'approved'].includes(this.entityStatus);
Accessibility
- Uses
role="menu"on the container androle="menuitem"for items. - Keyboard support:
Escapecloses,ArrowUp/ArrowDownnavigate,Enter/Spaceactivate. - Visible focus indicators follow Tailwind accessibility rules (
focus-visible:tw-ring-4etc.). - Announceable label via
aria-labelon<x-dropdown-box>.
Livewire integration
The box works with Livewire by emitting events from actions and listening to custom events as needed.
// In a Livewire component
protected $listeners = ['subscriptionPause' => 'pause'];
public function pause(int $subscriptionId): void
{
// Domain logic...
$this->dispatchBrowserEvent('notify', [
'type' => 'success',
'message' => trans('subscriptions.paused'),
]);
}
Positions
Supported positions: bottom-left, bottom-right, top-left, top-right.
The trigger computes absolute coordinates; the box adjusts transform so the dropdown stays within the viewport.
Extending for a new entity
-
Add entity-specific guards and handlers in
dropdown-box.js(e.g.,shouldShowUserAction,executeUserAction). -
Add template items using
x-show="shouldShowAction('your-action')"and click handlers that emit Livewire events or call Alpine methods. -
Implement corresponding Livewire methods and authorization checks in the backend.
Related files
resources/views/components/dropdown-box.blade.phpresources/js/alpine/alpine-components/dropdown-box.jsresources/js/alpine/alpine-components/ellipsis-dropdown.js
Updated for Laravel 10, Livewire 2, Alpine 2, and Tailwind 3.