- Add hybrid modal/page navigation based on screen size (1024px breakpoint) - Desktop (>1024px): Uses modal overlays for quick interactions - Mobile (≤1024px): Navigates to dedicated full pages for better UX - Extract shared NPC chat content into reusable partial template - Add responsive navigation JavaScript (responsive-modals.js) - Create dedicated NPC chat page route with back button navigation - Add mobile-optimized CSS with sticky header and chat input - Fix HTMX indicator errors by using htmx-indicator class pattern - Document responsive modal pattern for future features Addresses mobile UX issues: cramped space, nested scrolling, keyboard conflicts, and lack of native back button support in modals. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.4 KiB
Responsive Modal Pattern
Overview
This pattern provides an optimal UX for modal-like content by adapting to screen size:
- Desktop (>1024px): Displays content in modal overlays
- Mobile (≤1024px): Navigates to dedicated full pages
This addresses common mobile modal UX issues: cramped space, nested scrolling, keyboard conflicts, and lack of native back button support.
When to Use This Pattern
Use this pattern when:
- Content requires significant interaction (forms, chat, complex data)
- Mobile users need better scroll/keyboard handling
- The interaction benefits from full-screen on mobile
- Desktop users benefit from keeping context visible
Don't use for:
- Simple confirmations/alerts (use standard modals)
- Very brief interactions (dropdowns, tooltips)
- Content that must overlay the game state
Implementation Steps
1. Create Shared Content Partial
Extract the actual content into a reusable partial template that both the modal and page can use.
File: templates/game/partials/[feature]_content.html
{# Shared content partial #}
<div class="feature-container">
<!-- Your content here -->
<!-- This will be used by both modal and page -->
</div>
2. Create Two Routes
Create both a modal route and a page route in your view:
File: app/views/game_views.py
@game_bp.route('/session/<session_id>/feature/<feature_id>')
@require_auth
def feature_page(session_id: str, feature_id: str):
"""
Dedicated page view (mobile-friendly).
Used on mobile devices for better UX.
"""
# Fetch data
data = get_feature_data(session_id, feature_id)
return render_template(
'game/feature_page.html',
session_id=session_id,
feature=data
)
@game_bp.route('/session/<session_id>/feature/<feature_id>/modal')
@require_auth
def feature_modal(session_id: str, feature_id: str):
"""
Modal view (desktop overlay).
Used on desktop for quick interactions.
"""
# Fetch same data
data = get_feature_data(session_id, feature_id)
return render_template(
'game/partials/feature_modal.html',
session_id=session_id,
feature=data
)
3. Create Page Template
Create a dedicated page template with header, back button, and shared content.
File: templates/game/feature_page.html
{% extends "base.html" %}
{% block title %}{{ feature.name }} - Code of Conquest{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/play.css') }}">
{% endblock %}
{% block content %}
<div class="feature-page">
{# Page Header with Back Button #}
<div class="feature-header">
<a href="{{ url_for('game.play_session', session_id=session_id) }}"
class="feature-back-btn">
<svg><!-- Back arrow icon --></svg>
Back
</a>
<h1 class="feature-title">{{ feature.name }}</h1>
</div>
{# Include shared content #}
{% include 'game/partials/feature_content.html' %}
</div>
{% endblock %}
{% block scripts %}
<script>
// Add any feature-specific JavaScript here
</script>
{% endblock %}
4. Update Modal Template
Update your modal template to use the shared content partial.
File: templates/game/partials/feature_modal.html
{# Modal wrapper #}
<div class="modal-overlay" onclick="if(event.target === this) closeModal()">
<div class="modal-content modal-content--xl">
{# Modal Header #}
<div class="modal-header">
<h3 class="modal-title">{{ feature.name }}</h3>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
{# Modal Body - Uses shared content #}
<div class="modal-body">
{% include 'game/partials/feature_content.html' %}
</div>
{# Modal Footer (optional) #}
<div class="modal-footer">
<button class="btn btn--secondary" onclick="closeModal()">
Close
</button>
</div>
</div>
</div>
5. Add Responsive Navigation
Update the trigger element to use responsive navigation JavaScript.
File: templates/game/partials/sidebar_features.html
<div class="feature-item"
data-feature-id="{{ feature.id }}"
onclick="navigateResponsive(
event,
'{{ url_for('game.feature_page', session_id=session_id, feature_id=feature.id) }}',
'{{ url_for('game.feature_modal', session_id=session_id, feature_id=feature.id) }}'
)"
style="cursor: pointer;">
<div class="feature-name">{{ feature.name }}</div>
</div>
6. Add Mobile-Friendly CSS
Add CSS for the dedicated page with mobile optimizations.
File: static/css/play.css
/* Feature Page */
.feature-page {
min-height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-secondary);
}
/* Page Header with Back Button */
.feature-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--bg-primary);
border-bottom: 2px solid var(--play-border);
position: sticky;
top: 0;
z-index: 100;
}
.feature-back-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg-input);
color: var(--text-primary);
text-decoration: none;
border-radius: 4px;
border: 1px solid var(--play-border);
transition: all 0.2s ease;
}
.feature-back-btn:hover {
background: var(--bg-tertiary);
border-color: var(--accent-gold);
color: var(--accent-gold);
}
.feature-title {
flex: 1;
margin: 0;
font-family: var(--font-heading);
font-size: 1.5rem;
color: var(--accent-gold);
}
/* Responsive layout */
@media (min-width: 1025px) {
.feature-page .feature-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
}
@media (max-width: 1024px) {
.feature-page .feature-container {
padding: 1rem;
flex: 1;
}
}
7. Include JavaScript
Ensure the responsive modals JavaScript is included in your play page template.
File: templates/game/play.html
{% block scripts %}
<!-- Existing scripts -->
<script>
// Your existing JavaScript
</script>
<!-- Responsive Modal Navigation -->
<script src="{{ url_for('static', filename='js/responsive-modals.js') }}"></script>
{% endblock %}
JavaScript API
The responsive navigation is handled by responsive-modals.js:
Functions
navigateResponsive(event, pageUrl, modalUrl)
Navigates responsively based on screen size.
Parameters:
event(Event): Click event (will be prevented)pageUrl(string): Full page URL for mobile navigationmodalUrl(string): Modal content URL for desktop HTMX loading
Example:
navigateResponsive(
event,
'/game/session/123/npc/456', // Page route
'/game/session/123/npc/456/chat' // Modal route
)
isMobile()
Returns true if viewport is ≤1024px.
Example:
if (isMobile()) {
// Mobile-specific behavior
}
Breakpoint
The mobile/desktop breakpoint is 1024px to match the CSS media query:
const MOBILE_BREAKPOINT = 1024;
This ensures consistent behavior between JavaScript navigation and CSS responsive layouts.
Example: NPC Chat
See the NPC chat implementation for a complete working example:
Routes:
- Page:
/game/session/<session_id>/npc/<npc_id>(public_web/app/views/game_views.py:695) - Modal:
/game/session/<session_id>/npc/<npc_id>/chat(public_web/app/views/game_views.py:746)
Templates:
- Shared content:
templates/game/partials/npc_chat_content.html - Page:
templates/game/npc_chat_page.html - Modal:
templates/game/partials/npc_chat_modal.html
Trigger:
templates/game/partials/sidebar_npcs.html(line 11)
CSS:
- Page styles:
static/css/play.css(lines 1809-1956)
Testing Checklist
When implementing this pattern, test:
- Desktop (>1024px): Modal opens correctly
- Desktop: Modal closes with X button, overlay click, and Escape key
- Desktop: Game screen remains visible behind modal
- Mobile (≤1024px): Navigates to dedicated page
- Mobile: Back button returns to game
- Mobile: Content scrolls naturally
- Mobile: Keyboard doesn't break layout
- Both: Shared content renders identically
- Both: HTMX interactions work correctly
- Resize: Behavior adapts when crossing breakpoint
Best Practices
- Always share content: Use a partial template to avoid duplication
- Keep routes parallel: Use consistent URL patterns (
/featureand/feature/modal) - Test both views: Ensure feature parity between modal and page
- Mobile-first CSS: Design for mobile, enhance for desktop
- Consistent breakpoint: Always use 1024px to match the JavaScript
- Document examples: Link to working implementations in this doc
Future Improvements
Potential enhancements to this pattern:
- Add transition animations for modal open/close
- Support deep linking to modals on desktop
- Add browser back button handling for desktop modals
- Support customizable breakpoints per feature
- Add analytics tracking for modal vs page usage
Related Documentation
- HTMX_PATTERNS.md - HTMX integration patterns
- TEMPLATES.md - Template structure and conventions
- TESTING.md - Manual testing procedures