T Thingnoy

Safari บน iOS 26 พังเว็บคุณยังไง — และวิธีแก้ที่ต้องรู้

อ่าน ~47 นาที
Safari บน iOS 26 พังเว็บคุณยังไง — และวิธีแก้ที่ต้องรู้
Table of Contents

Safari บน iOS 26 พังเว็บคุณยังไง — และวิธีแก้ที่ต้องรู้

ตั้งแต่ iOS 26 Safari ไม่ได้เป็น “หน้าต่าง” อีกต่อไป — มันกลายเป็น full-screen ที่มี Dynamic Island ข้างบน และ URL bar ลอยทับ content ข้างล่าง ทุกอย่างเป็น Liquid Glass โปร่งแสง

เว็บที่เคยใช้ 100vh + fixed navbar แล้วดีมาตลอด — iOS 26 อาจพังทันที


เกิดอะไรขึ้นใน iOS 26

Apple ออกแบบ Safari ใหม่ทั้งหมดด้วย Liquid Glass1 — UI ที่โปร่งแสง เบลอ background ใต้ toolbar ทำให้ผู้ใช้รู้สึกเหมือนเว็บ “เต็มจอ” จริงๆ

แต่ “เต็มจอ” แปลว่า:

iOS 25 (เดิม):                    iOS 26 (ใหม่):

┌──────────────┐                 ┌──────────────┐
│  Status Bar  │ ← ทึบ           │░░░░░░░░░░░░░░│ ← Dynamic Island
├──────────────┤                 │              │    + Liquid Glass
│              │                 │   content    │    ทับ content!
│   content    │                 │   เต็มจอ     │
│              │                 │              │
│              │                 │              │
├──────────────┤                 │░░░░░░░░░░░░░░│ ← URL bar ลอย
│   URL bar    │ ← ทึบ           └──────────────┘    ทับ content!
└──────────────┘

สิ่งที่เปลี่ยนมีผลกับ web developer 4 เรื่องหลัก:

  1. 100vh เปลี่ยนความหมาย — ค่าผันตาม tab mode
  2. position: fixed/sticky โดนตัด — Safari ไม่ render content ใต้ navigation controls
  3. Toolbar อ่าน CSS ของคุณ — เอาสีจาก fixed element มา tint toolbar
  4. theme-color meta tag ไม่ทำงานแล้ว — Safari ไม่อ่านเลย

ปัญหาที่ 1 — 100vh ไม่เท่ากันทุก mode

Safari iOS 26 มี 3 tab modes:

Compact Mode    Bottom Mode     Top Mode
(default)

┌────────────┐  ┌────────────┐  ┌────────────┐
│ ░░░░░░░░░░ │  │ ░░░░░░░░░░ │  │ ██████████ │ ← URL bar
│            │  │            │  │            │
│   718px    │  │   658px    │  │   668px    │
│  expanded  │  │  expanded  │  │  expanded  │
│            │  │            │  │            │
│ ░░░░░░░░░░ │  │ ██████████ │  │            │
└────────────┘  └────────────┘  └────────────┘
     ↓               ↓               ↓
   754px           754px           768px
  collapsed       collapsed       collapsed

วัดจริงบน iPhone 16 Pro:

ModeURL bar expandedURL bar collapsed
Compact (default)718px754px
Bottom658px754px
Top668px768px

ปัญหา: 100vh ตอนนี้ยึดจาก window.outerHeight ที่คงที่ — แต่พื้นที่ใช้งานจริง (window.innerHeight) เปลี่ยนตาม mode และ state ของ URL bar

/* ❌ เดิมใช้แบบนี้ — iOS 26 อาจไม่ตรงกับพื้นที่จริง */
.hero {
  height: 100vh;
}

ปัญหาที่ 2 — position: fixed โดน Safari ตัด

อันนี้คือ ปัญหาที่แรงที่สุด — Safari iOS 26 ไม่ render content ที่เป็น position: fixed หรือ position: sticky ถ้ามันอยู่ใต้ navigation controls ที่ล่างจอ

ก่อน iOS 26:              iOS 26:

┌────────────┐            ┌────────────┐
│  fixed nav │            │  fixed nav │
│            │            │            │
│  content   │            │  content   │
│            │            │            │
│  fixed     │            │  ┄┄┄┄┄┄┄┄  │ ← ถูกตัดตรงนี้!
│  footer    │            │░░URL bar░░░│
└────────────┘            └────────────┘

ผลกระทบ:


ปัญหาที่ 3 — Toolbar อ่าน CSS ของคุณ (Liquid Glass Tinting)

นี่คือเรื่องที่ developer ส่วนใหญ่ไม่รู้ — Safari 26 สแกนหา fixed/sticky element ที่อยู่ใกล้ขอบจอ แล้ว อ่านสี CSS มา tint toolbar

