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 เรื่องหลัก:
100vhเปลี่ยนความหมาย — ค่าผันตาม tab modeposition: fixed/stickyโดนตัด — Safari ไม่ render content ใต้ navigation controls- Toolbar อ่าน CSS ของคุณ — เอาสีจาก fixed element มา tint toolbar
theme-colormeta 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:
| Mode | URL bar expanded | URL bar collapsed |
|---|---|---|
| Compact (default) | 718px | 754px |
| Bottom | 658px | 754px |
| Top | 668px | 768px |
ปัญหา: 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░░░│
└────────────┘ └────────────┘
ผลกระทบ:
- Full-screen modal ที่ fixed — ด้านล่างโดนตัดไม่ครบจอ
- Navigation menu ที่ fixed — ปุ่มด้านล่างกดไม่ได้
- Cookie banner / CTA bar ที่ fixed bottom — หายเลย
- Chat widget ที่ fixed bottom-right — โดนทับ
ปัญหาที่ 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 | ใช้ตอน |
|---|---|
dvh | Content ที่ต้อง “เต็มจอ” ตลอด — 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() บน source | Blur 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 ใต้ controls | viewport-fit=cover + env(safe-area-inset-*) |
| FAB/footer โดน URL bar ทับ | URL bar ลอยทับล่างจอ | calc(N + env(safe-area-inset-bottom)) |
| Toolbar สีผิด | Safari อ่าน CSS ของ fixed element | Transparent parent + absolute visual child |
| Modal tint toolbar ตลอด | opacity: 0 ≠ ซ่อนจาก Safari | ใช้ display: none แทน |
| Keyboard ไม่ blur | Keyboard อยู่นอก compositing | filter: blur() บน source แทน backdrop-filter |
สิ่งที่แย่ที่สุดคือ — Apple ไม่มี documentation สำหรับพฤติกรรมเหล่านี้เลย ทุกอย่างที่เขียนในบทความนี้มาจาก community reverse-engineering และ trial-and-error ทั้งนั้น WebKit bugs ที่ report ไปก็โดนปิดเป็น “by design”
ถ้าคุณเป็น web developer ที่มี user ใช้ iPhone — เช็คเว็บตาม checklist ข้างบนวันนี้ ก่อนที่ user จะมาบอกว่า “เว็บพังครับ”
อ้างอิง:
- iOS 26.0 | Be prepared for viewport changes in Safari
- Safari 26 Liquid Glass: fixing toolbar tinting for web developers
- WebKit Features in Safari 26.0
Footnotes
-
Liquid Glass — ภาษาออกแบบที่ Apple เปิดตัวใน WWDC 2025 ใช้ร่วมกันใน iOS 26, iPadOS 26, macOS Tahoe โดยเปลี่ยน toolbar/tab bar ให้เป็นกระจกโปร่งแสงที่ดูดสี background มาใช้ ↩
-
Dynamic Viewport Units —
dvh,svh,lvhเปิดตัวใน CSS spec ปี 2022 ออกแบบมาแก้ปัญหา mobile browser toolbar ที่ขนาดเปลี่ยนได้ Safari รองรับตั้งแต่ 15.4, Chrome ตั้งแต่ 108 ↩ -
env(safe-area-inset-*)— CSS environment variable ที่ browser inject ให้ มี 4 ค่า:top,bottom,left,rightใช้ได้ทั้งในpadding,margin,calc()ต้องมีviewport-fit=coverใน meta viewport ถึงจะได้ค่าจริง (ไม่งั้นได้ 0 หมด) ↩