Mobile components

Actions

An action sheet offers a short list of things a person can do next, anchored to the bottom of the screen where a thumb can reach it.

When to use it

Use an action sheet when a single tap needs to branch into several choices. Share, which opens Copy link, Send a copy, Save to Files. A row’s overflow button, which opens Rename, Duplicate, Delete.

Use an alert when there is one decision with consequences, not a menu of options. Use a plain button when there is only one thing to do.

Keep the list short. Seven rows is the practical ceiling before it stops reading as a list of choices and starts reading as a screen.

Anatomy

PartValue
Width300px, capped at 100vw - 32px
Radius34px, a plain arc
SurfaceThe alert’s overlay glass: rgba(247,247,247,0.73) light, rgba(26,26,26,0.9) dark, blur 30px, saturate 180%, with the same hairline, rims and glow
Shadow0 8px 48px rgba(0,0,0,0.25) (--p-shadow-overlay)
Padding14px, plus max(16px, env(safe-area-inset-bottom)) below the panel
HeaderLeft aligned; the title’s first line 22px from the top and left edges, 24px above the first row
Title17px, weight 590, line height 22px, tracking -0.43px
Message17px, weight 400, same color as the title, 10px below it
Row272px wide at the default width, 48px tall, 24px radius, label 17px at weight 590
Row fillrgba(0,0,0,0.11) light, rgba(255,255,255,0.065) dark
Row gap8px, and 8px extra above Cancel
Entry0.28s slide from 16px below, cubic-bezier(0.22, 1, 0.36, 1)

The rows are capsules stacked with a 10px gap, not a divided list with hairlines between the items. That is the current iOS treatment, and it is the same 48px capsule the alert uses for its actions.

The header is left aligned while the rows are centered. The alert centers both, because an alert asks a question. An action sheet labels a list, so the header reads as a caption over it.

Try it

Escape closes it, Tab is trapped inside it, focus returns to the button that opened it, and a tap outside dismisses it. The action sheet is a .p-modal, so all of that comes from the shared manager rather than from anything sheet-specific.

Rows

ClassAppearanceUse it for
.p-action-sheet__actionQuiet fill, label colorEvery ordinary choice
.p-action-sheet__action--destructiveQuiet fill, red labelDeleting or removing something
.p-action-sheet__cancelSame fill, 8px of separation aboveDismissing without choosing

Cancel goes last and gets the extra gap. The separation matters more than it looks: it stops a thumb aiming for Cancel from landing on the destructive row directly above it.

Destructive rows keep the quiet fill and only turn the label red, the same rule the alert follows. Put a destructive row near the bottom of the list, above Cancel, not at the top where a reflex tap lands.

Label rows with verbs. Copy link, Send a copy, Remove sharing. A row labelled with a noun makes people guess what tapping it does.

Markup

<div class="p-modal-background">
  <div class="p-modal p-action-sheet" id="share-sheet"
       data-p-close-on-outside="true"
       role="dialog" aria-modal="true" aria-hidden="true"
       aria-labelledby="share-sheet-title">
    <div class="p-action-sheet__surface">
      <div class="p-action-sheet__header">
        <h2 class="p-action-sheet__title" id="share-sheet-title">Share “Q3 Report”</h2>
        <p class="p-action-sheet__message">
          Anyone with the link will be able to view this file.
        </p>
      </div>
      <div class="p-action-sheet__actions">
        <button class="p-action-sheet__action" type="button">Copy link</button>
        <button class="p-action-sheet__action p-action-sheet__action--destructive"
                type="button">Remove sharing</button>
        <button class="p-action-sheet__action p-action-sheet__cancel"
                type="button" data-p-cancel>Cancel</button>
      </div>
    </div>
  </div>
</div>

<button class="p-button p-button-touch p-button-primary"
        data-p-open-modal="#share-sheet">Share</button>

The header is optional. Drop .p-action-sheet__header when the rows explain themselves, which is most of the time.

role="dialog" here, not role="alertdialog". An action sheet offers choices; it does not interrupt with something that must be acknowledged.

Dark mode

The surface moves to the dark overlay glass, rgba(26,26,26,0.9), rows become a faint lift of rgba(255,255,255,0.065), and the destructive red lightens from #FF383C to #FF5257. The blur is unchanged.

Accessibility

Every row is a real <button>, so the list is navigable by keyboard and announced as a set of buttons. Rows that navigate rather than act should be <a href> instead.

The panel clears the home indicator through env(safe-area-inset-bottom), falling back to 16px where the environment variable resolves to zero.

Reduced motion removes the slide and leaves a 0.25s fade. Reduced transparency and browsers without backdrop-filter both get an opaque surface, because text over a translucent panel on arbitrary content cannot be guaranteed to meet 4.5:1.

The 34px corner is a plain arc, as iOS draws it, matching the alert. Increased contrast swaps the glass edge for a solid 1px keyline over an opaque surface.

Share sheets

A share sheet offers somewhere to send the thing on screen: a person, an app, or a destination like Files.

It is mostly a composition of parts that already exist. The panel is a bottom sheet and the action groups under the targets are inset lists. Both already carry the radii this sheet uses, 38px and 26px. What .p-activity-view adds is the target row.

Q3 Report

PDF Document, 2.4 MB

  • Copy
  • Save to Files
  • Print
<div class="p-activity-view">
<div class="p-activity-view__header">
  <div>
    <h2 class="p-activity-view__title">Q3 Report</h2>
    <p class="p-activity-view__subtitle">PDF Document, 2.4 MB</p>
  </div>
  <span></span>
  <button class="p-activity-view__close" type="button" aria-label="Close">
    <i class="ph-bold ph-x"></i>
  </button>
</div>
<div class="p-activity-view__targets">
  <button class="p-activity-view__target" type="button">
    <span class="p-activity-view__avatar">AL</span>
    <span class="p-activity-view__target-label">Ada</span>
    <span class="p-activity-view__target-detail">Messages</span>
  </button>
  <button class="p-activity-view__target" type="button">
    <span class="p-activity-view__app"><i class="ph-bold ph-envelope-simple"></i></span>
    <span class="p-activity-view__target-label">Mail</span>
  </button>
</div>
</div>
PartValue
Panel402px, 38px radius, the bottom sheet’s own values
Header title17px, weight 590, tracking -0.43px
Header subtitle13px, tracking -0.1px, secondary
Close button44px circle
Person target70px circle
App target64px rounded square at 16px radius
Target label12px, tracking +0.06px
Target detail11px, secondary
Action groups26px radius, the inset list’s own value

The target row scrolls sideways rather than wrapping, so the first few destinations stay in the same place however many there are. It bleeds to the panel edges, which is what signals there is more to the right.

People come before apps. iOS puts the person you last shared with first, because that is the most likely destination and the least work to reach.

Each target is a button with a label, so an icon-only destination still announces its name. The detail line under a person names the app the share would go through.

The legacy action sheet

The v1 .p-action-* classes are still here and still work, with their own actions.js. The v2 sheet is a separate component that runs on the modal manager instead.

v1v2
.p-action-big-container.p-modal.p-action-sheet
.p-action-container.p-action-sheet__surface
.p-action-title.p-action-sheet__title
.p-action-neutral.p-action-sheet__action
.p-action-destructive.p-action-sheet__action--destructive
.p-action-cancel.p-action-sheet__cancel
actions.jsmodals.js

Adding .p-action-sheet opts the modal out of every v1 .p-modal rule, so the two can sit on the same page without interfering.

Copied