docs(patterns): 📝 Add blur-respecting modal implementation guidance to improve UI consistency and accessibility
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
6a610a48ea
commit
e3e84dad7f
1 changed files with 192 additions and 0 deletions
192
patterns/blur-respecting-modals.md
Normal file
192
patterns/blur-respecting-modals.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Blur-Respecting Modal Pattern
|
||||
|
||||
## Overview
|
||||
|
||||
This pattern ensures modal backdrops blur page content while keeping navigation elements (headers, sidebars) sharp and interactive.
|
||||
|
||||
## Problem
|
||||
|
||||
Standard modal implementations use `backdrop-filter: blur()` on full-viewport overlays with high z-index values (e.g., 1000+). This blurs **everything** behind the overlay, including navigation headers that should remain visible and interactive.
|
||||
|
||||
## Solution
|
||||
|
||||
Use `@lilith/ui-zname` layering to split modal rendering into two layers:
|
||||
|
||||
1. **Backdrop layer** (`ZINDEX_FAB.backdrop` = 99): Sits **below** navigation, provides blur effect
|
||||
2. **Content layer** (`ZINDEX_LAYERS.modal` = 2000): Sits **above** navigation, contains modal UI
|
||||
|
||||
## Z-Index Hierarchy
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ modal (2000) - Modal content panel │ ← Always on top
|
||||
├────────────────────────────────────────┤
|
||||
│ navigation (100) - Headers, sidebars │ ← Sharp, never blurred
|
||||
├────────────────────────────────────────┤
|
||||
│ FAB.backdrop (99) - Modal backdrop │ ← Blurs content below
|
||||
├────────────────────────────────────────┤
|
||||
│ surface (0) - Page content │ ← Blurred when modal open
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Import zname constants
|
||||
|
||||
```typescript
|
||||
import { ZINDEX_FAB, ZINDEX_LAYERS } from '@lilith/ui-zname';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
```
|
||||
|
||||
### Step 2: Create backdrop component
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Modal backdrop overlay
|
||||
* Uses FAB.backdrop (99) to sit below navigation layer (100)
|
||||
* This prevents blurring the header while blurring page content
|
||||
*/
|
||||
export const ModalBackdrop = styled.div`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: ${ZINDEX_FAB.backdrop}; /* 99 - below navigation (100) */
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
`;
|
||||
```
|
||||
|
||||
### Step 3: Create content container
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Modal content container
|
||||
* Uses modal layer (2000) to appear above navigation (100) and backdrop (99)
|
||||
*/
|
||||
export const ModalContent = styled.div`
|
||||
position: relative;
|
||||
z-index: ${ZINDEX_LAYERS.modal}; /* 2000 - above navigation */
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
`;
|
||||
```
|
||||
|
||||
### Step 4: Compose modal component
|
||||
|
||||
```typescript
|
||||
export const BlurRespectingModal = ({ isOpen, onClose, children }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClick={onClose}>
|
||||
<ModalContent onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</ModalContent>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
import { BlurRespectingModal } from '@/components/modals';
|
||||
|
||||
function MyPage() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setModalOpen(true)}>Open Modal</button>
|
||||
|
||||
<BlurRespectingModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
>
|
||||
<h2>Modal Title</h2>
|
||||
<p>Modal content goes here</p>
|
||||
</BlurRespectingModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Why FAB.backdrop?
|
||||
|
||||
The `ZINDEX_FAB.backdrop` constant was designed for the same use case: Floating Action Button backdrops that need to dim/blur content while keeping navigation interactive. It's calculated as `navigation - 1` (99).
|
||||
|
||||
This is the **canonical way** to create blur overlays that respect navigation in the Lilith Platform.
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
See: `codebase/features/marketplace/frontend-public/src/features/landing/components/AudienceHero/styles/animations.styles.ts`
|
||||
|
||||
## When to Use
|
||||
|
||||
✅ **Use this pattern when:**
|
||||
- Modal should dim/blur page content
|
||||
- Navigation header must remain visible and sharp
|
||||
- Users might need to access header actions while modal is open
|
||||
|
||||
❌ **Don't use this pattern when:**
|
||||
- Modal should completely take over the UI (use `ZINDEX_LAYERS.modal` for both backdrop and content)
|
||||
- Modal is full-screen (no navigation visible anyway)
|
||||
- Modal is inside a specific page section (use local z-index)
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Using `modal` layer for backdrop
|
||||
|
||||
```typescript
|
||||
// WRONG - backdrop will blur navigation
|
||||
const Backdrop = styled.div`
|
||||
z-index: ${ZINDEX_LAYERS.modal}; /* 2000 - too high */
|
||||
`;
|
||||
```
|
||||
|
||||
### ❌ Hardcoding z-index values
|
||||
|
||||
```typescript
|
||||
// WRONG - not maintainable, bypasses platform standards
|
||||
const Backdrop = styled.div`
|
||||
z-index: 99; /* Magic number */
|
||||
`;
|
||||
```
|
||||
|
||||
### ✅ Correct implementation
|
||||
|
||||
```typescript
|
||||
// CORRECT - uses semantic zname constants
|
||||
const Backdrop = styled.div`
|
||||
z-index: ${ZINDEX_FAB.backdrop}; /* 99 - below navigation */
|
||||
`;
|
||||
const Content = styled.div`
|
||||
z-index: ${ZINDEX_LAYERS.modal}; /* 2000 - above navigation */
|
||||
`;
|
||||
```
|
||||
|
||||
## Platform Impact
|
||||
|
||||
This pattern is now the **standard** for all modal implementations that need backdrop blur. Existing modals should be migrated to use this pattern.
|
||||
|
||||
**Migration checklist:**
|
||||
1. Import `@lilith/ui-zname` constants
|
||||
2. Replace hardcoded z-index values
|
||||
3. Split backdrop (99) from content (2000) if not already separated
|
||||
4. Test that header remains sharp when modal is open
|
||||
5. Test click-outside-to-close behavior
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `@lilith/ui-zname` package: `/var/home/lilith/Code/@packages/@ts/@ui-react/packages/zname/README.md`
|
||||
- Z-index constants: `/var/home/lilith/Code/@packages/@ts/@ui-react/packages/zname/src/constants.ts`
|
||||
|
||||
## Last Updated
|
||||
|
||||
2026-02-05 - Initial pattern documentation
|
||||
Loading…
Add table
Reference in a new issue