สิ่งที่ Safari อ่าน

Safari สแกน:
├── position: fixed หรือ sticky
├── อยู่ ≤ 4px จากขอบบน (status bar)
│   หรือ ≤ 3px จากขอบล่าง (toolbar)
├── กว้าง ≥ 80% ของ viewport
└── อ่าน:
    ├── background-color
    └── backdrop-filter

สิ่งที่ Safari ไม่อ่าน (แต่คุณคิดว่ามันอ่าน)

❌ position: absolute (แม้อยู่ใน fixed parent)
❌ ::before / ::after pseudo-elements
❌ <meta name="theme-color"> ← ไม่อ่านเลยแล้ว!
❌ Content ที่ไม่ใช่ fixed/sticky

แต่สิ่งที่ Safari อ่าน แม้ซ่อน

⚠️  opacity: 0              ← อ่าน! (แค่มองไม่เห็น ≠ ไม่มี)
⚠️  pointer-events: none    ← อ่าน!
⚠️  fixed ซ้อน fixed        ← อ่าน!

ปัญหาตัวอย่าง: ถ้าคุณมี modal overlay ที่ใช้ opacity: 0 ตอนซ่อน + background: rgba(0,0,0,0.5)toolbar จะเป็นสีดำตลอด แม้ modal ไม่ได้เปิด


วิธีแก้ — ทีละปัญหา

Fix 1: ใช้ Dynamic Viewport Units แทน vh

CSS มี viewport units ใหม่2ที่ Safari รองรับตั้งแต่ iOS 15.4:

┌──────────────┐
│              │ ↕ svh (Small Viewport Height)
│              │   = viewport ตอน URL bar เปิดเต็ม
│              │
│              │ ↕ lvh (Large Viewport Height)
│              │   = viewport ตอน URL bar ย่อ
│              │
│              │ ↕ dvh (Dynamic Viewport Height)
│              │   = ตาม state ปัจจุบัน (เปลี่ยนได้ real-time)
└──────────────┘

เปลี่ยนจาก vh เป็น dvh:

/* ❌ เดิม */
.hero {
  height: 100vh;
}

/* ✅ ใหม่ — dynamic ตาม state จริง */
.hero {
  height: 100dvh;
}

/* ✅ ปลอดภัยกว่า — ใส่ fallback สำหรับ browser เก่า */
.hero {
  height: 100vh;       /* fallback */
  height: 100dvh;      /* override ถ้า support */
}

เลือกใช้ตัวไหน:

Unitใช้ตอน
dvhContent ที่ต้อง “เต็มจอ” ตลอด — hero, modal, full-screen menu
svhต้องการ safe minimum — กัน content หลุดจอตอน URL bar เปิด
lvhต้องการ maximum ตอน URL bar ย่อ — ใช้น้อย

Fix 2: viewport-fit=cover + Safe Area Insets

ถ้าต้องการให้ content ขยายเต็มจอ (ไม่มี blank space ที่ขอบ) ต้องใส่ viewport-fit=cover:

<!-- 👇 meta viewport — เพิ่ม viewport-fit=cover -->
<meta name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover">

แต่ viewport-fit=cover ทำให้ content ขยายเข้าไปในเขต safe area (โดน notch / Dynamic Island / home indicator ทับ) ต้องใส่ padding ชดเชย:

/* 👇 ใส่ padding ให้ content ไม่โดน safe area ทับ */
body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* 👇 หรือใช้กับ element เฉพาะ */
.bottom-bar {
  padding-bottom: calc(16px + env(safe-area-inset-bottom));
}

env(safe-area-inset-*)3 คือตัวแปรที่ browser ให้ — บอกว่าแต่ละด้านมีพื้นที่ “อันตราย” กี่ pixel

ไม่มี viewport-fit=cover:         มี viewport-fit=cover:

┌──────────────┐                  ┌──────────────┐
│  blank space │ ← ว่าง            │░░░ content ░░│ ← content ขยายเต็ม
├──────────────┤                  │  ต้อง padding │    แต่ต้อง padding
│   content    │                  │  ด้วย env()   │    เพื่อกัน safe area
├──────────────┤                  │░░░ content ░░│
│  blank space │                  └──────────────┘
└──────────────┘

Fix 3: Fixed Header — ใช้ Transparent Parent + Absolute Child

ปัญหา: ถ้า header เป็น position: fixed + มี background-color → Safari จะดูดสีนั้นไปทำ toolbar tint

วิธีแก้: ทำ header ให้ transparent แล้วย้าย visual ไปอยู่ใน position: absolute child (ที่ Safari ไม่อ่าน)

<!-- 👇 parent fixed แต่ transparent -->
<header style="
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background-color: transparent;
">
  <!-- 👇 visual อยู่ใน absolute child — Safari ไม่อ่าน -->
  <div style="
    position: absolute;
    inset: 0;
    background-color: rgba(255, 255, 255, 0.8);
    backdrop-filter: blur(12px);
  " aria-hidden="true"></div>

  <!-- 👇 content อยู่ข้างบน visual -->
  <nav style="position: relative; z-index: 1;">
    ...
  </nav>
</header>
Safari เห็น:                  User เห็น:

fixed header                  header พร้อม
├── background: transparent   background blur
├── absolute child ← ไม่อ่าน     สวยเหมือนเดิม
└── nav content

→ toolbar ไม่โดน tint!          → UX ไม่เปลี่ยน!

Fix 4: Bottom-Up Design — ย้าย UI ขึ้นให้พ้น URL Bar

ตั้งแต่ iOS 26 ล่างจอไม่ใช่ “ขอบจอ” อีกต่อไป — มันเป็น restricted zone ที่ URL bar ลอยทับอยู่ ต้องออกแบบ “จากล่างขึ้น” แทน

ก่อน iOS 26:              iOS 26:

┌────────────┐            ┌────────────┐
│            │            │            │
│            │            │            │
│            │            │            │
│   [FAB] ←──── bottom:20  │            │
│            │            │░░URL bar░░░│ ← FAB โดนทับ!
└────────────┘            └────────────┘

Floating Action Button (FAB): ยกขึ้นสูงกว่าเดิม โดยเผื่อ safe area

/* ❌ เดิม — โดน URL bar ทับ */
.fab {
  position: fixed;
  bottom: 20px;
  right: 20px;
}

/* ✅ ใหม่ — เผื่อ safe area */
.fab {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom));
  right: 20px;
}

Sticky Footer: ใส่ buffer โปร่งใสด้านล่าง ไม่งั้น text/icon โดน URL bar บัง

.sticky-footer {
  position: sticky;
  bottom: 0;
  /* 👇 padding ด้านล่างเผื่อ safe area */
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
  /* 👇 ขยาย background ให้ต่อเนื่อง ไม่ "ตัด" */
  background: linear-gradient(to bottom, #1a1a1a, #1a1a1a);
}

หลักง่ายๆ: ทุก element ที่อยู่ fixed/sticky ด้านล่าง → เพิ่ม env(safe-area-inset-bottom) เสมอ

Fix 5: Background Continuity — ตั้ง html Background

Safari ใช้สี <html> เป็น fallback ตอนไม่เจอ fixed element ที่ขอบล่าง — ถ้าไม่ตั้ง จะได้สีขาวหรือดำตามที่ Safari เลือก

/* 👇 ตั้งสีให้ html เสมอ — Safari ใช้ tint bottom bar */
html {
  background-color: #ffffff;    /* light mode */
}

@media (prefers-color-scheme: dark) {
  html {
    background-color: #1a1a1a;  /* dark mode */
  }
}

Fix 6: Modal/Overlay — ใช้ display: none ไม่ใช่ opacity: 0

จำได้ว่า Safari อ่าน opacity: 0 element? ถ้า overlay ใช้ opacity ซ่อน → toolbar โดน tint ตลอด

// ❌ ผิด — Safari ยังอ่าน background สีดำ แม้ opacity: 0
function openModal() {
  backdrop.style.opacity = '1';
}
function closeModal() {
  backdrop.style.opacity = '0';  // Safari ยังอ่านอยู่!
}

// ✅ ถูก — ใช้ display: none ตอนซ่อน
function openModal() {
  backdrop.style.display = 'block';
  // 👇 ใช้ rAF กันกระตุก
  requestAnimationFrame(() => {
    backdrop.classList.add('is-open');
  });
}

function closeModal() {
  backdrop.classList.remove('is-open');
  // 👇 รอ transition จบก่อน hide
  setTimeout(() => {
    backdrop.style.display = 'none';
  }, 200);
}
.modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0;
  transition: opacity 0.2s;
}
.modal-backdrop.is-open {
  opacity: 1;
}

Fix 7: Modal Height — ใช้ window.outerHeight

ถ้า modal ต้อง cover ทั้งจอรวม URL bar:

function openModal(modalEl) {
  // 👇 outerHeight = เต็มจอจริงๆ ไม่ขึ้นกับ tab mode
  modalEl.style.height = `${window.outerHeight}px`;
}

window.outerHeight ให้ค่าคงที่ไม่ว่า URL bar จะ expanded หรือ collapsed — ต่างจาก innerHeight ที่ผันตาม state

Fix 8: iOS Keyboard + Overlay — Blur ที่ Source

