Skip to content

Instantly share code, notes, and snippets.

@wsydney76
Last active August 8, 2026 08:37
Show Gist options
  • Select an option

  • Save wsydney76/128643bed2e7b4c821d98de3224e4eed to your computer and use it in GitHub Desktop.

Select an option

Save wsydney76/128643bed2e7b4c821d98de3224e4eed to your computer and use it in GitHub Desktop.
Reusable dialog blade component - extends <flux:modal> with title/footer sections
@use(Flux\Flux)
@props([
'name' => 'modal',
'title' => config('app.name'),
'buttons' => null,
'extraHeader' => null,
'cancelText' => 'Close',
'fixedHeight' => '',
'showFooter' => true,
])
@php
$titleAttributes = Flux::attributesAfter('title:', $attributes, []);
$wrapperAttributes = Flux::attributesAfter('wrapper:', $attributes, []);
$contentAttributes = Flux::attributesAfter('content:', $attributes, []);
$footerAttributes = Flux::attributesAfter('footer:', $attributes, []);
$contentColor = $titleColor = $footerColor = $attributes->get('color', 'zinc');
$colorAttributes = Flux::attributesAfter('color:', $attributes, []);
foreach (['title', 'content', 'footer'] as $section) {
if ($colorAttributes->has($section)) {
${$section . 'Color'} = $colorAttributes->get($section);
}
}
$isFlyout = $attributes->has('flyout');
if ($attributes->get('position') === 'bottom') {
throw new \InvalidArgumentException('position="bottom" is not supported');
}
if ($attributes->get('variant') === 'floating') {
throw new \InvalidArgumentException('variant="floating" is not supported');
}
@endphp
<flux:modal
name="{{ $name }}"
{{
$attributes->class([
'overflow-hidden p-0!',
'h-[100dvh] max-h-[100dvh]' => $isFlyout,
])
}}
:closable="false"
>
<div
data-dialog-wrapper
{{
$wrapperAttributes->class([
'flex min-h-0 flex-col',
'max-h-[80vh]' => ! $isFlyout,
'h-[100dvh] max-h-[100dvh]' => $isFlyout,
])
}}
>
{{-- Title bar --}}
<div
data-dialog-title
{{
$titleAttributes->class([
'flex shrink-0 items-center justify-between border-b px-6 py-2 font-semibold',
'sticky top-0 z-10' => $isFlyout,
'bg-red-300' => $titleColor === 'red',
'dark:bg-red-800' => $titleColor === 'red',
'bg-orange-300' => $titleColor === 'orange',
'dark:bg-orange-800' => $titleColor === 'orange',
'bg-amber-300' => $titleColor === 'amber',
'dark:bg-amber-800' => $titleColor === 'amber',
'bg-yellow-300' => $titleColor === 'yellow',
'dark:bg-yellow-800' => $titleColor === 'yellow',
'bg-lime-300' => $titleColor === 'lime',
'dark:bg-lime-800' => $titleColor === 'lime',
'bg-green-300' => $titleColor === 'green',
'dark:bg-green-800' => $titleColor === 'green',
'bg-emerald-300' => $titleColor === 'emerald',
'dark:bg-emerald-800' => $titleColor === 'emerald',
'bg-teal-300' => $titleColor === 'teal',
'dark:bg-teal-800' => $titleColor === 'teal',
'bg-cyan-300' => $titleColor === 'cyan',
'dark:bg-cyan-800' => $titleColor === 'cyan',
'bg-sky-300' => $titleColor === 'sky',
'dark:bg-sky-800' => $titleColor === 'sky',
'bg-blue-300' => $titleColor === 'blue',
'dark:bg-blue-800' => $titleColor === 'blue',
'bg-indigo-300' => $titleColor === 'indigo',
'dark:bg-indigo-800' => $titleColor === 'indigo',
'bg-violet-300' => $titleColor === 'violet',
'dark:bg-violet-800' => $titleColor === 'violet',
'bg-purple-300' => $titleColor === 'purple',
'dark:bg-purple-800' => $titleColor === 'purple',
'bg-fuchsia-300' => $titleColor === 'fuchsia',
'dark:bg-fuchsia-800' => $titleColor === 'fuchsia',
'bg-pink-300' => $titleColor === 'pink',
'dark:bg-pink-800' => $titleColor === 'pink',
'bg-rose-300' => $titleColor === 'rose',
'dark:bg-rose-800' => $titleColor === 'rose',
'bg-slate-300' => $titleColor === 'slate',
'dark:bg-slate-800' => $titleColor === 'slate',
'bg-gray-300' => $titleColor === 'gray',
'dark:bg-gray-800' => $titleColor === 'gray',
'bg-zinc-300' => $titleColor === 'zinc',
'dark:bg-zinc-800' => $titleColor === 'zinc',
'bg-neutral-300' => $titleColor === 'neutral',
'dark:bg-neutral-800' => $titleColor === 'neutral',
'bg-stone-300' => $titleColor === 'stone',
'dark:bg-stone-800' => $titleColor === 'stone',
'bg-taupe-300' => $titleColor === 'taupe',
'dark:bg-taupe-800' => $titleColor === 'taupe',
'bg-mauve-300' => $titleColor === 'mauve',
'dark:bg-mauve-800' => $titleColor === 'mauve',
'bg-mist-300' => $titleColor === 'mist',
'dark:bg-mist-800' => $titleColor === 'mist',
'bg-olive-300' => $titleColor === 'olive',
'dark:bg-olive-800' => $titleColor === 'olive',
])
}}
>
<flux:heading size="lg">
{{ $title }}
</flux:heading>
<flux:modal.close>
<flux:button icon="x-mark" variant="ghost" size="xs" />
</flux:modal.close>
</div>
{{-- Extra header slot --}}
@if ($extraHeader)
{{ $extraHeader }}
@endif
{{-- Scrollable content --}}
<div
data-dialog-content
{{
$contentAttributes->class([
'min-h-0 flex-1 overflow-y-auto px-6 py-4',
'max-h-none' => $isFlyout,
'max-h-[20vh] min-h-[20vh]' => $fixedHeight == 'xs',
'max-h-[40vh] min-h-[40vh]' => $fixedHeight == 'sm',
'max-h-[50vh] min-h-[50vh]' => $fixedHeight == 'md',
'max-h-[60vh] min-h-[60vh]' => $fixedHeight == 'lg',
'max-h-[70vh] min-h-[70vh]' => $fixedHeight == 'xl',
'max-h-[80vh] min-h-[80vh]' => $fixedHeight == '2xl',
'bg-red-50' => $contentColor === 'red',
'dark:bg-red-950' => $contentColor === 'red',
'bg-orange-50' => $contentColor === 'orange',
'dark:bg-orange-950' => $contentColor === 'orange',
'bg-amber-50' => $contentColor === 'amber',
'dark:bg-amber-950' => $contentColor === 'amber',
'bg-yellow-50' => $contentColor === 'yellow',
'dark:bg-yellow-950' => $contentColor === 'yellow',
'bg-lime-50' => $contentColor === 'lime',
'dark:bg-lime-950' => $contentColor === 'lime',
'bg-green-50' => $contentColor === 'green',
'dark:bg-green-950' => $contentColor === 'green',
'bg-emerald-50' => $contentColor === 'emerald',
'dark:bg-emerald-950' => $contentColor === 'emerald',
'bg-teal-50' => $contentColor === 'teal',
'dark:bg-teal-950' => $contentColor === 'teal',
'bg-cyan-50' => $contentColor === 'cyan',
'dark:bg-cyan-950' => $contentColor === 'cyan',
'bg-sky-50' => $contentColor === 'sky',
'dark:bg-sky-950' => $contentColor === 'sky',
'bg-blue-50' => $contentColor === 'blue',
'dark:bg-blue-950' => $contentColor === 'blue',
'bg-indigo-50' => $contentColor === 'indigo',
'dark:bg-indigo-950' => $contentColor === 'indigo',
'bg-violet-50' => $contentColor === 'violet',
'dark:bg-violet-950' => $contentColor === 'violet',
'bg-purple-50' => $contentColor === 'purple',
'dark:bg-purple-950' => $contentColor === 'purple',
'bg-fuchsia-50' => $contentColor === 'fuchsia',
'dark:bg-fuchsia-950' => $contentColor === 'fuchsia',
'bg-pink-50' => $contentColor === 'pink',
'dark:bg-pink-950' => $contentColor === 'pink',
'bg-rose-50' => $contentColor === 'rose',
'dark:bg-rose-950' => $contentColor === 'rose',
'bg-slate-50' => $contentColor === 'slate',
'dark:bg-slate-950' => $contentColor === 'slate',
'bg-gray-50' => $contentColor === 'gray',
'dark:bg-gray-950' => $contentColor === 'gray',
'bg-zinc-50' => $contentColor === 'zinc',
'dark:bg-zinc-950' => $contentColor === 'zinc',
'bg-neutral-50' => $contentColor === 'neutral',
'dark:bg-neutral-950' => $contentColor === 'neutral',
'bg-stone-50' => $contentColor === 'stone',
'dark:bg-stone-950' => $contentColor === 'stone',
'bg-taupe-50' => $contentColor === 'taupe',
'dark:bg-taupe-950' => $contentColor === 'taupe',
'bg-mauve-50' => $contentColor === 'mauve',
'dark:bg-mauve-950' => $contentColor === 'mauve',
'bg-mist-50' => $contentColor === 'mist',
'dark:bg-mist-950' => $contentColor === 'mist',
'bg-olive-50' => $contentColor === 'olive',
'dark:bg-olive-950' => $contentColor === 'olive',
'bg-white' => $contentColor === 'white',
'dark:bg-zinc-950' => $contentColor === 'white',
])
}}
>
{{ $slot }}
</div>
{{-- Footer bar --}}
@if ($showFooter)
<div
data-dialog-footer
{{
$footerAttributes->class([
'shrink-0 border-t px-6 py-4',
'sticky bottom-0 z-10' => $isFlyout,
'bg-red-100' => $footerColor === 'red',
'dark:bg-red-800' => $footerColor === 'red',
'bg-orange-100' => $footerColor === 'orange',
'dark:bg-orange-800' => $footerColor === 'orange',
'bg-amber-100' => $footerColor === 'amber',
'dark:bg-amber-800' => $footerColor === 'amber',
'bg-yellow-100' => $footerColor === 'yellow',
'dark:bg-yellow-800' => $footerColor === 'yellow',
'bg-lime-100' => $footerColor === 'lime',
'dark:bg-lime-800' => $footerColor === 'lime',
'bg-green-100' => $footerColor === 'green',
'dark:bg-green-800' => $footerColor === 'green',
'bg-emerald-100' => $footerColor === 'emerald',
'dark:bg-emerald-800' => $footerColor === 'emerald',
'bg-teal-100' => $footerColor === 'teal',
'dark:bg-teal-800' => $footerColor === 'teal',
'bg-cyan-100' => $footerColor === 'cyan',
'dark:bg-cyan-800' => $footerColor === 'cyan',
'bg-sky-100' => $footerColor === 'sky',
'dark:bg-sky-800' => $footerColor === 'sky',
'bg-blue-100' => $footerColor === 'blue',
'dark:bg-blue-800' => $footerColor === 'blue',
'bg-indigo-100' => $footerColor === 'indigo',
'dark:bg-indigo-800' => $footerColor === 'indigo',
'bg-violet-100' => $footerColor === 'violet',
'dark:bg-violet-800' => $footerColor === 'violet',
'bg-purple-100' => $footerColor === 'purple',
'dark:bg-purple-800' => $footerColor === 'purple',
'bg-fuchsia-100' => $footerColor === 'fuchsia',
'dark:bg-fuchsia-800' => $footerColor === 'fuchsia',
'bg-pink-100' => $footerColor === 'pink',
'dark:bg-pink-800' => $footerColor === 'pink',
'bg-rose-100' => $footerColor === 'rose',
'dark:bg-rose-800' => $footerColor === 'rose',
'bg-slate-100' => $footerColor === 'slate',
'dark:bg-slate-800' => $footerColor === 'slate',
'bg-gray-100' => $footerColor === 'gray',
'dark:bg-gray-800' => $footerColor === 'gray',
'bg-zinc-100' => $footerColor === 'zinc',
'dark:bg-zinc-800' => $footerColor === 'zinc',
'bg-neutral-100' => $footerColor === 'neutral',
'dark:bg-neutral-800' => $footerColor === 'neutral',
'bg-stone-100' => $footerColor === 'stone',
'dark:bg-stone-800' => $footerColor === 'stone',
'bg-taupe-100' => $footerColor === 'taupe',
'dark:bg-taupe-800' => $footerColor === 'taupe',
'bg-mauve-100' => $footerColor === 'mauve',
'dark:bg-mauve-800' => $footerColor === 'mauve',
'bg-mist-100' => $footerColor === 'mist',
'dark:bg-mist-800' => $footerColor === 'mist',
'bg-olive-100' => $footerColor === 'olive',
'dark:bg-olive-800' => $footerColor === 'olive',
])
}}
>
<div class="flex justify-end gap-2">
@if ($cancelText)
<flux:modal.close>
<flux:button variant="filled" size="sm">
{{ $cancelText }}
</flux:button>
</flux:modal.close>
@endif
<flux:spacer />
{{-- Buttons slot --}}
@if ($buttons)
{{ $buttons }}
@endif
</div>
</div>
@endif
</div>
</flux:modal>

Modal Dialog Component (<x-dialog>)

This document describes how to use a reusable Blade modal component.

Basic usage

<x-dialog
    name="playlist-modal"
    title="Select a song"
    fixed-height="md"
    content:class="space-y-2"
>
    <p>Choose one of the songs below.</p>
</x-dialog>

How it works

<x-dialog> wraps a flux:modal and renders three main areas:

  1. Title bar (header with title + close button)
  2. Scrollable content (your default slot)
  3. Footer (cancel button + optional custom action buttons, can be hidden)

The modal is rendered with :closable="false", so close behavior is handled via the built-in close buttons.

Props

Prop Type Default Description
name string modal Required unique modal name/id for Flux modal targeting.
title string config('app.name') Header title text.
buttons named slot content (<x-slot name="buttons">) null Optional custom footer actions rendered on the right (next to cancel).
extraHeader named slot content (<x-slot name="extraHeader">) null Optional content rendered directly under the title bar.
color string zinc Base color variant used for title/content/footer backgrounds.
cancelText string Close Label for the default cancel/close button in the footer.
fixedHeight string '' Optional fixed content area height preset.
showFooter bool true Show or hide the entire footer section.

Attribute targeting (prefix:...)

buttons and extraHeader are typically provided as named slots inside <x-dialog>, not as scalar string props.

The component supports scoped attribute forwarding:

  • title:* -> applies to the title bar wrapper
  • wrapper:* -> applies to the outer modal body wrapper
  • content:* -> applies to the scrollable content area
  • footer:* -> applies to the footer bar

Color targeting is also supported:

  • color:title="..." -> overrides title bar color only
  • color:content="..." -> overrides content background color only
  • color:footer="..." -> overrides footer background color only

Most commonly, you will use *:class to add utility classes:

<x-dialog
    name="user-details"
    title="User Details"
    wrapper:class="max-w-3xl"
    title:class="text-slate-900"
    content:class="space-y-4"
    footer:class="bg-slate-50"
>
    ...
</x-dialog>

Data attributes for styling

The component now exposes stable data attributes on key wrapper elements so you can target them from global CSS when utility classes are not enough:

  • data-dialog-wrapper -> outer dialog body wrapper
  • data-dialog-title -> title bar container
  • data-dialog-content -> scrollable content area
  • data-dialog-footer -> footer container (when showFooter is true)

Example:

<style>
    [data-dialog-content] {
        background-color: red;
    }

    [data-dialog-title] [data-flux-heading] {
        color: red;
    }
</style>

Flux components (including the modal shell itself) also add their own data-flux-* attributes. You can combine dialog-level and Flux-level selectors for precise targeting.

