xiaohongshu
xiaohongshu
ipad-pro-design
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.
Full skill instructions
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.
utopia-fluid-scales: Fluid typography and spacing tokens (cqi-based)utopia-container-queries: Container setup for fluid scalesweb-components: Component architecture patternsux-accessibility: Focus management and WCAG complianceux-animation-motion: Animation timing and reduced motionanimejs-v4: Hardware-accelerated animations| 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 |
CSS Pixels (Landscape): 1366 x 1024
CSS Pixels (Portrait): 1024 x 1366
Device Pixels (Landscape): 2732 x 2048
Device Pixels (Portrait): 2048 x 2732
| 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") |
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
| 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) |
<!-- DON'T: Disabling zoom breaks accessibility -->
<meta name="viewport" content="... user-scalable=no">
<meta name="viewport" content="... maximum-scale=1">
<!-- DON'T: Fixed width creates horizontal scroll -->
<meta name="viewport" content="width=1024">
<!-- Full-screen web app (home screen) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- Prevent text size adjustment -->
<meta name="apple-mobile-web-app-title" content="Fantasy Phonics">
<!-- Theme color for Safari UI -->
<meta name="theme-color" content="#252119">
Safari on iPadOS provides safe area values via env():
: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);
}
/* 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));
}
The home indicator appears at the bottom of screen. Avoid placing interactive elements in this zone:
.interactive-bottom-zone {
/* Ensure 20pt clearance from bottom */
margin-bottom: max(20px, var(--safe-area-inset-bottom));
}
| 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 |
/* 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));
}
/* 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);
}
/* 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 */
}
iPadOS supports both touch and Apple Pencil/trackpad. Detect input type:
/* 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 are preferred over viewport queries for component responsiveness:
/* 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);
}
}
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 |
At 120Hz, each frame is 8.33ms. Animation timing feels different:
: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 */
}
.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);
}
Use Anime.js for multi-property animations:
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 }),
});
}
Use will-change sparingly - only on elements that will animate:
/* Apply before animation starts */
.card {
will-change: transform;
}
/* Remove after animation completes (via JavaScript) */
.card.animation-complete {
will-change: auto;
}
// Anime.js lifecycle hooks
animate(element, {
scale: [1, 1.1, 1],
onBegin: () => {
element.style.willChange = 'transform';
},
onComplete: () => {
element.style.willChange = 'auto';
}
});
Use Pointer Events API for unified touch/mouse/stylus handling:
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'
});
}
}
/* 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 */
}
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 }
}));
}
}
iPadOS triggers :hover on first tap, then :active on second. Handle this:
/* 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);
}
/* 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);
}
}
/* 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);
}
/* 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;
}
}
}
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 }
}));
}
}
| 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 |
// 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'
});
}
<!-- Show only essential options initially -->
<nav class="game-nav" aria-label="Game navigation">
<button class="nav-item nav-item--current" aria-current="page">
<span class="icon" aria-hidden="true">play_arrow</span>
<span class="label">Play</span>
</button>
<!-- Advanced options hidden until needed -->
<button class="nav-item" hidden data-requires-progress="5">
<span class="icon" aria-hidden="true">settings</span>
<span class="label">Settings</span>
</button>
</nav>
/* 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 */
}
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;
}
}
| 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 |
/* 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;
}
| Issue | Workaround |
|---|---|
No popover attribute | Use <dialog> 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 |
// 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": "/"
}
transform and opacity for animations (GPU accelerated)will-change before animations, remove aftercontain: layout on fixed-size containersscroll-behavior: smooth instead)touch-action: manipulation to remove 300ms tap delaypointer-events: none to elements during dragpointerdown/pointermove/pointerup (not touch events)passive: true on scroll/touch listeners when not preventing defaultdisconnectedCallbackAbortController for cancellable fetch requestssrcset for images at 1x and 2x resolutionsloading="lazy"<link rel="preload">/* Essential meta tag */
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
/* Safe area padding */
padding: env(safe-area-inset-top) env(safe-area-inset-right)
env(safe-area-inset-bottom) env(safe-area-inset-left);
/* Adults */ min-width: 44px; min-height: 44px;
/* Children */ min-width: 72px; min-height: 72px;
/* 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) { }
--duration-instant: 100ms; /* Tap feedback */
--duration-quick: 150ms; /* State changes */
--duration-normal: 200ms; /* Transitions */
/* 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 */
}
This skill references and integrates with:
css/styles/accessibility.css - Touch target tokens, focus ringscss/styles/space.css - Utopia spacing scalecss/styles/typography.css - Utopia type scalecss/styles/transitions.css - Animation timing tokensxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience