🎯

ipad-pro-design

🎯Skill

from matthewharwood/fantasy-phonics

VibeIndex|
What it does

ipad-pro-design skill from matthewharwood/fantasy-phonics

ipad-pro-design

Installation

Install skill:
npx skills add https://github.com/matthewharwood/fantasy-phonics --skill ipad-pro-design
1
AddedJan 29, 2026

Skill Details

SKILL.md

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.

Overview

# 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

or custom implementation |

| Limited Fullscreen API | Works in standalone PWA mode only |

| No Web MIDI | Use Web Audio API instead |

| IndexedDB storage limits | 1GB quota, request via Storage API |

| No beforeinstallprompt | Manual PWA install instructions |

PWA Capabilities

```json

// manifest.json

{

"name": "Fantasy Phonics",

"short_name": "Phonics",

"display": "standalone",

"orientation": "any",

"background_color": "#252119",

"theme_color": "#252119",

"icons": [

{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },

{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }

],

"start_url": "/",

"scope": "/"

}

```

---

Performance Checklist

Rendering Performance

  • [ ] Use transform and opacity for animations (GPU accelerated)
  • [ ] Apply will-change before animations, remove after
  • [ ] Use contain: layout on fixed-size containers
  • [ ] Avoid animating during scroll (use scroll-behavior: smooth instead)
  • [ ] Batch DOM reads and writes (avoid layout thrashing)

Touch Performance

  • [ ] Use touch-action: manipulation to remove 300ms tap delay
  • [ ] Apply pointer-events: none to elements during drag
  • [ ] Use pointerdown/pointermove/pointerup (not touch events)
  • [ ] Set passive: true on scroll/touch listeners when not preventing default

Memory Management

  • [ ] Clean up event listeners in disconnectedCallback
  • [ ] Cancel animations when component unmounts
  • [ ] Use AbortController for cancellable fetch requests
  • [ ] Avoid creating objects in animation loops

Asset Optimization

  • [ ] Use srcset for images at 1x and 2x resolutions
  • [ ] Lazy load images below the fold with loading="lazy"
  • [ ] Preload critical fonts with
  • [ ] Use WebP/AVIF images where supported

---

Quick Reference

Viewport & Safe Areas

```css

/ Essential meta tag /

/ Safe area padding /

padding: env(safe-area-inset-top) env(safe-area-inset-right)

env(safe-area-inset-bottom) env(safe-area-inset-left);

```

Touch Targets

```css

/ Adults / min-width: 44px; min-height: 44px;

/ Children / min-width: 72px; min-height: 72px;

```

Media Queries

```css

/ iPad Pro 12.9" specific /

@media (min-width: 1024px) and (max-width: 1366px) { }

/ Touch-only /

@media (hover: none) and (pointer: coarse) { }

/ Orientation /

@media (orientation: landscape) { }

```

Animation Timing (120Hz)

```css

--duration-instant: 100ms; / Tap feedback /

--duration-quick: 150ms; / State changes /

--duration-normal: 200ms; / Transitions /

```

Integration with Project Tokens

```css

/ Use Utopia space tokens for consistent sizing /

.touch-target-child {

min-width: var(--space-2xl); / 72-80px /

min-height: var(--space-2xl);

padding: var(--space-m); / 27-30px /

gap: var(--space-s); / 18-20px /

}

/ Use Utopia type tokens for readable text /

.game-label {

font-size: var(--step-1); / 21.6-25px /

}

```

---

Files

This skill references and integrates with:

  • css/styles/accessibility.css - Touch target tokens, focus rings
  • css/styles/space.css - Utopia spacing scale
  • css/styles/typography.css - Utopia type scale
  • css/styles/transitions.css - Animation timing tokens