Created
March 11, 2025 16:56
-
-
Save yaodong/8fb183d2a728d8bfba5c2a92477c2c80 to your computer and use it in GitHub Desktop.
example - modal html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Animated Modal</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<style> | |
/* Modal visibility control */ | |
[data-modal][data-state="closed"] { | |
visibility: hidden; | |
} | |
[data-modal][data-state="open"] { | |
visibility: visible; | |
} | |
[data-modal="backdrop"] { | |
transition-property: opacity; | |
transition-duration: 300ms; | |
} | |
[data-modal][data-state="open"] [data-modal="backdrop"] { | |
opacity: 0.5; | |
} | |
[data-modal][data-state="closed"] [data-modal="backdrop"] { | |
opacity: 0; | |
} | |
/* Container animations */ | |
[data-modal="container"] { | |
transition-property: opacity; | |
transition-duration: 300ms; | |
} | |
[data-modal][data-state="open"] [data-modal="container"] { | |
opacity: 1; | |
pointer-events: auto; | |
} | |
[data-modal][data-state="closed"] [data-modal="container"] { | |
opacity: 0; | |
pointer-events: none; | |
} | |
/* Content animations - combined zoom and fade effects */ | |
[data-modal="content"] { | |
transition-property: all; | |
transition-duration: 300ms; | |
transform: scale(0.95) translateY(-1rem); | |
} | |
[data-modal][data-state="open"] [data-modal="content"] { | |
transform: scale(1) translateY(0); | |
} | |
[data-modal][data-state="closed"] [data-modal="content"] { | |
transform: scale(0.95) translateY(-1rem); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="flex items-center justify-center min-h-screen bg-gray-100 p-4"> | |
<!-- Trigger Button --> | |
<button | |
id="openModalBtn" | |
class="px-4 py-2 bg-purple-600 text-white font-medium rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition-all duration-300" | |
aria-haspopup="dialog" | |
> | |
Open Beautiful Modal | |
</button> | |
<!-- Modal with improved accessibility and animation --> | |
<div | |
id="modal" | |
data-modal="root" | |
data-state="closed" | |
aria-hidden="true" | |
class="hidden" | |
> | |
<!-- Modal Backdrop --> | |
<div | |
class="fixed inset-0 bg-black z-40" | |
data-modal="backdrop" | |
aria-hidden="true" | |
></div> | |
<!-- Modal Container --> | |
<div | |
class="fixed inset-0 flex items-center justify-center p-4 z-50" | |
data-modal="container" | |
> | |
<div | |
class="bg-white rounded-xl shadow-xl p-6 max-w-md w-full mx-auto" | |
data-modal="content" | |
role="dialog" | |
aria-modal="true" | |
aria-labelledby="modal-title" | |
aria-describedby="modal-description" | |
tabindex="-1" | |
> | |
<!-- Modal Header --> | |
<div class="flex justify-between items-center border-b border-gray-200 pb-3 mb-4"> | |
<h3 id="modal-title" class="text-xl font-semibold text-gray-800"> | |
Beautiful Modal | |
</h3> | |
<button | |
id="closeModalBtn" | |
class="text-gray-400 hover:text-gray-600 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400 rounded-sm" | |
aria-label="Close modal" | |
> | |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> | |
</svg> | |
</button> | |
</div> | |
<!-- Modal Body --> | |
<div id="modal-description" class="mb-6"> | |
<p class="text-gray-600 mb-4"> | |
This is a beautiful modal created with Tailwind CSS and vanilla JavaScript. It has smooth fade-in and fade-out animations, | |
along with scaling and slide effects for a polished user experience. | |
</p> | |
<p class="text-gray-600"> | |
The modal now uses a single <code>data-modal</code> attribute with different values, making the code even more | |
semantic and maintainable. | |
</p> | |
</div> | |
<!-- Modal Footer --> | |
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-3"> | |
<button | |
id="cancelBtn" | |
class="px-4 py-2 mt-3 sm:mt-0 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-opacity-50" | |
> | |
Cancel | |
</button> | |
<button | |
id="confirmBtn" | |
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50" | |
> | |
Confirm | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Get DOM elements | |
const openModalBtn = document.getElementById('openModalBtn'); | |
const closeModalBtn = document.getElementById('closeModalBtn'); | |
const cancelBtn = document.getElementById('cancelBtn'); | |
const confirmBtn = document.getElementById('confirmBtn'); | |
const modal = document.getElementById('modal'); | |
const modalContent = modal.querySelector('[data-modal="content"]'); | |
// Trap focus within modal when open | |
function trapFocus(element) { | |
const focusableElements = element.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); | |
const firstFocusableElement = focusableElements[0]; | |
const lastFocusableElement = focusableElements[focusableElements.length - 1]; | |
element.addEventListener('keydown', function(e) { | |
if (e.key === 'Tab') { | |
if (e.shiftKey && document.activeElement === firstFocusableElement) { | |
e.preventDefault(); | |
lastFocusableElement.focus(); | |
} else if (!e.shiftKey && document.activeElement === lastFocusableElement) { | |
e.preventDefault(); | |
firstFocusableElement.focus(); | |
} | |
} | |
if (e.key === 'Escape') { | |
closeModal(); | |
} | |
}); | |
} | |
// Open modal function | |
function openModal() { | |
// First remove hidden class so the modal is in the DOM | |
modal.classList.remove('hidden'); | |
// Small delay to ensure DOM is updated before animations | |
requestAnimationFrame(() => { | |
// Then update state to trigger animations | |
modal.setAttribute('data-state', 'open'); | |
modal.setAttribute('aria-hidden', 'false'); | |
// Focus the first element and trap focus | |
setTimeout(() => { | |
modalContent.focus(); | |
trapFocus(modalContent); | |
}, 50); | |
}); | |
} | |
// Close modal function | |
function closeModal() { | |
// Update state to trigger animations | |
modal.setAttribute('data-state', 'closed'); | |
modal.setAttribute('aria-hidden', 'true'); | |
// Add a delay before hiding completely to allow for animations | |
setTimeout(() => { | |
modal.classList.add('hidden'); | |
}, 300); | |
// Return focus to trigger button | |
openModalBtn.focus(); | |
} | |
// Event listeners | |
openModalBtn.addEventListener('click', openModal); | |
closeModalBtn.addEventListener('click', closeModal); | |
cancelBtn.addEventListener('click', closeModal); | |
confirmBtn.addEventListener('click', closeModal); | |
// Event delegation for backdrop click | |
modal.addEventListener('click', function(event) { | |
if (event.target.hasAttribute('data-modal') && event.target.getAttribute('data-modal') === 'backdrop') { | |
closeModal(); | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment