Modal Neo Brutalism Design Using HTML, CSS, and JavaScript

Modal Neo Brutalism Design Using HTML, CSS, and JavaScript

Modal Neo Brutalism Design Using HTML, CSS, and JavaScript

Create a modern, responsive modal design in the neo-brutalist style using HTML, CSS, and JavaScript, featuring bold colors, thick borders, and heavy shadows for a raw, unpolished aesthetic. This implementation focuses on striking visuals, accessible interactions, and maintainable code, ideal for dialogs in web interfaces.

Prerequisites

  • Basic HTML, CSS, and JavaScript knowledge
  • A code editor (e.g., VS Code)

Part 1: Modal Neo Brutalism Design

Step 1: HTML Structure (index.html)

Create a semantic HTML structure for the neo-brutalist modals with trigger buttons, overlay, and modal content.

<div class="modal-trigger-container" role="group" aria-label="Modal trigger buttons">
    <button class="modal-trigger" data-modal="modal-primary" aria-controls="modal-primary" aria-label="Open primary modal">Open Primary Modal</button>
    <button class="modal-trigger secondary" data-modal="modal-secondary" aria-controls="modal-secondary" aria-label="Open secondary modal">Open Secondary Modal</button>
</div>

<div class="modal-overlay" id="modal-primary" role="dialog" aria-labelledby="modal-primary-title" aria-hidden="true">
    <div class="modal">
        <h2 class="modal-header" id="modal-primary-title">Primary Modal</h2>
        <p class="modal-content">This is a neo-brutalist modal with a bold, raw design. Use it for important messages or actions.</p>
        <button class="modal-button close" aria-label="Close primary modal">Close</button>
    </div>
</div>

<div class="modal-overlay" id="modal-secondary" role="dialog" aria-labelledby="modal-secondary-title" aria-hidden="true">
    <div class="modal">
        <h2 class="modal-header" id="modal-secondary-title">Secondary Modal</h2>
        <p class="modal-content">This modal uses an accent color for variety. It retains the neo-brutalist aesthetic.</p>
        <button class="modal-button close" aria-label="Close secondary modal">Close</button>
    </div>
</div>

Step 2: Core CSS Implementation (styles.css)

Style the modal, triggers, and buttons with neo-brutalist aesthetics: bold colors, thick borders, heavy shadows, and a monospace font.

:root {
    --gray-900: #111827;
    --gray-700: #374151;
    --gray-200: #e5e7eb;
    --gray-100: #f3f4f6;
    --white: #ffffff;
    --primary-600: #2563eb;
    --primary-500: #3b82f6;
    --accent-500: #f59e0b;
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 12px;
    --spacing-lg: 16px;
    --radius-sm: 4px;
    --radius-md: 6px;
    --transition: 0.2s ease;
    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    --shadow-neo: 6px 6px 0 var(--gray-900);
    --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --font-mono: 'IBM Plex Mono', monospace;
}

.modal-trigger-container {
    display: flex;
    gap: var(--spacing-md);
    background: var(--white);
    padding: var(--spacing-lg);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    justify-content: center;
}

.modal-trigger {
    padding: var(--spacing-md) var(--spacing-lg);
    font-family: var(--font-mono);
    font-size: 1rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--gray-900);
    background: var(--primary-500);
    border: 3px solid var(--gray-900);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-neo);
    transition: transform var(--transition), box-shadow var(--transition);
    cursor: pointer;
}

.modal-trigger:hover {
    transform: translate(3px, 3px);
    box-shadow: 3px 3px 0 var(--gray-900);
}

.modal-trigger:focus {
    outline: none;
    box-shadow: 3px 3px 0 var(--accent-500), 0 0 0 3px var(--primary-600);
}

.modal-trigger.secondary {
    background: var(--accent-500);
}

.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--transition), visibility var(--transition);
}

.modal-overlay.active {
    opacity: 1;
    visibility: visible;
}

.modal {
    background: var(--white);
    border: 4px solid var(--gray-900);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-neo);
    max-width: 400px;
    width: 100%;
    padding: var(--spacing-lg);
    transform: scale(0);
    transition: transform var(--transition);
    font-family: var(--font-mono);
}

.modal.active {
    transform: scale(1);
}

.modal-header {
    font-size: 1.25rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--gray-900);
    margin-bottom: var(--spacing-md);
}

.modal-content {
    font-size: 1rem;
    color: var(--gray-700);
    margin-bottom: var(--spacing-lg);
}

.modal-button {
    padding: var(--spacing-sm) var(--spacing-md);
    font-family: var(--font-mono);
    font-size: 0.875rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--gray-900);
    background: var(--primary-500);
    border: 3px solid var(--gray-900);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-neo);
    transition: transform var(--transition), box-shadow var(--transition);
    cursor: pointer;
}

.modal-button:hover {
    transform: translate(2px, 2px);
    box-shadow: 2px 2px 0 var(--gray-900);
}

.modal-button:focus {
    outline: none;
    box-shadow: 2px 2px 0 var(--accent-500), 0 0 0 3px var(--primary-600);
}

.modal-button.close {
    background: var(--accent-500);
}

@media (max-width: 640px) {
    .modal-trigger-container {
        flex-direction: column;
        padding: var(--spacing-md);
        gap: var(--spacing-sm);
    }

    .modal-trigger {
        padding: var(--spacing-sm) var(--spacing-md);
        font-size: 0.875rem;
    }

    .modal {
        max-width: 90%;
        padding: var(--spacing-md);
    }

    .modal-header {
        font-size: 1rem;
    }

    .modal-content {
        font-size: 0.875rem;
    }

    .modal-button {
        font-size: 0.75rem;
    }
}

Step 3: JavaScript Implementation (script.js)

Add JavaScript to manage modal visibility, focus trapping, and keyboard navigation for accessibility.

document.addEventListener('DOMContentLoaded', () => {
    const triggers = document.querySelectorAll('.modal-trigger');
    const modals = document.querySelectorAll('.modal-overlay');
    const closeButtons = document.querySelectorAll('.modal-button.close');

    // Open modal
    triggers.forEach(trigger => {
        trigger.addEventListener('click', () => {
            const modalId = trigger.getAttribute('data-modal');
            const modal = document.getElementById(modalId);
            modal.classList.add('active');
            modal.setAttribute('aria-hidden', 'false');
            modal.querySelector('.modal').classList.add('active');
            trapFocus(modal);
        });
    });

    // Close modal
    closeButtons.forEach(button => {
        button.addEventListener('click', () => {
            const modal = button.closest('.modal-overlay');
            closeModal(modal);
        });
    });

    // Close modal on overlay click
    modals.forEach(modal => {
        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                closeModal(modal);
            }
        });
    });

    // Close modal on Escape key
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            const activeModal = document.querySelector('.modal-overlay.active');
            if (activeModal) {
                closeModal(activeModal);
            }
        }
    });

    // Trap focus within modal
    function trapFocus(modal) {
        const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
        const firstFocusable = focusableElements[0];
        const lastFocusable = focusableElements[focusableElements.length - 1];

        firstFocusable.focus();

        modal.addEventListener('keydown', (e) => {
            if (e.key === 'Tab') {
                if (e.shiftKey && document.activeElement === firstFocusable) {
                    e.preventDefault();
                    lastFocusable.focus();
                } else if (!e.shiftKey && document.activeElement === lastFocusable) {
                    e.preventDefault();
                    firstFocusable.focus();
                }
            }
        });
    }

    // Close modal function
    function closeModal(modal) {
        modal.classList.remove('active');
        modal.setAttribute('aria-hidden', 'true');
        modal.querySelector('.modal').classList.remove('active');
        document.querySelector('.modal-trigger').focus();
    }
});

Explanation

  • Layout: Trigger buttons are housed in a flexbox container with a white background, shadow, and rounded corners. Modals appear within an overlay, centered on the screen, with a white background, thick borders, and shadows.
  • Styling: Triggers and modal buttons use blue (--primary-500) or orange (--accent-500), thick black borders (3px), offset shadows (--shadow-neo), and a monospace font. Modals have a bold border (4px) and shadow, with uppercase headers and readable content.
  • Interactivity: JavaScript toggles modal visibility by adding/removing the `active` class. Clicking triggers, close buttons, or the overlay, or pressing Escape, controls the modal. Focus trapping ensures accessibility.
  • Visual Feedback: Hovering shifts buttons (translate(3px, 3px)) with reduced shadows; focus adds a dual shadow (accent and primary). Modals scale in (scale(1)) and fade with the overlay.
  • Responsiveness: A media query (@media (max-width: 640px)) switches the trigger container to a column layout, reduces modal size, and adjusts font sizes for mobile usability.
  • Accessibility: role="group" and aria-label on the trigger container, plus role="dialog", aria-labelledby, and aria-hidden on modals, ensure screen reader compatibility. Focus trapping, keyboard support (Tab, Escape, Enter), and high contrast ratios (4.5:1 minimum) enhance usability.
UX Tip: Neo-brutalist modals should feel bold and tactile with heavy shadows and stark contrasts. Ensure accessibility with ARIA attributes, focus trapping, and keyboard support. In production, add form elements or dynamic content and test for edge cases (e.g., screen readers, mobile).

Accessibility Features

  • Use role="group", role="dialog", aria-labelledby, and aria-hidden for semantic structure
  • Ensure high contrast ratios for text, borders, and backgrounds (4.5:1 minimum)
  • Support keyboard navigation with focus trapping, Tab, and Escape keys
  • Use bold, readable typography (monospace for neo-brutalism)

Golden Rules

  • Use CSS variables for consistent theming
  • Implement bold borders and heavy shadows for neo-brutalist style
  • Ensure accessibility with ARIA and keyboard support
  • Optimize for mobile with responsive design

Conclusion

A professional neo-brutalist modal should embrace raw, bold aesthetics, provide clear user interactions, remain responsive, and follow accessibility guidelines. This solution uses HTML, CSS, and JavaScript for striking modals with primary and secondary trigger variants, featuring heavy shadows and a monospace font. Experiment with colors, shadow offsets, or add complex content like forms. Test across devices and browsers for usability. Feel free to leave comments with any questions or suggestions!

Comments