color options

The component supports Tailwind's 4.2 color names for color, color:title, color:content, and color:footer, with a default of zinc.

Depending on your project's standards, you may want to remove unwanted colors, and/or adjust the colors.

fixed-height options

Use kebab-case in Blade (fixed-height), mapped to fixedHeight internally.

fixed-height Content area height
xs 20vh
sm 40vh
md 50vh
lg 60vh
xl 70vh
2xl 80vh

If you want to set a custom height, set min-height and max-height on the content area to the same value.

E.g. content:style="min-height: 400px; max-height: 400px;" or content:class="max-h-32 min-h-32"

This is required to overrule the default flexbox flex-x/shrink behavior of the default wrapper.

If omitted, content height is flexible and constrained by modal max height.

Custom footer buttons

Use a named slot (buttons) to append action buttons in the footer. These render to the right of the default cancel button.

<x-dialog name="edit-talk" :title="$mode === 'update' ? 'Update Talk' : 'Create Talk'">
    <form id="edit-form" wire:submit="save">
        ...
    </form>
    <x-slot name="buttons">
        <flux:button variant="filled" wire:click="clearForm" size="sm">Reset</flux:button>
        <flux:button type="submit" form="edit-form" variant="primary" color="sky" size="sm">
            {{ $mode === 'update' ? 'Save Talk' : 'Create Talk' }}
        </flux:button>
    </x-slot>