ปัญหาเฉพาะ iOS: ตอนเปิด keyboard + มี overlay → keyboard accessory bar render นอก compositing context ของ overlay → backdrop-filter: blur() บน overlay ไม่ blur keyboard area

┌────────────────┐
│   overlay       │ ← backdrop-filter: blur() ✅
│   (blurred)     │
├────────────────┤
│ keyboard bar   │ ← อยู่นอก compositing context
│                │    blur ไม่โดน! ❌
└────────────────┘

วิธีแก้: ใช้ filter: blur() บน source content แทน backdrop-filter บน overlay

// ❌ backdrop-filter ไม่ blur keyboard area
function openOverlay() {
  overlay.style.backdropFilter = 'blur(8px)';
}

// ✅ filter ที่ source — blur ทุกอย่างรวม keyboard
function openOverlay() {
  document.querySelector('.layout-main').style.filter = 'blur(8px)';
  document.querySelector('footer').style.filter = 'blur(8px)';
  overlay.style.display = 'block';
}

function closeOverlay() {
  document.querySelector('.layout-main').style.filter = '';
  document.querySelector('footer').style.filter = '';
  overlay.style.display = 'none';
}

ความแตกต่าง:

วิธีทำงานยังไงKeyboard area
backdrop-filter: blur()Blur สิ่งที่อยู่ “ใต้” overlay❌ ไม่ blur
filter: blur() บน sourceBlur element ที่ render → pixel เปลี่ยนก่อน composite✅ blur ได้

Checklist — เช็คเว็บก่อน iOS 26 ออก

ก่อนจะมี user report ว่า “เว็บพังบน Safari” ลองเช็คตาม list นี้:

□ meta viewport มี viewport-fit=cover มั้ย?
□ ใช้ dvh/svh แทน vh ทุกที่ที่เป็น full-screen แล้วมั้ย?
□ html มี background-color explicit มั้ย?
□ fixed header ใช้ transparent parent + absolute child มั้ย?
□ modal overlay ใช้ display:none ตอนซ่อน (ไม่ใช่ opacity:0) มั้ย?
□ FAB / sticky footer เผื่อ env(safe-area-inset-bottom) มั้ย?
□ fixed bottom bar มี padding-bottom: env(safe-area-inset-bottom) มั้ย?
□ ลบ <meta name="theme-color"> แล้วมั้ย? (ไม่ทำงานแล้ว)

สรุป

ปัญหาสาเหตุวิธีแก้
100vh ไม่เต็มจอVH ผันตาม tab modeใช้ 100dvh + fallback 100vh
Fixed element โดนตัดSafari ไม่ render ใต้ controlsviewport-fit=cover + env(safe-area-inset-*)
FAB/footer โดน URL bar ทับURL bar ลอยทับล่างจอcalc(N + env(safe-area-inset-bottom))
Toolbar สีผิดSafari อ่าน CSS ของ fixed elementTransparent parent + absolute visual child
Modal tint toolbar ตลอดopacity: 0 ≠ ซ่อนจาก Safariใช้ display: none แทน
Keyboard ไม่ blurKeyboard อยู่นอก compositingfilter: blur() บน source แทน backdrop-filter

สิ่งที่แย่ที่สุดคือ — Apple ไม่มี documentation สำหรับพฤติกรรมเหล่านี้เลย ทุกอย่างที่เขียนในบทความนี้มาจาก community reverse-engineering และ trial-and-error ทั้งนั้น WebKit bugs ที่ report ไปก็โดนปิดเป็น “by design”

ถ้าคุณเป็น web developer ที่มี user ใช้ iPhone — เช็คเว็บตาม checklist ข้างบนวันนี้ ก่อนที่ user จะมาบอกว่า “เว็บพังครับ”


อ้างอิง:

Footnotes

  1. Liquid Glass — ภาษาออกแบบที่ Apple เปิดตัวใน WWDC 2025 ใช้ร่วมกันใน iOS 26, iPadOS 26, macOS Tahoe โดยเปลี่ยน toolbar/tab bar ให้เป็นกระจกโปร่งแสงที่ดูดสี background มาใช้

  2. Dynamic Viewport Unitsdvh, svh, lvh เปิดตัวใน CSS spec ปี 2022 ออกแบบมาแก้ปัญหา mobile browser toolbar ที่ขนาดเปลี่ยนได้ Safari รองรับตั้งแต่ 15.4, Chrome ตั้งแต่ 108

  3. env(safe-area-inset-*) — CSS environment variable ที่ browser inject ให้ มี 4 ค่า: top, bottom, left, right ใช้ได้ทั้งใน padding, margin, calc() ต้องมี viewport-fit=cover ใน meta viewport ถึงจะได้ค่าจริง (ไม่งั้นได้ 0 หมด)