Safe Area

세이프 에어리어

The margin rule that keeps content clear of hardware and gesture zones at the screen edges — the notch, Dynamic Island, home indicator.

Also known as: Safe area inset노치 대응
···
html
<div class="stage"><div class="phone"><div class="notch"></div><div class="screen" id="screen">
<div class="content">
  <div class="topbar" id="topbar">Settings</div>
  <div class="body2"><div class="bar w70"></div><div class="bar w50"></div><div class="bar w60"></div></div>
</div>
<div class="insetTop"></div>
<div class="insetBottom"></div>
<button class="badge" id="badge">ignoring safe-area-inset-top</button></div><div class="home"></div></div></div>
css
.stage{position:absolute;inset:0;display:grid;place-items:center;overflow:hidden}
.phone{position:relative;height:90vmin;max-height:640px;aspect-ratio:9/19.5;background:#08080c;
  border-radius:20%/8.6%;box-shadow:0 0 0 2px rgba(255,255,255,.08) inset,0 12px 30px rgba(0,0,0,.35)}
.phone .notch{position:absolute;left:50%;top:0;transform:translateX(-50%);width:38%;height:5.6%;
  background:#08080c;border-radius:0 0 46% 46%;z-index:80}
.phone .screen{position:absolute;top:7.6%;left:3%;right:3%;bottom:3%;border-radius:11%/4.6%;overflow:hidden;
  background:var(--surface);color:var(--fg)}
.phone .home{position:absolute;left:50%;bottom:1.1%;transform:translateX(-50%);width:32%;height:3px;
  border-radius:2px;background:rgba(255,255,255,.45);z-index:80}
.pfx{position:absolute;left:0;top:0;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;
  background:color-mix(in srgb,var(--accent) 55%,transparent);
  box-shadow:0 0 0 6px color-mix(in srgb,var(--accent) 15%,transparent);pointer-events:none;z-index:999;
  transition:opacity .2s}
.pfx.hold{width:30px;height:30px;margin:-15px 0 0 -15px;
  box-shadow:0 0 0 9px color-mix(in srgb,var(--accent) 20%,transparent);
  transition:width .5s linear,height .5s linear,margin .5s linear,box-shadow .5s linear,opacity .2s}

.content{position:absolute;inset:0}
.topbar{padding:2% 8%;font-weight:800;font-size:clamp(9px,3.2vmin,13px);background:var(--surface);transition:padding-top .35s}
.body2{padding:0 8%;display:grid;gap:8%;margin-top:6%}
.bar{height:clamp(3px,2.4vmin,6px);border-radius:3px;background:var(--line)}
.w70{width:70%}.w50{width:50%}.w60{width:60%}
.insetTop,.insetBottom{position:absolute;left:0;right:0;pointer-events:none;z-index:60;
  background:repeating-linear-gradient(45deg,color-mix(in srgb,var(--accent-2) 32%,transparent) 0 4px,transparent 4px 8px)}
.insetTop{top:0;height:11%}
.insetBottom{bottom:0;height:5%}
.badge{position:absolute;left:50%;bottom:8%;transform:translateX(-50%);padding:3% 8%;border:none;border-radius:999px;
  font-size:clamp(6.5px,2.6vmin,9.5px);font-weight:800;white-space:nowrap;background:#e5534b;color:#fff;
  transition:background .3s,transform .12s}
.badge.ok{background:#1a9e6d}
js
const pfx=document.createElement('div');pfx.className='pfx';document.body.appendChild(pfx);
const screenEl=document.getElementById('screen');
let pfxAuto=true;
function pfxHide(){pfx.style.opacity='0';}
addEventListener('pointerdown',()=>{pfxAuto=false;pfxHide();},{capture:true});
function pfxAt(fx,fy){
  const r=screenEl.getBoundingClientRect();
  const x=r.left+r.width*fx, y=r.top+r.height*fy;
  pfx.style.transform='translate('+x+'px,'+y+'px)';
  return {x,y};
}
function pfxWalk(x0,y0,x1,y1,ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){
      if(!pfxAuto) return res();
      const p=Math.min(1,(now-start)/ms), e=1-Math.pow(1-p,3);
      pfxAt(x0+(x1-x0)*e, y0+(y1-y0)*e);
      if(p<1) requestAnimationFrame(step); else res();
    }
    requestAnimationFrame(step);
  });
}
function pfxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!pfxAuto) return res(); if(now-start<ms) requestAnimationFrame(step); else res(); }
    requestAnimationFrame(step);
  });
}

const topbar=document.getElementById('topbar'), badge=document.getElementById('badge');
let ok=false;
function apply(){
  topbar.style.paddingTop= ok?'12%':'2%';
  badge.textContent= ok?'padded':'ignoring';
  badge.classList.toggle('ok',ok);
}
apply();
badge.addEventListener('pointerdown',()=>{ pfxAuto=false; pfxHide(); ok=!ok; apply(); });
async function autoSeq(){
  while(pfxAuto){
    const r=badge.getBoundingClientRect(), sr=screenEl.getBoundingClientRect();
    const fx=(r.left+r.width/2-sr.left)/sr.width, fy=(r.top+r.height/2-sr.top)/sr.height;
    await pfxWalk(fx,fy,fx,fy,450); if(!pfxAuto)break;
    ok=!ok; apply();
    badge.style.transform='translateX(-50%) scale(.88)'; await pfxWait(120); badge.style.transform='translateX(-50%)';
    await pfxWait(1100);
  }
}
autoSeq();

On iOS, Safari and web views expose the CSS environment variables `env(safe-area-inset-top/right/bottom/left)` once `<meta name="viewport" content="viewport-fit=cover">` is set. Let content fill the screen edge-to-edge, but push anything that must be tapped or read inward by `padding: env(safe-area-inset-top) ... env(safe-area-inset-bottom)`.

Background color or imagery can safely extend all the way into the safe area — that's what avoids an awkward blank stripe around the notch — but interactive elements like buttons or text that land there either get hidden behind the notch or collide with the home-indicator gesture zone. The distinction between "fill it" and "avoid it" is the whole point.

Android exposes the same information per-device through `WindowInsets`. On both platforms the inset values vary by device and orientation, so hardcoding a fixed pixel margin only breaks on some models — you always read it from the system rather than guessing a number.

When to use

Apply it by default to any edge-to-edge mobile web view or PWA — easy to forget on a bottom-pinned CTA or a custom top header, exactly where tappable elements sit at the screen edge.