</x-dialog>

The buttons slot is rendered outside the content area, so you have to use an id on your form and form="..." on your submit button to link them.

Footer visibility and cancel button

  • Set :show-footer="false" to hide the entire footer block.
  • Set cancel-text="" to hide only the cancel button while still rendering custom buttons.

Extra header content

Use extraHeader for helper text, tabs, filters, or context directly below the title bar:

<x-dialog
    name="search-modal"
    title="Advanced Search"
>
    <x-slot name="extraHeader">
        <flux:input type="search" placeholder="Search speakers..." size="sm" />
    </x-slot>

    ...
</x-dialog>

Complete example

<x-dialog
    name="playlist-modal"
    title="Select a song"
    color="indigo"
    color:content="zinc"
    color:footer="sky"
    fixed-height="md"
    cancel-text="Cancel"
    content:class="space-y-2"
    title:class="text-slate-900"
>
    <ul class="space-y-2">
        <li>Song A</li>
        <li>Song B</li>
        <li>Song C</li>
    </ul>
</x-dialog>

Flyout

A flyout modal is partially supported.

<x-dialog
    name="playlist-modal"
    title="Select a song"
    flyout
    position="left"
>
  ...
</x-dialog>

In flyout mode, the modal and wrapper use full viewport height and the title/footer become sticky.

position="bottom" and variant="floating" are not supported (an InvalidArgumentException is thrown).

Notes

  • The top-right icon close button and footer cancel button both close the modal.
  • Default wrapper classes include overflow-hidden p-0! on flux:modal and a max-h-[80vh] flex column wrapper.
  • Content area is scrollable (overflow-y-auto) by default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment