---
name: ipad-pro-design
description: CSS and UX patterns for iPad Pro 12.9" development. Covers viewport configuration, touch optimization, safe areas, ProMotion animations, and child-friendly design patterns. Use when building educational apps targeting iPad Pro.
allowed-tools: Read, Write, Edit, Glob, Grep
---
# iPad Pro 12.9" Design Skill
Comprehensive CSS and UX patterns for building educational web applications optimized for iPad Pro 12.9" (M1/M2/M4). This skill integrates with the project's Utopia fluid scales and web components architecture.
## Related Skills
- **`utopia-fluid-scales`**: Fluid typography and spacing tokens (cqi-based)
- **`utopia-container-queries`**: Container setup for fluid scales
- **`web-components`**: Component architecture patterns
- **`ux-accessibility`**: Focus management and WCAG compliance
- **`ux-animation-motion`**: Animation timing and reduced motion
- **`animejs-v4`**: Hardware-accelerated animations
---
## iPad Pro 12.9" Technical Specifications
### Display Specifications
| Property | Value |
|----------|-------|
| Screen Resolution | 2732 x 2048 pixels |
| Points Resolution | 1366 x 1024 points |
| Device Pixel Ratio | 2x (@2x Retina) |
| PPI | 264 pixels per inch |
| Display Type | Liquid Retina XDR |
| Refresh Rate | ProMotion 24-120Hz adaptive |
| Color | P3 wide color gamut |
| Peak Brightness | 1600 nits HDR, 1000 nits full-screen |
### Viewport Calculations
```
CSS Pixels (Landscape): 1366 x 1024
CSS Pixels (Portrait): 1024 x 1366
Device Pixels (Landscape): 2732 x 2048
Device Pixels (Portrait): 2048 x 2732
```
### Safe Areas
| Edge | Safe Inset (Points) |
|------|---------------------|
| Top (Portrait, no notch) | 24pt status bar |
| Top (Landscape, no notch) | 24pt status bar |
| Bottom (all orientations) | 20pt home indicator |
| Left/Right (Landscape) | 0pt (no notch on 12.9") |
---
## Viewport Configuration
### Required Meta Tag
```html
```
| Attribute | Purpose |
|-----------|---------|
| `width=device-width` | Match CSS viewport to device width |
| `initial-scale=1` | Prevent default zoom, 1:1 scale |
| `viewport-fit=cover` | Extend content to screen edges (enables safe area insets) |
### AVOID These Viewport Settings
```html
```
### PWA Optimizations
```html
```
---
## Safe Area Handling
### CSS Environment Variables
Safari on iPadOS provides safe area values via `env()`:
```css
:root {
/* Safe area fallbacks for non-Safari browsers */
--safe-area-inset-top: env(safe-area-inset-top, 0px);
--safe-area-inset-right: env(safe-area-inset-right, 0px);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-inset-left: env(safe-area-inset-left, 0px);
}
```
### Applying Safe Areas
```css
/* Full-screen layout with safe areas */
.app-container {
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
}
/* Fixed bottom navigation */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-bottom: calc(var(--space-s) + var(--safe-area-inset-bottom));
}
/* Fixed header */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding-top: calc(var(--space-s) + var(--safe-area-inset-top));
}
```
### Home Indicator Avoidance
The home indicator appears at the bottom of screen. Avoid placing interactive elements in this zone:
```css
.interactive-bottom-zone {
/* Ensure 20pt clearance from bottom */
margin-bottom: max(20px, var(--safe-area-inset-bottom));
}
```
---
## Touch Target Optimization
### Apple Human Interface Guidelines
| Guideline | Value | Project Token |
|-----------|-------|---------------|
| Minimum touch target | 44pt x 44pt | `--min-touch-target` |
| Recommended touch target | 48pt x 48pt | Use `--space-xl` |
| Child-friendly target | 56pt+ | Use `--space-2xl` |
| Spacing between targets | 8pt minimum | Use `--space-2xs` |
### Touch Target Patterns
```css
/* Standard touch target (adults) */
.touch-target {
min-width: var(--min-touch-target);
min-height: var(--min-touch-target);
}
/* Large touch target (children 3-8 years) */
.touch-target-child {
min-width: var(--space-2xl); /* 72-80px */
min-height: var(--space-2xl);
}
/* Extra-large target (toddlers/accessibility) */
.touch-target-xl {
min-width: var(--space-3xl); /* 108-120px */
min-height: var(--space-3xl);
}
/* Expanded hit area for small visual elements */
.touch-expand::before {
content: '';
position: absolute;
inset: -8px;
/* Or for child-friendly: */
inset: calc(-1 * var(--space-s));
}
```
### Button Sizing for Children
```css
/* Primary game button - child-friendly */
.game-button {
min-width: var(--space-3xl);
min-height: var(--space-2xl);
padding: var(--space-m) var(--space-xl);
font-size: var(--step-1);
border-radius: var(--space-s);
}
/* Card selection - large tap area */
.word-card {
min-width: 120px;
min-height: 120px;
padding: var(--space-m);
}
```
---
## Media Queries for iPad Pro
### Device-Specific Targeting
```css
/* iPad Pro 12.9" Landscape */
@media screen and (min-width: 1024px) and (max-width: 1366px) and (orientation: landscape) {
/* Landscape-specific styles */
}
/* iPad Pro 12.9" Portrait */
@media screen and (min-width: 1024px) and (max-width: 1366px) and (orientation: portrait) {
/* Portrait-specific styles */
}
/* iPad Pro 12.9" - Any orientation */
@media screen and (min-width: 1024px) and (max-width: 1366px) {
/* Tablet-specific styles */
}
/* High-DPI screens (Retina) */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
/* High-DPI assets and adjustments */
}
```
### Pointer and Hover Detection
iPadOS supports both touch and Apple Pencil/trackpad. Detect input type:
```css
/* Touch-only devices */
@media (hover: none) and (pointer: coarse) {
.tooltip-trigger:hover .tooltip {
/* Don't show hover tooltips on touch */
display: none;
}
}
/* Devices with fine pointer (trackpad/mouse) */
@media (hover: hover) and (pointer: fine) {
.interactive:hover {
/* Show hover states */
background: var(--color-hover-overlay);
}
}
/* Devices with coarse pointer (finger/stylus) */
@media (pointer: coarse) {
.interactive {
/* Larger touch targets */
min-height: var(--min-touch-target);
min-width: var(--min-touch-target);
}
}
```
### Container Queries (Preferred)
Container queries are preferred over viewport queries for component responsiveness:
```css
/* Set container on parent */
.game-board {
container-type: inline-size;
}
/* Component responds to container, not viewport */
@container (inline-size > 800px) {
.word-card {
flex-direction: row;
padding: var(--space-l);
}
}
@container (inline-size <= 600px) {
.word-card {
flex-direction: column;
padding: var(--space-m);
}
}
```
---
## ProMotion (120Hz) Animation Optimization
### Hardware Acceleration
Safari on iPadOS uses hardware acceleration for specific CSS properties. **Always animate these properties for smooth 120Hz performance:**
| Property | Hardware Accelerated |
|----------|---------------------|
| `transform` | Yes - always use |
| `opacity` | Yes - always use |
| `filter` | Yes (simple filters) |
| `backdrop-filter` | Yes |
| `will-change` | Promotes to GPU layer |
| Property | NOT Hardware Accelerated |
|----------|-------------------------|
| `width`, `height` | Triggers layout |
| `margin`, `padding` | Triggers layout |
| `top`, `left`, `right`, `bottom` | Triggers layout |
| `border-width` | Triggers layout |
| `font-size` | Triggers layout |
| `box-shadow` | CPU intensive |
### Animation Timing for 120Hz
At 120Hz, each frame is 8.33ms. Animation timing feels different:
```css
:root {
/* Snappy micro-interactions */
--duration-instant: 100ms; /* 12 frames at 120Hz */
--duration-quick: 150ms; /* 18 frames at 120Hz */
--duration-normal: 200ms; /* 24 frames at 120Hz */
--duration-slow: 350ms; /* 42 frames at 120Hz */
/* Ease curves for natural feel */
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1); /* Decelerate */
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1); /* Standard */
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Overshoot */
}
```
### CSS Transitions (ProMotion Optimized)
```css
.card {
/* Use transform instead of position */
transform: translateY(0) scale(1);
opacity: 1;
transition:
transform var(--duration-normal) var(--ease-out),
opacity var(--duration-quick) var(--ease-out);
}
.card:active {
/* Press feedback - instant response */
transform: scale(0.97);
transition-duration: var(--duration-instant);
}
.card.selected {
transform: translateY(-8px) scale(1.02);
}
```
### Anime.js 4.0 for Complex Animations
Use Anime.js for multi-property animations:
```javascript
import { animate, createSpring } from 'animejs';
// Check reduced motion preference first
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function cardSelectAnimation(element) {
if (prefersReducedMotion) {
element.classList.add('selected');
return;
}
return animate(element, {
scale: [1, 1.05, 1],
translateY: [0, -8, -4],
duration: 250,
ease: 'outBack'
});
}
// Spring physics for natural feel
function dragDropAnimation(element) {
return animate(element, {
x: 0,
y: 0,
ease: createSpring({ stiffness: 400, damping: 25 }),
});
}
```
### will-change Usage
Use `will-change` sparingly - only on elements that will animate:
```css
/* Apply before animation starts */
.card {
will-change: transform;
}
/* Remove after animation completes (via JavaScript) */
.card.animation-complete {
will-change: auto;
}
```
```javascript
// Anime.js lifecycle hooks
animate(element, {
scale: [1, 1.1, 1],
onBegin: () => {
element.style.willChange = 'transform';
},
onComplete: () => {
element.style.willChange = 'auto';
}
});
```
---
## Touch Interaction Patterns
### Pointer Events (Unified API)
Use Pointer Events API for unified touch/mouse/stylus handling:
```javascript
class DraggableCard extends HTMLElement {
#isDragging = false;
#startX = 0;
#startY = 0;
connectedCallback() {
this.addEventListener('pointerdown', this);
this.addEventListener('pointermove', this);
this.addEventListener('pointerup', this);
this.addEventListener('pointercancel', this);
// Prevent default touch behaviors
this.style.touchAction = 'none';
}
disconnectedCallback() {
this.removeEventListener('pointerdown', this);
this.removeEventListener('pointermove', this);
this.removeEventListener('pointerup', this);
this.removeEventListener('pointercancel', this);
}
handleEvent(e) {
switch (e.type) {
case 'pointerdown':
this.#handlePointerDown(e);
break;
case 'pointermove':
this.#handlePointerMove(e);
break;
case 'pointerup':
case 'pointercancel':
this.#handlePointerUp(e);
break;
}
}
#handlePointerDown(e) {
this.#isDragging = true;
this.#startX = e.clientX;
this.#startY = e.clientY;
// Capture pointer for reliable tracking
this.setPointerCapture(e.pointerId);
// Visual feedback
this.setAttribute('aria-grabbed', 'true');
}
#handlePointerMove(e) {
if (!this.#isDragging) return;
const deltaX = e.clientX - this.#startX;
const deltaY = e.clientY - this.#startY;
// Direct transform for 120Hz smoothness - no animate() during drag
this.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
}
#handlePointerUp(e) {
if (!this.#isDragging) return;
this.#isDragging = false;
this.releasePointerCapture(e.pointerId);
this.removeAttribute('aria-grabbed');
// Animate back with physics
this.#animateDrop();
}
#animateDrop() {
animate(this, {
x: 0,
y: 0,
duration: 300,
ease: 'outBack'
});
}
}
```
### CSS touch-action Control
```css
/* Allow vertical scroll only */
.scrollable-list {
touch-action: pan-y;
}
/* Disable all touch behaviors (for custom drag) */
.draggable {
touch-action: none;
}
/* Allow pinch-zoom but not pan */
.zoomable-image {
touch-action: pinch-zoom;
}
/* Default - allow all */
.interactive {
touch-action: manipulation; /* Disables double-tap zoom delay */
}
```
### Tap vs Long-Press
```javascript
class TappableElement extends HTMLElement {
#longPressTimeout = null;
#longPressTriggered = false;
static LONG_PRESS_DURATION = 500; // ms
connectedCallback() {
this.addEventListener('pointerdown', this);
this.addEventListener('pointerup', this);
this.addEventListener('pointercancel', this);
}
handleEvent(e) {
switch (e.type) {
case 'pointerdown':
this.#longPressTriggered = false;
this.#longPressTimeout = setTimeout(() => {
this.#longPressTriggered = true;
this.#handleLongPress(e);
}, TappableElement.LONG_PRESS_DURATION);
break;
case 'pointerup':
clearTimeout(this.#longPressTimeout);
if (!this.#longPressTriggered) {
this.#handleTap(e);
}
break;
case 'pointercancel':
clearTimeout(this.#longPressTimeout);
break;
}
}
#handleTap(e) {
this.dispatchEvent(new CustomEvent('tap', {
bubbles: true,
detail: { x: e.clientX, y: e.clientY }
}));
}
#handleLongPress(e) {
// Haptic feedback if available
if (navigator.vibrate) navigator.vibrate(10);
this.dispatchEvent(new CustomEvent('longpress', {
bubbles: true,
detail: { x: e.clientX, y: e.clientY }
}));
}
}
```
### Hover State Handling
iPadOS triggers `:hover` on first tap, then `:active` on second. Handle this:
```css
/* Default: no hover effects for touch */
@media (hover: none) {
.button:hover {
/* Reset hover styles for touch devices */
background: var(--button-bg);
}
}
/* Only apply hover for devices that support it */
@media (hover: hover) {
.button:hover {
background: var(--color-hover-overlay);
transform: translateY(-1px);
}
}
/* Active state works on both */
.button:active {
background: var(--color-active-overlay);
transform: scale(0.98);
}
```
---
## Layout Patterns for 12.9" Display
### Grid for Large Tablets
```css
/* Game board - uses full width */
.game-board {
container-type: inline-size;
display: grid;
gap: var(--grid-gutter);
padding: var(--space-m);
}
/* Landscape: 4 columns */
@container (inline-size >= 1024px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Portrait: 3 columns */
@container (inline-size >= 768px) and (inline-size < 1024px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Small containers: 2 columns */
@container (inline-size < 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
```
### Avoiding "Blown-Up Phone" Layouts
```css
/* DON'T: Single column on large screens */
.page-content {
max-width: 100%; /* Content stretches uncomfortably */
}
/* DO: Constrain content width, use whitespace */
.page-content {
max-width: var(--grid-max-width); /* 1240px */
margin-inline: auto;
padding-inline: var(--space-l);
}
/* DO: Multi-column layouts on large screens */
.main-layout {
display: grid;
grid-template-columns: 1fr 2fr 1fr; /* Sidebar | Main | Aside */
gap: var(--grid-gutter);
}
```
### Orientation Change Handling
```css
/* Landscape-specific adjustments */
@media (orientation: landscape) {
.game-ui {
flex-direction: row;
.sidebar {
width: 280px;
flex-shrink: 0;
}
.main-content {
flex: 1;
}
}
}
/* Portrait-specific adjustments */
@media (orientation: portrait) {
.game-ui {
flex-direction: column;
.sidebar {
width: 100%;
height: auto;
}
.main-content {
flex: 1;
}
}
}
```
### JavaScript Orientation Detection
```javascript
class OrientationAware extends HTMLElement {
#mediaQuery = window.matchMedia('(orientation: landscape)');
connectedCallback() {
this.#mediaQuery.addEventListener('change', this);
this.#updateOrientation();
}
disconnectedCallback() {
this.#mediaQuery.removeEventListener('change', this);
}
handleEvent(e) {
if (e.type === 'change') {
this.#updateOrientation();
}
}
#updateOrientation() {
const isLandscape = this.#mediaQuery.matches;
this.setAttribute('data-orientation', isLandscape ? 'landscape' : 'portrait');
this.dispatchEvent(new CustomEvent('orientation-change', {
bubbles: true,
detail: { isLandscape }
}));
}
}
```
---
## Child-Friendly UX Patterns
### Design Principles for Children (Ages 3-8)
| Principle | Implementation |
|-----------|----------------|
| Large tap targets | Minimum 56pt (prefer 72pt+) |
| Clear visual feedback | Immediate bounce/glow on tap |
| Simple navigation | Max 2-3 choices visible |
| Forgiving interactions | Accept imprecise taps, allow undo |
| Consistent layout | Same button positions across screens |
| No time pressure | Avoid timers for young children |
| Positive reinforcement | Celebrate success, gentle error handling |
### Visual Feedback for Children
```javascript
// Immediate, exaggerated feedback
function childFriendlyTapFeedback(element) {
// Instant scale down
animate(element, {
scale: 0.9,
duration: 80,
ease: 'linear'
}).then(() => {
// Bouncy return
animate(element, {
scale: [0.9, 1.15, 1],
duration: 300,
ease: 'outBack'
});
});
}
// Success celebration
function celebrateSuccess(element) {
return animate(element, {
scale: [1, 1.3, 1],
rotate: [0, -8, 8, -4, 4, 0],
duration: 500,
ease: 'outElastic(1, 0.5)'
});
}
// Gentle error shake (not scary)
function gentleErrorShake(element) {
return animate(element, {
x: [0, -4, 4, -2, 2, 0],
duration: 300,
ease: 'linear'
});
}
```
### Progressive Disclosure
```html
```
### Forgiving Touch Zones
```css
/* Extra-large interactive cards for children */
.word-card-child {
min-width: 140px;
min-height: 140px;
padding: var(--space-l);
/* Expanded hit area beyond visual bounds */
position: relative;
}
.word-card-child::before {
content: '';
position: absolute;
inset: calc(-1 * var(--space-m)); /* 27-30px expansion */
}
/* Wide spacing between cards to prevent mis-taps */
.card-grid-child {
gap: var(--space-l); /* 36-40px */
}
```
### Audio Feedback (Optional)
```javascript
class AudioFeedback {
#audioContext = null;
#enabled = true;
constructor() {
// Initialize on first user interaction
document.addEventListener('pointerdown', () => {
if (!this.#audioContext) {
this.#audioContext = new AudioContext();
}
}, { once: true });
}
playTap() {
if (!this.#enabled || !this.#audioContext) return;
this.#playTone(800, 50); // Short, high beep
}
playSuccess() {
if (!this.#enabled || !this.#audioContext) return;
this.#playMelody([523, 659, 784], 100); // C-E-G chord
}
playError() {
if (!this.#enabled || !this.#audioContext) return;
this.#playTone(200, 150); // Low, longer tone
}
#playTone(frequency, duration) {
const oscillator = this.#audioContext.createOscillator();
const gainNode = this.#audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.#audioContext.destination);
oscillator.frequency.value = frequency;
gainNode.gain.setValueAtTime(0.1, this.#audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.#audioContext.currentTime + duration / 1000);
oscillator.start();
oscillator.stop(this.#audioContext.currentTime + duration / 1000);
}
#playMelody(frequencies, noteDuration) {
frequencies.forEach((freq, i) => {
setTimeout(() => this.#playTone(freq, noteDuration), i * noteDuration);
});
}
toggle(enabled) {
this.#enabled = enabled;
}
}
```
---
## Safari/WebKit Considerations
### Supported CSS Features (iPadOS 17+)
| Feature | Support | Notes |
|---------|---------|-------|
| Container Queries | Full | Use `cqi` units |
| `:has()` selector | Full | Use for complex states |
| Subgrid | Full | Use for nested grids |
| `view-transitions` | Partial | Same-document only |
| `field-sizing` | Yes | Auto-sizing textareas |
| `@layer` | Full | CSS cascade layers |
| `color-mix()` | Full | Dynamic color blending |
| Backdrop filter | Full | With `-webkit-` prefix |
### Safari-Specific CSS
```css
/* Backdrop blur - requires prefix */
.modal-backdrop {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
/* Prevent iOS font boosting */
body {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
/* Smooth scrolling with momentum */
.scrollable {
-webkit-overflow-scrolling: touch;
overflow-y: auto;
}
/* Hide scrollbar but keep functionality */
.scrollable::-webkit-scrollbar {
display: none;
}
```
### Known Safari Limitations
| Issue | Workaround |
|-------|------------|
| No `popover` attribute | Use `