Skip to main content

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

NameTypeRequiredDescription
entityIdnumberstringyes
options.entityTypestringyesEntity type: subscription, order, user, product.
options.entityStatusstringyesCurrent status (e.g. approved, paused, active, pending).
options.hasNotesbooleannoWhether the entity has notes.
options.entityIntervalstringnoSubscription interval like prepaid (used by visibility rules).
positionstringnoOne 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 and role="menuitem" for items.
  • Keyboard support: Escape closes, ArrowUp/ArrowDown navigate, Enter/Space activate.
  • Visible focus indicators follow Tailwind accessibility rules (focus-visible:tw-ring-4 etc.).
  • Announceable label via aria-label on <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

  1. Add entity-specific guards and handlers in dropdown-box.js (e.g., shouldShowUserAction, executeUserAction).

  2. Add template items using x-show="shouldShowAction('your-action')" and click handlers that emit Livewire events or call Alpine methods.

  3. Implement corresponding Livewire methods and authorization checks in the backend.

  • resources/views/components/dropdown-box.blade.php
  • resources/js/alpine/alpine-components/dropdown-box.js
  • resources/js/alpine/alpine-components/ellipsis-dropdown.js

Updated for Laravel 10, Livewire 2, Alpine 2, and Tailwind 3.

X

Graph View