UIDex

Action Sheet

ورقة الإجراءات

UIAlertController .actionSheet

also calledoptions sheet، confirmation dialog، UIActionSheet، iOS options menu، bottom action menu، أكشن شيت، قائمة الإجراءات، ورقة الخيارات، قائمة الأوامر السفلية، شيت الخيارات، ورقة التأكيد

An action sheet is a short list of commands about one object, and it rises from the bottom edge of the screen to block whatever is under it until the user picks one or cancels. It came from iOS, where it is a UIAlertController created with preferredStyle .actionSheet. It is not a bottom sheet: a bottom sheet takes arbitrary content such as a form, a map, or a long list, its height belongs to the user who drags it between detents, and it can sit on screen with no scrim at all, while an action sheet is only as tall as its commands make it, cannot be dragged, and has no non-blocking form. It is not an alert: an alert stands in the middle of the screen and asks one question with two answers, where an action sheet lists what can be done to the thing it was opened from. It is not a menu either: a menu hangs off the control that opened it and its rows can carry an icon, a checkmark, or a submenu, while an action sheet is pinned to the bottom edge whatever opened it and every row is a plain centred label. On the phone it is modal and ends with a cancel row set apart from the rest; on iPad the same call becomes a popover that draws no cancel row. The moment a text field, an image, or a seventh command goes in, the right name is bottom sheet.

If you called it…

"the list that slides up when you tap the three dots""the popup with the red delete option""the sheet where cancel sits on its own""the ios popup that asks share or save or delete""the options thing that closes if you tap outside it"

Live specimen

Interact with the demo. Every part is real and numbered.

٩:٤١
الصورة ٤ من ١٢

آخر أمر نُفّذ: لا شيء بعد

RTLالصفوف متمركزة بعرض الورقة فلا ينعكس فيها شيء، واللمس بالخارج إلغاء

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a mobile action sheet. Keep it modal at all times with a scrim that swallows taps and takes everything under it out of the accessibility tree. Present it on iOS with UIAlertController and preferredStyle .actionSheet, or with SwiftUI .confirmationDialog plus titleVisibility: .visible; on Android with a ModalBottomSheet holding a short list of ListItem rows; on the web with a <dialog> opened by showModal() and nothing more than an aria-labelledby pointing at the title, since the element already brings the dialog role, the scrim, Escape, and an inert page, so role="dialog" and aria-modal="true" are redundant on it. Keep the command count between two and six, put the destructive command at the top of the sheet far from cancel, and order the rest so the most used sits nearest the bottom edge. Write the style rather than the colour: .destructive in UIKit, role: .destructive in SwiftUI, and put the word delete in the label itself, because red never reaches a screen reader. Separate cancel with real empty space rather than a hairline, allow exactly one action with style .cancel, and run that same handler on a scrim tap, on the Android back button, and on Escape on the web. Before presenting on iPad, set popoverPresentationController.sourceItem, or sourceView with sourceRect, or barButtonItem, or the app dies with NSGenericException; and draw no cancel row in the popover form. Use logical properties throughout: padding-inline, margin-inline, inset-inline, and text-align: center or start, never padding-left, margin-right, or text-align: left. Let a row grow with its text by using min-block-size instead of height, and keep white-space: nowrap, text-overflow: ellipsis, and maxLines = 1 off the command names so a longer Arabic label wraps onto a second line. Add bottom padding of env(safe-area-inset-bottom) or navigationBarsPadding(), guarantee a 44pt touch target per row, return focus to the control that opened the sheet, and honour prefers-reduced-motion on the rise animation.

How it behaves right-to-left

This section is ours alone.

The rows have nothing to mirror

never mirrors

An action row takes the full inline size of the sheet with its label centred, so it has no start and no end to swap, and UIAlertAction gives you no alignment knob anyway. The risk is in hand-written versions: text-align: left inside a row leaves the Arabic label hanging on the left after the rest of the screen has flipped. Write text-align: center to match the iOS form, or text-align: start if you align the names to the beginning of the line the way Material list rows do.

The leading icon

mirrors

Where a row does carry an icon before its name, as in Material list rows or a hand-written web sheet, that icon belongs at the logical start and lands on the right of the row in Arabic. Build the row with flex, gap, and padding-inline-start, never padding-left or margin-right, and remember that on Android nothing moves until android:supportsRtl="true" is in the manifest. Only a directional glyph also flips its own drawing: take those from Icons.AutoMirrored in Compose or from imageFlippedForRightToLeftLayoutDirection() in UIKit, and leave the trash can and the share glyph alone.

The destructive row keeps its colour and its slot

never mirrors

Nothing about the destructive row mirrors: the red is a state rather than a direction, and its slot at the top of the sheet is the same in both languages. Row order runs down the block axis, which direction never touches, so flex-direction: column-reverse has no business anywhere near this list. The recurring mistake is a blanket [dir="rtl"] svg { transform: scaleX(-1) }, which flips the trash glyph with everything else and leaves it drawn back to front; transform has no logical counterpart, so scope that rule to the icons that really are directional.

The popover anchor on iPad

mirrors

The tablet form grows out of whatever opened it, so it follows that control's logical side: a button at the end of the navigation bar opens the popover on the left in Arabic after opening on the right in English. The trap is sourceRect, a physical CGRect in the source view's own coordinate space, so CGRect(x: view.bounds.width - 44, …) stays pinned to the right whatever the interface direction is. Pass sourceItem or barButtonItem and let UIKit compute the frame, or read view.effectiveUserInterfaceLayoutDirection and mirror x yourself. permittedArrowDirections is physical in the same way, since .left and .right have no logical twin, so swap them with the direction or settle for .any.

The Arabic name is a different length

never mirrors

Arabic does not track English length: «حذف» is half of Delete in characters, while «إضافة إلى الشاشة الرئيسية» runs a third longer than Add to Home Screen. The native sheet wraps a long title onto a second line and grows the row, and hand-written versions break that with a fixed row height, with white-space: nowrap plus text-overflow: ellipsis, or with maxLines = 1 in Compose. Swap height for min-block-size and leave the label wrapping, because clipping the end of an Arabic command drops its object, the part that says what the command acts on. Arabic also needs a taller line box than Latin at the same size, so a line-height tuned on English clips the descenders of ج and ح and the dots under ي.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

UIKitUIAlertController(title:message:preferredStyle: .actionSheet)The original the element is named after; the same class gives you the centred alert by switching preferredStyle to .alert.
UIKitUIAlertAction(title:style: .destructive)The style is what produces the red, and it is never spoken, so the warning goes in the title; its partner .cancel may appear only once per controller.
UIKitpopoverPresentationController.sourceItemThe iOS 16 name for the anchor, replacing sourceView with sourceRect or barButtonItem; leaving it out crashes the app on iPad.
SwiftUI.confirmationDialog(_:isPresented:titleVisibility:)The declarative replacement from iOS 15; its buttons carry role: .destructive and role: .cancel, and the system adds a cancel when you write none.
Jetpack ComposeModalBottomSheet { ListItem(…) }Android has no type called an action sheet; the equivalent is a modal bottom sheet holding a short list, and BottomSheetDialogFragment is the Views-era version.
React NativeActionSheetIOS.showActionSheetWithOptionsdestructiveButtonIndex and cancelButtonIndex are positions inside the options array, and anchor is the iPad anchor; the module runs on iOS only, so Android needs its own path.
HTML<dialog>.showModal() + ::backdropThe closest web approximation: the dialog role, the top layer, the backdrop, Escape, and an inert page come free, so aria-labelledby is the only thing left to add.

See also

Updated · 2026-08-19