Modal
Dialog overlay that requires user attention or interaction.
TailwindCSS
Alpine.js
Are you absolutely sure?
This action cannot be undone. This will permanently delete your account and remove your data from our servers.
package showcase
import "github.com/axzilla/templui/components"
templ ModalDefault() {
@components.ModalTrigger(components.ModalTriggerProps{
ModalID: "default-modal",
}) {
@components.Button() {
Open Modal
}
}
@components.Modal(components.ModalProps{
ID: "default-modal",
Class: "max-w-md",
}) {
@components.ModalHeader() {
Are you absolutely sure?
}
@components.ModalBody() {
This action cannot be undone. This will permanently delete your account and remove your data from our servers.
}
@components.ModalFooter() {
<div class="flex gap-2">
@components.ModalClose(components.ModalCloseProps{
ModalID: "default-modal",
}) {
@components.Button() {
Cancel
}
}
@components.ModalClose(components.ModalCloseProps{
ModalID: "default-modal",
}) {
@components.Button(components.ButtonProps{
Variant: components.ButtonVariantSecondary,
}) {
Continue
}
}
</div>
}
}
}
package components
import "github.com/axzilla/templui/utils"
type ModalProps struct {
ID string
Class string
Attributes templ.Attributes
DisableClickAway bool
DisableESC bool
}
type ModalTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Disabled bool
ModalID string
}
type ModalCloseProps struct {
ID string
Class string
Attributes templ.Attributes
ModalID string
}
type ModalHeaderProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ModalBodyProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ModalFooterProps struct {
ID string
Class string
Attributes templ.Attributes
}
templ Modal(props ...ModalProps) {
{{ var p ModalProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.ID == "" {
{{ p.ID = utils.RandomID() }}
}
<div
id={ p.ID }
x-data="modal"
x-on:open-modal.window="handleOpenModal"
x-on:close-modal.window="handleCloseModal"
data-modal-id={ p.ID }
x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
{ p.Attributes... }
>
<div class="fixed inset-0 bg-background/70 bg-opacity-50 transition-opacity" aria-hidden="true"></div>
<div
id={ p.ID + "-content" }
class={
utils.TwMerge(
"relative bg-background rounded-lg border text-left overflow-hidden shadow-xl transform transition-all sm:my-8 w-full",
p.Class,
),
}
if !p.DisableClickAway {
@click.outside="handleClickAway"
}
if !p.DisableESC {
x-on:keydown.esc.window="handleClickAway"
}
>
{ children... }
</div>
</div>
}
templ ModalTrigger(props ...ModalTriggerProps) {
{{ var p ModalTriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
x-data="modalTriggers"
if p.ModalID != "" {
data-modal-id={ p.ModalID }
}
if !p.Disabled {
@click="openModal"
}
class={
utils.TwMerge(
utils.IfElse(p.Disabled, "cursor-not-allowed", "cursor-pointer"),
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
</span>
}
templ ModalClose(props ...ModalCloseProps) {
{{ var p ModalCloseProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
x-data="modalTriggers"
if p.ModalID != "" {
data-modal-id={ p.ModalID }
}
@click="closeModal"
class={ utils.TwMerge("", p.Class) }
{ p.Attributes... }
>
{ children... }
</span>
}
templ ModalHeader(props ...ModalHeaderProps) {
{{ var p ModalHeaderProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-4 pt-5 pb-4 sm:p-6 sm:pb-4", p.Class) }
{ p.Attributes... }
>
<h3 class="text-lg leading-6 font-medium text-foreground" id="modal-title">
{ children... }
</h3>
</div>
}
templ ModalBody(props ...ModalBodyProps) {
{{ var p ModalBodyProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-4 pt-5 pb-4 sm:p-6 sm:pb-4", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ ModalFooter(props ...ModalFooterProps) {
{{ var p ModalFooterProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ ModalScript() {
{{ handler := templ.NewOnceHandle() }}
@handler.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('modal', () => ({
open: false,
handleOpenModal(event) {
if (event.detail.id === this.$el.dataset.modalId) {
this.open = true
}
},
handleCloseModal(event) {
if (event.detail.id === this.$el.dataset.modalId) {
this.open = false
}
},
handleClickAway() {
this.open = false;
},
}))
Alpine.data('modalTriggers', () => ({
openModal() {
this.$dispatch('open-modal', {
id: this.$el.dataset.modalId
})
},
closeModal() {
this.$dispatch('close-modal', {
id: this.$el.dataset.modalId
})
}
}))
})
</script>
}
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just Modal
@components.ModalScript()
2. Use the component:
@components.Modal(components.ModalProps{...})