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:
Quinn Ftw 2026-02-05 18:36:01 -08:00
parent 6a610a48ea
commit e3e84dad7f

View 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