One default action with its variants folded under the arrow beside it: a wide button that does the obvious thing, and a narrow one next to it that opens a menu of its near-relatives. Reach for it wherever a screen has an obvious action and two or three cousins of it — Save and Save as draft, Merge and Squash and merge and Rebase and merge, Deploy and Deploy to staging and Roll back, Export CSV and Export XLSX, Send and Schedule send, Invite and Copy invite link, Publish and Publish privately, Download and Download as PDF, Reply and Reply all, Run and Run with options, Approve and Approve with comment, Delete and Delete and archive, Add and Add from template, Print and Print selection — anywhere the default is right nine times out of ten and the tenth needs a different one rather than a different screen. Common asks it answers: "split button react", "shadcn split button", "shadcn button with dropdown", "button with arrow dropdown react", "button with chevron that opens a menu", "primary action with secondary actions react", "default action plus more options button", "save and save as draft button", "merge button with dropdown like github", "deploy button with options", "export button with format menu", "send later button", "shadcn button-group with dropdown-menu example", "combine button and dropdown menu shadcn", "split button without radix", "dropdown button no dependencies", "menu button pattern react", "aria-haspopup menu button react", "accessible dropdown button", "two buttons joined together css", "remove double border between adjacent buttons", "button group double border 2px", "rounded corners between two joined buttons", "focus ring clipped by next button", "focus ring cut off in button group", "focus-visible z-10 button group", "no visible divider between filled buttons", "split button divider not showing", "button group rtl broken corners", "split button keyboard navigation", "arrow down should open the dropdown", "menu opens but arrow keys do nothing", "focus first menu item when the menu opens", "menu button focus management", "escape should close the menu and give focus back", "focus lost after closing a dropdown", "focus jumps to body when menu closes", "tab starts from the top of the page after closing a menu", "tab out of an open dropdown menu", "dropdown trigger submits my form", "button inside form submits instead of opening menu", "type=button on dropdown trigger", "chevron button has no accessible name", "icon button announces just button", "two buttons both read as button screen reader", "aria-label for the dropdown arrow", "name the arrow of a split button", "disabled menu item still focusable", "aria-disabled vs disabled menu item", "arrow keys skip my disabled menu item", "menu arrow keys should wrap around", "primary action disabled but menu still usable", "分割ボタン react", "ボタンの隣に矢印でメニュー", "既定の操作とその他の操作", "保存と下書き保存のボタン", "ドロップダウン付きボタン shadcn", "矢印キーでメニューを開く", "Escape でフォーカスを戻す", "メニュー項目を無効にしても読み上げたい". Official shadcn/ui ships both halves and not the thing they make. Measured across its whole registry today — 63 items listed, 62 fetchable, `questionnaire` indexed and 404 on both style tracks, 223,287 bytes of `files[].content` concatenated — `SplitButton`, `split-button`, `splitButton`, `primaryAction` and `defaultAction` are every one of them a zero hit, and the route it documents is to compose `button-group` with `dropdown-menu` yourself. That is a real route, and `button-group` is real help: `[&>*:not(:first-child)]:rounded-l-none`, `[&>*:not(:first-child)]:border-l-0` and `[&>*]:focus-visible:relative [&>*]:focus-visible:z-10` between them collapse the doubled corner radius and the doubled border, and lift whichever child has focus so its ring is not painted over by the sibling that comes after it in the DOM. That last one is worth measuring rather than believing: in Chrome, two adjacent buttons with a 2px ring on the first draw the ring on three sides, and the same pair with `relative` and `z-10` on the focused half draws it on four — the ring extends 2px past the first button's border box, the sibling's box begins at exactly that edge, and `document.elementFromPoint` at that pixel answers with the sibling, whose background is painted later. Nothing is clipped and nothing errors; the ring just stops on the one side that says which half has focus. Two things are left over. The first is the divider, and it is left over precisely because the group did its job: of official's own `button` variants only `outline` carries a border, so `default`, `destructive` and `secondary` enter a group with none at all and `border-l-0` removes nothing — two filled halves then butt together as one unbroken block of colour, and where to press for the action and where to press for the menu stop being distinguishable. A filled split button needs a divider drawn in the foreground colour at low alpha, which is the opposite of what the group's rule does, and an outlined one needs one shared border rather than two. The second is every piece of the behaviour, and `dropdown-menu` does not write it either — it hands it to `radix-ui` as an npm dependency, 8,408 bytes of thin wrappers over `DropdownMenuPrimitive`. So grepping official for the aria proves nothing in either direction: `aria-haspopup`, `role="menu"` and `menuitem` are zero hits across all 62 items because Radix supplies them at runtime. What the search does establish is that no file in the registry contains a split button's keyboard — `ArrowDown` occurs once in the whole registry, in `message-scroller`, and the single `aria-expanded` is the `has-aria-expanded:bg-muted/50` styling hook on `TableRow`. It is one thing to look at and two things to press, and everything here is a consequence of that sentence. Focus stops twice, so the arrow needs a name of its own, and it has to carry the default action: it is a chevron with no text, so left to itself it announces the word "button" one position away from another button that also announces "button", and a toolbar with three split buttons becomes three identical "More options". Here the name is built from the label — "More Save options" — and `labels.more` rewrites it in any language. Down and Up on the default action open the menu too, and they land inside it: opening while leaving focus on the default action is the version that looks right and is useless, because the keys that drive a menu are handled in the menu and the next Down does nothing, while handing focus to the arrow first is the other near-miss, since the menu is already open and the person's next Down walks from the arrow into the menu and skips the first item. Escape, Tab and choosing an item all put focus back on the half that opened the menu, which is not always the arrow; the element standing where they are is about to be removed, and a browser does not move focus when that happens — it drops to `<body>`, so the next Tab starts again from the top of the page. Tab is not prevented, because Tab means leave, but focus is moved onto the opener before the menu goes, so the browser computes the next stop from an element still in the document. A press outside is the one exception to all of that: the person has already said where they want to be, and pulling focus back would undo their click. The arrow is `type="button"` and no prop can change it, because a split button lives in a form more often than not and `<button>` in a form defaults to submit — an arrow that inherited the default would post the form the moment somebody went looking for the second option; `type` reaches the default action, which is the half that may legitimately want to submit. An unavailable item gets `aria-disabled` and never the `disabled` attribute: `disabled` takes a button out of the focus order, so arrowing a menu of four whose third is unavailable walks 1, 2, 4, and the person learns not that the item is unavailable but that it does not exist. The arrow keys wrap rather than clamp, which is the menu convention and the opposite of the listboxes elsewhere in this registry — three items clamped make the last one a key that does nothing, which reads as the keyboard having broken. And the flat corners are logical (`rounded-e-none`, `rounded-s-none`) rather than left and right, so the unit mirrors itself on an Arabic or Hebrew page instead of coming apart. What you get: `SplitButton`, the whole unit, with `label` required because it is both the default action's text and the source of the arrow's accessible name; `actions`, the variants, each `{ label, onSelect, disabled?, icon?, id? }`, where an empty list renders no arrow at all because an arrow that opens an empty panel is a dead end; `variant` of `primary`, `outline` or `destructive`, each with the divider its own fill needs; `align` of `end` or `start` for which edge the menu lines up with; and the three ways to make something unavailable kept separate — `disabled` for both halves, `actionDisabled` for the default action alone with the menu still open for business, which is the combination the whole pattern exists for (a form that cannot be saved yet but can be saved as a draft, a pull request that cannot be merged but can still be closed), and `menuDisabled` for the arrow alone. `nextMenuIndex` is the wrap rule as a plain function, so the one place this differs from a listbox can be asserted on its own, and `defaultSplitButtonLabels` is the arrow's name as data. `onOpenChange` is reported from an effect rather than from inside the state setter, so a caller is told once per change instead of twice under strict mode. Per-part `className` for the wrapper, each half, the panel and the items. Nothing is portalled and nothing measures the viewport: the panel is absolutely positioned inside the wrapper, which is what a two-to-five item menu under a button needs — a menu that has to flip above the trigger near the bottom of a scroll container wants a positioning library, and that is a different component. One file, one lucide icon, and no `radix-ui`. Distinct from the neighbours: `confirm-button` is one action that asks twice, `loading-button` is one action that is busy, and `share-button`, `print-button` and `csv-export-button` are each a single action — none of them has a second half. `command-palette` is a searchable dialog over everything, reached by a shortcut rather than by a button. `multi-select`, `country-select`, `language-select` and `currency-select` are listboxes: they pick a value, they are named by a field label, and they clamp at the ends. This is the first `role="menu"` in the registry — `menuitem` appears in none of the other 110 components — and a menu of actions is the one popup where the items are buttons that do something rather than options that select something. `bulk-action-bar` acts on a selection, and `keyboard-shortcuts` is the sheet that lists what the keys do.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/split-button.json"