Component
Device Optimized Modal
Device Optimized Modal.
- Component
- Vanilla
- HTML
- MIT
Preview
Live component
No interactive preview for this component.
Install it with the CLI below, or read the implementation on this page.
Install
Add to your project
npx devsnips add Vanilla/Components/Modals/device-optimized-modal Vanilla/Components/Modals/device-optimized-modal <!--
Device-optimized Modal Popup with Overlay
- Accessible dialog implementation
- Keyboard trap when open
- Click outside to close
- Focus management
- Smooth animations
-->
<button class="modal-trigger" aria-haspopup="dialog">Open Modal</button>
<div class="modal" role="dialog" aria-labelledby="modal-title" aria-modal="true" hidden>
<div class="modal-overlay" data-close-modal></div>
<div class="modal-content">
<header class="modal-header">
<h2 id="modal-title">Modal Title</h2>
<button class="modal-close" aria-label="Close modal" data-close-modal>
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</header>
<div class="modal-body">
<p>This is the modal content. You can add any HTML content here.</p>
<p>The modal is fully accessible and can be closed by:</p>
<ul>
<li>Clicking the close button</li>
<li>Clicking the overlay</li>
<li>Pressing the Escape key</li>
</ul>
</div>
<footer class="modal-footer">
<button class="modal-button" data-close-modal>Close</button>
<button class="modal-button primary">Confirm</button>
</footer>
</div>
</div>
<style>
.modal {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
z-index: 1000;
}
.modal[open] {
opacity: 1;
visibility: visible;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s;
}
.modal-content {
position: relative;
width: 100%;
max-width: 500px;
background: white;
border-radius: var(--ds-radius-md, 8px);
box-shadow: var(--ds-shadow-lg, 0 4px 12px rgba(0, 0, 0, 0.15));
animation: slideIn 0.3s;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--ds-surface-3, #eee);
}
.modal-title {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
padding: 0.5rem;
border: none;
background: none;
color: var(--ds-muted, #666);
cursor: pointer;
transition: color 0.3s;
}
.modal-close:hover {
color: var(--ds-foreground, #333);
}
.modal-body {
padding: 1rem;
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid var(--ds-surface-3, #eee);
}
.modal-button {
padding: 0.5rem 1rem;
border: 1px solid var(--ds-border, #ddd);
background: white;
border-radius: var(--ds-radius-sm, 4px);
cursor: pointer;
transition: all 0.3s;
}
.modal-button.primary {
background: var(--ds-accent, #4a90e2);
border-color: var(--ds-accent, #4a90e2);
color: white;
}
.modal-button:hover {
background: var(--ds-surface-2, #f5f5f5);
}
.modal-button.primary:hover {
background: var(--ds-accent, #357abd);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Focus styles for accessibility */
.modal-close:focus-visible,
.modal-button:focus-visible {
outline: 2px solid var(--ds-accent, #4a90e2);
outline-offset: 2px;
}
@media (max-width: 600px) {
.modal-content {
margin: 1rem;
}
.modal-footer {
flex-direction: column-reverse;
}
.modal-button {
width: 100%;
}
}
/* devsnips-qa:quality-bar */
@media (prefers-reduced-motion: reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}
/* DevSnips design tokens (Swiss) — edit tokens.css to re-theme */
:root{
--ds-bg:#fff;--ds-surface:#fff;--ds-surface-2:#f7f7f7;--ds-surface-3:#ededed;--ds-foreground:#0a0a0a;--ds-muted:#525252;--ds-subtle:#a3a3a3;--ds-border:#e5e5e5;--ds-border-strong:#d4d4d4;--ds-accent:#2563eb;--ds-accent-hover:#1d4ed8;--ds-accent-fg:#fff;--ds-accent-soft:#eff6ff;--ds-success:#16a34a;--ds-success-soft:#f0fdf4;--ds-warning:#d97706;--ds-warning-soft:#fffbeb;--ds-danger:#dc2626;--ds-danger-soft:#fef2f2;--ds-info:#2563eb;--ds-info-soft:#eff6ff;--ds-font-sans:system-ui,-apple-system,'Segoe UI',Roboto,Inter,'Helvetica Neue',Arial,sans-serif;--ds-font-mono:ui-monospace,'SF Mono','JetBrains Mono',Menlo,Consolas,monospace;--ds-text-xs:.75rem;--ds-text-sm:.875rem;--ds-text-base:1rem;--ds-text-lg:1.125rem;--ds-text-xl:1.25rem;--ds-text-2xl:1.5rem;--ds-text-3xl:1.875rem;--ds-text-4xl:2.25rem;--ds-leading-tight:1.25;--ds-leading-normal:1.5;--ds-leading-relaxed:1.625;--ds-weight-normal:400;--ds-weight-medium:500;--ds-weight-semibold:600;--ds-weight-bold:700;--ds-space-0:0;--ds-space-px:1px;--ds-space-1:.25rem;--ds-space-2:.5rem;--ds-space-3:.75rem;--ds-space-4:1rem;--ds-space-5:1.25rem;--ds-space-6:1.5rem;--ds-space-8:2rem;--ds-space-10:2.5rem;--ds-space-12:3rem;--ds-space-16:4rem;--ds-radius-sm:.25rem;--ds-radius-md:.5rem;--ds-radius-lg:.75rem;--ds-radius-xl:1rem;--ds-radius-2xl:1.5rem;--ds-radius-full:9999px;--ds-shadow-sm:0 1px 2px 0 rgba(0,0,0,.05);--ds-shadow-md:0 4px 6px -1px rgba(0,0,0,.08),0 2px 4px -2px rgba(0,0,0,.05);--ds-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.05);--ds-duration-fast:120ms;--ds-duration-normal:200ms;--ds-duration-slow:300ms;--ds-ease-out:cubic-bezier(.16,1,.3,1);--ds-ease-in-out:cubic-bezier(.4,0,.2,1);--ds-ring:0 0 0 2px #2563eb;--ds-container:1200px;--ds-gutter:1rem;
}
@media (prefers-color-scheme: dark){:root{--ds-bg:#0a0a0a;--ds-surface:#111;--ds-surface-2:#171717;--ds-surface-3:#1f1f1f;--ds-foreground:#fafafa;--ds-muted:#a3a3a3;--ds-subtle:#737373;--ds-border:#262626;--ds-border-strong:#404040;--ds-accent:#3b82f6;--ds-accent-hover:#60a5fa;--ds-accent-fg:#fff;--ds-accent-soft:#172554;--ds-success:#22c55e;--ds-success-soft:#052e16;--ds-warning:#f59e0b;--ds-warning-soft:#422006;--ds-danger:#ef4444;--ds-danger-soft:#450a0a;--ds-info:#3b82f6;--ds-info-soft:#172554;--ds-ring:0 0 0 2px #3b82f6;}}
</style>
<script>
class Modal {
constructor(element) {
this.modal = element;
this.trigger = document.querySelector('.modal-trigger');
this.focusableElements = this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
this.bindEvents();
}
bindEvents() {
this.trigger.addEventListener('click', () => this.open());
this.modal.querySelectorAll('[data-close-modal]')
.forEach(el => el.addEventListener('click', () => this.close()));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !this.modal.hidden) this.close();
if (e.key === 'Tab' && !this.modal.hidden) this.handleTab(e);
});
}
open() {
this.modal.hidden = false;
this.modal.setAttribute('open', '');
document.body.style.overflow = 'hidden';
this.firstFocusable.focus();
// Store last focused element to restore later
this.lastFocused = document.activeElement;
}
close() {
this.modal.hidden = true;
this.modal.removeAttribute('open');
document.body.style.overflow = '';
// Restore focus
if (this.lastFocused) {
this.lastFocused.focus();
}
}
handleTab(e) {
if (e.shiftKey) {
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
}
} else {
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
}
// Initialize modal
const modal = new Modal(document.querySelector('.modal'));
</script> # Snippet 07 Device Optimized Modal
Snippet 07 Device Optimized Modal with responsive design with animations and hover interactions.
**Technology:** vanilla
**Category:** components
**Subcategory:** modals
**Tags:** modal, animation, hover-effect, shadow, responsive, flexbox, center, button
**Responsive:** Yes 253 lines UTF-8 · LF · Spaces: 2
Continue browsing