Action Sheet

액션 시트

Tap a single trigger and a full-width card of related actions plus a Cancel button slides up from the bottom.

Also known as: Bottom action sheetiOS 액션 시트
···
html
<div class="stage"><div class="phone"><div class="notch"></div><div class="screen" id="screen">
<div class="ascreen"><div class="topbar2">Photo</div><button class="trigger" id="trigger">Share &#8599;</button></div>
<div class="scrim" id="scrim"></div>
<div class="asheet" id="asheet">
  <div class="asgroup"><div class="asitem">Save Image</div><div class="asitem">Copy</div><div class="asitem del">Delete</div></div>
  <div class="ascancel" id="ascancel">Cancel</div>
</div></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}

.ascreen{position:absolute;inset:0;padding:10% 8%}
.topbar2{font-weight:800;font-size:clamp(8px,3vmin,12px);margin-bottom:8%}
.trigger{padding:3% 6%;border:1px solid var(--line);border-radius:999px;background:var(--surface);
  font-size:clamp(6.5px,2.4vmin,9px);font-weight:700;color:var(--accent)}
.scrim{position:absolute;inset:0;background:#000;opacity:0;transition:opacity .25s;pointer-events:none;z-index:10}
.scrim.show{opacity:.35;pointer-events:auto}
.asheet{position:absolute;left:3%;right:3%;bottom:3%;display:grid;gap:5%;transform:translateY(140%);
  transition:transform .32s cubic-bezier(.2,.8,.2,1);z-index:20}
.asheet.show{transform:translateY(0)}
.asgroup{background:var(--surface);border-radius:12px;overflow:hidden;box-shadow:0 4px 14px rgba(0,0,0,.14)}
.asitem{padding:8% 0;text-align:center;font-size:clamp(6.5px,2.4vmin,9px);font-weight:700;border-bottom:1px solid var(--line)}
.asitem:last-child{border-bottom:none}
.asitem.del{color:#e5534b}
.ascancel{background:var(--surface);border-radius:12px;padding:8% 0;text-align:center;font-weight:800;
  font-size:clamp(6.5px,2.4vmin,9px);box-shadow:0 4px 14px rgba(0,0,0,.14)}
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 trigger=document.getElementById('trigger'), scrim=document.getElementById('scrim'), asheet=document.getElementById('asheet'), cancel=document.getElementById('ascancel');
function open(){ scrim.classList.add('show'); asheet.classList.add('show'); }
function close(){ scrim.classList.remove('show'); asheet.classList.remove('show'); }
trigger.addEventListener('pointerdown',()=>{ pfxAuto=false; pfxHide(); open(); });
scrim.addEventListener('pointerdown',close);
cancel.addEventListener('pointerdown',()=>{ pfxAuto=false; pfxHide(); close(); });
function fracOf(el){ const r=el.getBoundingClientRect(), sr=screenEl.getBoundingClientRect();
  return {x:(r.left+r.width/2-sr.left)/sr.width,y:(r.top+r.height/2-sr.top)/sr.height}; }
async function autoSeq(){
  while(pfxAuto){
    close();
    let f=fracOf(trigger);
    await pfxWalk(f.x,f.y,f.x,f.y,400); if(!pfxAuto)break;
    open();
    await pfxWait(1300); if(!pfxAuto)break;
    f=fracOf(cancel);
    await pfxWalk(0.5,0.15,f.x,f.y,350); if(!pfxAuto)break;
    close();
    await pfxWait(950);
  }
}
autoSeq();

The wiring resembles a bottom sheet — an overlay plus a panel rising from below — but it's simpler because there are only two states, open and closed, with no drag-to-resize detents. Tapping the scrim or picking an option closes it immediately.

iOS convention groups reversible and destructive actions (delete, in red) in the same card, but keeps "Cancel" visually separated into its own card below — a gap that lowers the odds of mis-tapping it for something else. Android Material generally skips that spacing rule and folds everything into one bottom-sheet-style menu.

Past around five options it starts needing its own scroll, and at that point a full-screen menu or a dedicated screen usually serves better than an action sheet.

When to use

Use it for 2–5 clear choices tied to one trigger — share targets, sort order. If the user needs to type a value, use a dialog; if it's a real destination, use navigation.