Split Button
Use CSplitButton when one action is clearly dominant and a short Menu holds closely related alternatives. The primary and Menu trigger are separate native Buttons with separate names and Tab stops.
Use CButton for one action, CButtonGroup for visible peers, and CMenu when there is no dominant action. Use CSelect or CCombobox when the reader is choosing a value rather than running an action.
Related guidance: Button, Button Group, Menu, the WAI-ARIA APG Menu Button pattern, and the native Button element.
Split Button at a glance
Save the specimen directly or open related save actions. The Menu does not repeat the dominant Save action.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonAtAGlance(Component):
template = """
<section
class="split-button-glance"
x-data="{saved:0,last:'No action yet'}"
>
<p class="split-button-glance__eyebrow">Field journal</p>
<h2>Alpine gentian specimen</h2>
<p>Keep the primary save action visible and related work nearby.</p>
<c-CSplitButton
label="Save specimen actions"
menu_label="More save specimen actions"
c-primary_attrs="{'@click':'saved += 1; last = `Saved specimen ${saved}`'}"
$c-props="{onAction:(value)=>last=value}"
>
<c-fill name="default">Save specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="Save a copy">Save a copy</c-CMenuItem>
<c-CMenuItem value="Export record">Export record</c-CMenuItem>
<c-CMenuItem value="Archive specimen" intent="danger">
Archive specimen
</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<output x-text="last">No action yet</output>
</section>
"""
css = """
:where(.split-button-glance) {
display: grid;
gap: 0.75rem;
justify-items: start;
min-block-size: 18rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-glance h2, .split-button-glance p) {
margin: 0;
}
:where(.split-button-glance__eyebrow) {
color: light-dark(#3f6b42, #9ed5a1);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
"""
preview = SplitButtonAtAGlance()
preview # noqa: B018
Compose the two actions
Supply a nonempty group label, a specific menu_label, visible primary content, and at least one existing Menu declaration.
<c-CSplitButton
label="Save specimen actions"
menu_label="More save specimen actions"
>
<c-fill name="default">Save specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="save-copy">Save a copy</c-CMenuItem>
<c-CMenuItem value="export">Export record</c-CMenuItem>
</c-fill>
</c-CSplitButton>
Direct Python composition uses the same slots and public Menu declarations:
from citry_ui import CMenuItem, CSplitButton
save_actions = CSplitButton(
label="Save specimen actions",
menu_label="More save specimen actions",
slots={
"default": "Save specimen",
"menu": (
CMenuItem(
value="save-copy",
slots={"default": "Save a copy"},
),
CMenuItem(
value="export",
slots={"default": "Export record"},
),
),
},
)
The next example renders both composition forms.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CMenuItem, CSplitButton
citry.register_library(citry_ui)
class BasicSplitButtonActions(Component):
class Kwargs:
pass
class Slots:
pass
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, Any]:
return {
"python_split_button": CSplitButton(
label="Publish specimen actions",
menu_label="More publish specimen actions",
variant="outline",
slots={
"default": "Publish specimen",
"menu": (
CMenuItem(
value="preview",
slots={"default": "Preview publication"},
),
CMenuItem(
value="schedule",
slots={"default": "Schedule publication"},
),
),
},
)
}
template = """
<section class="split-button-basic">
<article>
<p>Template composition</p>
<c-CSplitButton
label="Save specimen actions"
menu_label="More save specimen actions"
>
<c-fill name="default">Save specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="save-copy">Save a copy</c-CMenuItem>
<c-CMenuItem value="export">Export record</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</article>
<article>
<p>Python composition</p>
{{ python_split_button }}
</article>
</section>
"""
css = """
:where(.split-button-basic) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1rem;
min-block-size: 17rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-basic article) {
display: grid;
gap: 0.75rem;
align-content: start;
padding: 1rem;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
border-radius: 0.75rem;
}
:where(.split-button-basic p) {
margin: 0;
font-weight: 700;
}
"""
preview = BasicSplitButtonActions()
preview # noqa: B018
Submit and reset native Forms
Only the primary Button participates in a Form. Set type="submit" or type="reset", then pass name, value, form, and submitter overrides through primary_attrs. The Menu Button and Menu items never submit.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonForms(Component):
template = """
<section
class="split-button-forms"
x-data="{result:'No Form action yet',owner:'accession-form'}"
>
<form
id="accession-form"
x-ref="accession"
@submit.prevent="
result = `Submitted ${new FormData($event.target, $event.submitter).get('action')}`
"
@reset="setTimeout(() => result = 'Reset accession', 0)"
>
<label>
Accession name
<input name="specimen" value="Alpine gentian" required />
</label>
<div class="split-button-forms__actions">
<c-CSplitButton
label="Commit accession actions"
menu_label="More commit accession actions"
type="submit"
c-primary_attrs="{'name':'action','value':'commit'}"
>
<c-fill name="default">Commit accession</c-fill>
<c-fill name="menu">
<c-CMenuItem value="export-draft">
Export draft
</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<c-CSplitButton
label="Reset accession actions"
menu_label="More reset accession actions"
type="reset"
variant="outline"
>
<c-fill name="default">Reset accession</c-fill>
<c-fill name="menu">
<c-CMenuItem value="restore-snapshot">
Restore saved snapshot
</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</div>
</form>
<form
id="secondary-accession-form"
@submit.prevent="
result = `Submitted to secondary Form with ${$event.submitter.value}`
"
>
<label>
Secondary accession
<input name="secondary-specimen" value="Sea thrift" required />
</label>
</form>
<label>
External primary Form owner
<select x-model="owner">
<option value="accession-form">Main accession Form</option>
<option value="secondary-accession-form">Secondary accession Form</option>
</select>
</label>
<c-CSplitButton
id="external-commit-actions"
label="External commit actions"
menu_label="More external commit actions"
type="submit"
size="sm"
c-primary_attrs="{
':form':'owner',
'name':'action',
'value':'external-commit'
}"
>
<c-fill name="default">Commit from outside</c-fill>
<c-fill name="menu">
<c-CMenuItem value="download">Download draft</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<button
type="button"
@click="
document.getElementById(owner).requestSubmit(
document.getElementById('external-commit-actions-primary')
)
"
>
Request native submit
</button>
<output x-text="result">No Form action yet</output>
</section>
"""
css = """
:where(.split-button-forms) {
display: grid;
gap: 1rem;
max-inline-size: 38rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-forms form, .split-button-forms label) {
display: grid;
gap: 0.75rem;
}
:where(.split-button-forms__actions) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
"""
preview = SplitButtonForms()
preview # noqa: B018
An open Menu closes internally before an uncontrolled primary default action. Its public onOpenChange action notice runs afterward, so the callback cannot cancel or duplicate the accepted native submit or reset. A valid form.requestSubmit(primary) follows the same rule. Native constraint validation can prevent submission before a submit event; that path leaves the Menu unchanged.
Without JavaScript, an enabled primary submit or reset remains a useful native Button, while server disabled or loading output uses CButton's native-safe fallback. The Menu Button cannot toggle before initialization. A closed Menu stays noninteractive in server flow, and an initially open Menu remains readable; neither path can submit the Form.
Control Menu visibility
Pass a Boolean client open to own Menu visibility. Omit it or pass null to release control from the latest committed state. onOpenChange reports Menu gestures and the primary action close request. Forced disabled or ancestor closes cannot be refused.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ControlledSplitButtonMenu(Component):
template = """
<section
class="split-button-controlled"
x-data="{
open:false,
controlled:true,
accept:true,
lastReason:'none'
}"
>
<c-CSplitButton
label="Publication actions"
menu_label="More publication actions"
$c-props="{
open: controlled ? open : null,
onOpenChange: (nextOpen, detail) => {
lastReason = detail.reason;
if (controlled && accept) open = nextOpen;
},
}"
>
<c-fill name="default">Publish specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="preview">Preview publication</c-CMenuItem>
<c-CMenuItem value="schedule">Schedule publication</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<label>
<input type="checkbox" x-model="accept" />
Accept Menu requests
</label>
<div role="group" aria-label="Menu owner controls">
<button type="button" @click="controlled=true;open=true">
Show
</button>
<button type="button" @click="controlled=true;open=false">
Hide
</button>
<button type="button" @click="controlled=false">
Release control
</button>
</div>
<output>
Ownership:
<span x-text="controlled ? 'controlled' : 'released'">
controlled
</span>
· Last reason:
<span x-text="lastReason">none</span>
</output>
</section>
"""
css = """
:where(.split-button-controlled) {
display: grid;
gap: 0.85rem;
justify-items: start;
min-block-size: 17rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-controlled [role="group"]) {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
"""
preview = ControlledSplitButtonMenu()
preview # noqa: B018
Primary native events remain distinct from Menu callbacks. Pass @click through primary_attrs, and use onAction for valued Menu commands and choices.
Choose presentation and placement
variant, intent, and size style both Buttons. block fills the available inline size while the Menu Button keeps its target width. placement and match_width use the full joined group as their anchor, not the narrow Menu Button.
Customize example
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonVariantsAndSizes(Component):
template = """
<section
class="split-button-variants"
x-data="{
variant:'solid',
intent:'primary',
size:'md',
block:false,
placement:'bottom-end',
match_width:false
}"
@citry-ui-preview-controls.window="Object.assign($data, $event.detail)"
>
<div class="split-button-variants__subject">
<c-CSplitButton
label="Live collection actions"
menu_label="More live collection actions"
$c-props="{
variant,
intent,
size,
block,
placement,
matchWidth: match_width
}"
>
<c-fill name="default">Collect specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="photograph">Photograph first</c-CMenuItem>
<c-CMenuItem value="label">Print field label</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</div>
<div class="split-button-variants__matrix">
<c-CSplitButton
label="Approve actions"
menu_label="More approve actions"
variant="outline"
intent="success"
size="sm"
>
<c-fill name="default">Approve</c-fill>
<c-fill name="menu">
<c-CMenuItem value="review">Return to review</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<c-CSplitButton
label="Warning actions"
menu_label="More warning actions"
variant="ghost"
intent="warn"
size="lg"
>
<c-fill name="default">Flag specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="quarantine" intent="danger">
Quarantine specimen
</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</div>
</section>
"""
css = """
:where(.split-button-variants) {
display: grid;
gap: 1.5rem;
min-block-size: 20rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-variants__subject) {
inline-size: min(100%, 30rem);
}
:where(.split-button-variants__matrix) {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: start;
}
"""
preview_controls = (
{
"name": "variant",
"label": "Variant",
"type": "select",
"default": "solid",
"options": (
("solid", "Solid"),
("outline", "Outline"),
("ghost", "Ghost"),
),
},
{
"name": "intent",
"label": "Intent",
"type": "select",
"default": "primary",
"options": (
("primary", "Primary"),
("neutral", "Neutral"),
("success", "Success"),
("warn", "Warn"),
("danger", "Danger"),
),
},
{
"name": "size",
"label": "Size",
"type": "select",
"default": "md",
"options": (("sm", "Small"), ("md", "Medium"), ("lg", "Large")),
},
{
"name": "placement",
"label": "Placement",
"type": "select",
"default": "bottom-end",
"options": (
("bottom-start", "Bottom start"),
("bottom-end", "Bottom end"),
("top-start", "Top start"),
("top-end", "Top end"),
),
},
{"name": "block", "label": "Full width", "type": "checkbox", "default": False},
{
"name": "match_width",
"label": "Match group width",
"type": "checkbox",
"default": False,
},
)
preview = SplitButtonVariantsAndSizes()
preview # noqa: B018
Keep alternatives available while loading
loading affects only the primary action. It remains focusable, exposes busy state, and blocks new activation while an otherwise enabled Menu remains available. Use common disabled when both halves must be unavailable, or the per-half inputs when only one action is unavailable.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonDisabledAndLoading(Component):
template = """
<section
class="split-button-disabled-demo"
x-data="{
disabled: false,
primaryDisabled: false,
menuDisabled: false,
loading: false,
saves: 0,
last: 'Ready',
}"
>
<h2>Large specimen image</h2>
<div class="split-button-disabled-demo__controls" aria-label="Split Button state">
<label><input type="checkbox" x-model="disabled" /> Disable both</label>
<label><input type="checkbox" x-model="primaryDisabled" /> Disable primary</label>
<label><input type="checkbox" x-model="menuDisabled" /> Disable Menu</label>
<label><input type="checkbox" x-model="loading" /> Save pending</label>
</div>
<c-CSplitButton
label="Specimen image actions"
menu_label="More specimen image actions"
c-primary_attrs="{'@click':'saves += 1; last = `Saved ${saves} times`'}"
$c-props="{
disabled,
primaryDisabled,
menuDisabled,
loading,
onAction: (value) => last = value,
}"
>
<c-fill name="default">Save image</c-fill>
<c-fill name="menu">
<c-CMenuItem value="Export TIFF">Export TIFF</c-CMenuItem>
<c-CMenuItem value="Export JPEG">Export JPEG</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<output aria-live="polite" x-text="last">Ready</output>
<fieldset disabled>
<legend>Disabled fieldset lifecycle</legend>
<c-CSplitButton
label="Fieldset-owned image actions"
menu_label="More fieldset-owned image actions"
>
<c-fill name="default">Save fieldset image</c-fill>
<c-fill name="menu">
<c-CMenuItem value="export-fieldset">Export fieldset image</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</fieldset>
</section>
"""
css = """
:where(.split-button-disabled-demo) {
display: grid;
gap: 1rem;
justify-items: start;
inline-size: min(100%, 34rem);
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-disabled-demo h2) { margin: 0; }
:where(.split-button-disabled-demo__controls) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1rem;
}
:where(.split-button-disabled-demo__controls label) {
display: inline-flex;
gap: 0.4rem;
align-items: center;
}
:where(.split-button-disabled-demo fieldset) {
inline-size: 100%;
padding: 1rem;
border: 1px solid GrayText;
border-radius: 0.75rem;
}
@media (forced-colors: active) {
:where(.split-button-disabled-demo fieldset) { border-color: CanvasText; }
}
"""
preview = SplitButtonDisabledAndLoading()
preview # noqa: B018
Native disabled fieldset ancestry remains authoritative. When disabling the Menu hides focused Menu content, focus moves to an enabled primary Button or a safe modal/document fallback.
Reuse the complete Menu collection
The menu slot accepts the current CMenuItem, CMenuCheckboxItem, CMenuRadioGroup, CMenuRadioItem, CMenuGroup, CMenuSeparator, and CMenuSubmenu declarations. Their values, callbacks, paths, parts, keyboard rules, and content limits remain the CMenu contract.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonMenuComposition(Component):
template = """
<section
class="split-button-menu-composition"
x-data="{publicRecord:true, format:'tiff', last:'No Menu action yet'}"
dir="rtl"
>
<h2>Specimen publication</h2>
<c-CSplitButton
label="Specimen publication actions"
menu_label="More specimen publication actions"
c-close_on_select="False"
$c-props="{onAction:(value, detail)=>last=`${detail.path.join(' / ') || 'root'}: ${value}`}"
>
<c-fill name="default">Publish specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="copy-citation">Copy citation</c-CMenuItem>
<c-CMenuItem href="#specimen-public-record">Open public record</c-CMenuItem>
<c-CMenuCheckboxItem
value="public-record"
$c-props="{
checked: publicRecord,
onCheckedChange: (next) => publicRecord = next,
}"
>
Publicly visible
</c-CMenuCheckboxItem>
<c-CMenuRadioGroup
value="tiff"
$c-props="{
value: format,
onValueChange: (next) => format = next,
}"
>
<c-fill name="label">Export format</c-fill>
<c-fill name="default">
<c-CMenuRadioItem value="tiff">TIFF</c-CMenuRadioItem>
<c-CMenuRadioItem value="jpeg">JPEG</c-CMenuRadioItem>
</c-fill>
</c-CMenuRadioGroup>
<c-CMenuSeparator />
<c-CMenuGroup>
<c-fill name="label">Archive destination</c-fill>
<c-fill name="default">
<c-CMenuSubmenu value="regional-archive">
<c-fill name="label">Regional archive</c-fill>
<c-fill name="default">
<c-CMenuItem value="alpine">Alpine collection</c-CMenuItem>
<c-CMenuItem value="coastal">Coastal collection</c-CMenuItem>
</c-fill>
</c-CMenuSubmenu>
</c-fill>
</c-CMenuGroup>
<c-CMenuItem value="withdraw" intent="danger">Withdraw record</c-CMenuItem>
</c-fill>
</c-CSplitButton>
<output aria-live="polite" x-text="last">No Menu action yet</output>
<p id="specimen-public-record">The linked public record remains native navigation.</p>
</section>
"""
css = """
:where(.split-button-menu-composition) {
display: grid;
gap: 0.875rem;
justify-items: start;
min-block-size: 23rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-menu-composition h2, .split-button-menu-composition p) { margin: 0; }
:where(.split-button-menu-composition output) {
max-inline-size: 100%;
overflow-wrap: anywhere;
}
"""
preview = SplitButtonMenuComposition()
preview # noqa: B018
Do not repeat the primary action in the Menu. Give the Menu Button a full secondary name such as “More save specimen actions”, not only “More”.
Use the two-stop keyboard model
Tab visits the primary Button and then the Menu Button in DOM order in both LTR and RTL. Enter and Space activate the focused native Button. Arrow Down or Arrow Up on the Menu Button opens and focuses the first or last item. The primary does not gain Menu arrow behavior.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonFocusAndKeyboard(Component):
template = """
<section
class="split-button-keyboard-demo"
x-data="{trace:[], loading:false, primaryDisabled:false, menuDisabled:false}"
>
<h2>Keyboard specimen workflow</h2>
<p>
Tab reaches the primary and Menu Button in DOM order. Enter or Space activates the
focused Button. In the Menu, use arrows, Home, End, typeahead, and Escape.
</p>
<div class="split-button-keyboard-demo__controls">
<label><input type="checkbox" x-model="loading" /> Primary loading</label>
<label><input type="checkbox" x-model="primaryDisabled" /> Primary disabled</label>
<label><input type="checkbox" x-model="menuDisabled" /> Menu disabled</label>
</div>
<div class="split-button-keyboard-demo__row" dir="ltr">
<span>LTR</span>
<c-CSplitButton
label="Keyboard save actions"
menu_label="More keyboard save actions"
c-primary_attrs="{'@focus':'trace.push(`LTR primary`)'}"
c-trigger_attrs="{'@focus':'trace.push(`LTR menu`)'}"
$c-props="{loading, primaryDisabled, menuDisabled}"
>
<c-fill name="default">Save field note</c-fill>
<c-fill name="menu">
<c-CMenuItem value="duplicate">Duplicate note</c-CMenuItem>
<c-CMenuItem value="export">Export note</c-CMenuItem>
<c-CMenuItem value="archive">Archive note</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</div>
<div class="split-button-keyboard-demo__row" dir="rtl">
<span>RTL</span>
<c-CSplitButton
label="إجراءات حفظ العينة"
menu_label="المزيد من إجراءات حفظ العينة"
c-primary_attrs="{'@focus':'trace.push(`RTL primary`)'}"
c-trigger_attrs="{'@focus':'trace.push(`RTL menu`)'}"
>
<c-fill name="default">حفظ ملاحظة العينة</c-fill>
<c-fill name="menu">
<c-CMenuItem value="duplicate-rtl">نسخ الملاحظة</c-CMenuItem>
<c-CMenuItem value="export-rtl">تصدير الملاحظة</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</div>
<output aria-live="polite" x-text="trace.length ? trace.join(' → ') : 'Focus trace is empty'">
Focus trace is empty
</output>
<button type="button" @click="trace=[]">Clear focus trace</button>
</section>
"""
css = """
:where(.split-button-keyboard-demo) {
display: grid;
gap: 1rem;
inline-size: min(100%, 32rem);
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-keyboard-demo h2, .split-button-keyboard-demo p) { margin: 0; }
:where(.split-button-keyboard-demo__controls) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
:where(.split-button-keyboard-demo__row) {
display: grid;
gap: 0.5rem;
justify-items: start;
inline-size: min(100%, 20rem);
}
:where(.split-button-keyboard-demo output) { overflow-wrap: anywhere; }
"""
preview = SplitButtonFocusAndKeyboard()
preview # noqa: B018
Once open, the collection uses CMenu arrow navigation, Home, End, typeahead, submenus, Escape, and Tab behavior without trapping focus.
Compose with clipping and Dialogs
The Menu uses the native top layer and the shared anchored-layer coordinator. It escapes ordinary overflow while placement and width still follow the full SplitButton root. A sibling Dialog opened by either action owns modal focus.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonLayersAndDialog(Component):
template = """
<section
class="split-button-layer-demo"
x-data="{dialogOpen:false,last:'No layer action yet'}"
@click="if ($event.target.closest('[data-open-provenance]')) dialogOpen=true"
x-init="$nextTick(() => {
const host = $refs.shadowHost;
const fixture = $refs.shadowFixture;
if (!host.shadowRoot && fixture) host.attachShadow({mode:'open'}).append(fixture);
})"
>
<h2>Clipped specimen tray</h2>
<div class="split-button-layer-demo__clip" dir="rtl">
<c-CSplitButton
label="Clipped specimen actions"
menu_label="More clipped specimen actions"
placement="bottom-end"
c-primary_attrs="{'data-open-provenance':'','@click':'last=`Primary requested provenance`'}"
$c-props="{onAction:(value)=>{
last=value;
if (value === 'open-provenance') dialogOpen=true;
}}"
>
<c-fill name="default">Record provenance</c-fill>
<c-fill name="menu">
<c-CMenuItem value="open-provenance">Open provenance Dialog</c-CMenuItem>
<c-CMenuSubmenu value="archive">
<c-fill name="label">Archive destination</c-fill>
<c-fill name="default">
<c-CMenuItem value="alpine-archive">Alpine archive</c-CMenuItem>
<c-CMenuItem value="coastal-archive">Coastal archive</c-CMenuItem>
</c-fill>
</c-CMenuSubmenu>
</c-fill>
</c-CSplitButton>
</div>
<div x-ref="shadowHost" class="split-button-layer-demo__shadow-host">
<div x-ref="shadowFixture">
<c-CSplitButton
label="Shadow specimen actions"
menu_label="More Shadow specimen actions"
>
<c-fill name="default">Save Shadow specimen</c-fill>
<c-fill name="menu">
<c-CMenuItem value="shadow-export">Export from ShadowRoot</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</div>
</div>
<output aria-live="polite" x-text="last">No layer action yet</output>
<c-CDialog
size="sm"
$c-props="{
open:dialogOpen,
onOpenChange:(next)=>dialogOpen=next,
}"
>
<c-fill name="title">Specimen provenance</c-fill>
<c-fill name="default">Collected above the tree line during the August survey.</c-fill>
<c-fill name="actions" data="{ close_attrs }">
<c-CButton c-attrs="close_attrs">Close provenance</c-CButton>
</c-fill>
</c-CDialog>
</section>
"""
css = """
:where(.split-button-layer-demo) {
display: grid;
gap: 1rem;
justify-items: start;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-layer-demo h2) { margin: 0; }
:where(.split-button-layer-demo__clip) {
overflow: hidden;
inline-size: min(100%, 24rem);
block-size: 7rem;
padding: 2rem;
border: 1px solid color-mix(in srgb, CanvasText 30%, transparent);
border-radius: 0.75rem;
}
:where(.split-button-layer-demo__shadow-host) {
display: block;
padding: 0.75rem;
border: 1px dashed GrayText;
}
"""
preview = SplitButtonLayersAndDialog()
preview # noqa: B018
Render Dialog and other peer overlays as siblings. Menu declaration content still follows CMenu's noninteractive item-content boundary.
Customize the joined control and Menu
Both Buttons consume the public --cui-button-* variables, and the Menu keeps the public --cui-menu-* contract. SplitButton adds variables for its divider, Menu Button width, and joined radius. Stable part selectors target each half and its content without changing semantic ownership.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SplitButtonCustomization(Component):
template = """
<section class="split-button-brand-demo">
<article class="split-button-brand-demo__card split-button-brand-demo__card--orchard">
<p>Orchard field guide</p>
<c-CSplitButton
class_="split-button-brand-demo__subject"
label="Orchard specimen publishing actions"
menu_label="More Orchard specimen publishing actions"
open
>
<c-fill name="default">Publish alpine gentian observation</c-fill>
<c-fill name="menu">
<c-CMenuItem value="orchard-draft">Save Orchard draft</c-CMenuItem>
<c-CMenuItem value="orchard-export">Export Orchard record</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</article>
<article
class="split-button-brand-demo__card split-button-brand-demo__card--harbor"
dir="rtl"
>
<p>دليل ميناء للأبحاث الميدانية</p>
<c-CSplitButton
class_="split-button-brand-demo__subject"
label="إجراءات نشر ملاحظة العينة الساحلية"
menu_label="المزيد من إجراءات نشر ملاحظة العينة الساحلية"
variant="outline"
open
>
<c-fill name="default">نشر ملاحظة العينة الساحلية الطويلة</c-fill>
<c-fill name="menu">
<c-CMenuItem value="harbor-draft">حفظ المسودة الساحلية</c-CMenuItem>
<c-CMenuItem value="harbor-export">تصدير السجل الساحلي</c-CMenuItem>
</c-fill>
</c-CSplitButton>
</article>
</section>
"""
css = """
:where(.split-button-brand-demo) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
gap: 1rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.split-button-brand-demo__card) {
min-block-size: 18rem;
padding: 1.25rem;
border-radius: 1rem;
}
:where(.split-button-brand-demo__card > p) { margin-block: 0 1rem; font-weight: 700; }
:where(.split-button-brand-demo__subject) { max-inline-size: 20rem; }
:where(.split-button-brand-demo__card--orchard) {
color-scheme: light;
background: #f5f0df;
--cui-button-background: #315f37;
--cui-button-foreground: #fffdf5;
--cui-button-hover-background: #244c2a;
--cui-menu-background: #fffdf5;
--cui-menu-foreground: #203422;
--cui-menu-focus-background: #d9e9cf;
--cui-menu-focus-foreground: #17351c;
--cui-split-button-divider-color: #c5d7bb;
--cui-split-button-divider-width: 2px;
--cui-split-button-radius: 0.75rem;
}
:where(.split-button-brand-demo__card--harbor) {
color-scheme: dark;
background: #102b38;
--cui-button-background: #c6ecff;
--cui-button-foreground: #082633;
--cui-button-border-color: #79bfdc;
--cui-button-hover-background: #a7ddf5;
--cui-menu-background: #173c4c;
--cui-menu-foreground: #eefaff;
--cui-menu-focus-background: #95d9f4;
--cui-menu-focus-foreground: #062531;
--cui-split-button-divider-color: #29586b;
--cui-split-button-divider-width: 1px;
--cui-split-button-radius: 0.375rem;
}
:where(.split-button-brand-demo [data-citry-ui-part="split-button-primary"]) {
min-inline-size: 0;
}
:where(.split-button-brand-demo [data-citry-ui-part="split-button-primary-content"]) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
:where(.split-button-brand-demo) {
--cui-menu-duration: 0ms;
}
}
@media (forced-colors: active) {
:where(.split-button-brand-demo__card) {
border: 1px solid CanvasText;
forced-color-adjust: auto;
}
}
@media print {
:where(.split-button-brand-demo__card) {
break-inside: avoid;
border: 1px solid currentColor;
background: transparent;
color: black;
}
}
"""
preview = SplitButtonCustomization()
preview # noqa: B018
The horizontal compound keeps the primary at logical start in RTL, preserves the Menu Button target width at narrow sizes, removes motion under reduced motion, and retains visible focus and divider boundaries in forced colors.
Choose explicit composition for a different policy
Compose CButtonGroup and CMenu when the primary must be a link, actions are equal peers, the layout must be vertical, or the two surfaces need separate state owners. SplitButton intentionally keeps one dominant command, one Menu owner, one horizontal anatomy, and no imperative methods or custom DOM events.
Trust the four attribute destinations deliberately
attrs, primary_attrs, trigger_attrs, and menu_attrs are copied and validated for their documented roots. They accept ordinary styling, language, permitted ARIA, and data-* except data-citry-*, data-cev*, data-cid*, and owned reflections. @event and x-on:event Alpine listeners are allowed; raw on* browser-expression attributes are rejected. The primary also accepts the documented native Form attributes, but URL-like action destinations remain consumer-owned and are not sanitized or trusted by Citry. Component-owned identity, semantics, state, focus order, popover targeting, Citry runtime fields, and structural Alpine ownership are rejected.
Primary content accepts text and decorative noninteractive content. The final Button needs a nonempty accessible name from visible text, aria-label, or aria-labelledby. Menu declarations retain CMenu's exact trust boundary.
API reference
Inputs
CSplitButton server inputs
Server inputs are passed in a template through <c-CSplitButton ... /> or in Python through CSplitButton(...).
| Input | Type | Default | Effect |
|---|---|---|---|
id | str | None | generated | Sets the literal root ID base and the primary, Menu Button, and Menu surface ID family. |
label | non-whitespace str | required | Names the related two-Button group. |
menu_label | non-whitespace str | required | Names the secondary Menu Button with its specific purpose. |
type | "button" | "submit" | "reset" (CButtonType) | "button" | Selects the primary native Button activation and Form behavior. |
disabled | bool | False | Disables both Buttons and force-closes the Menu. |
primary_disabled | bool | False | Disables only the primary Button. |
menu_disabled | bool | False | Disables only the Menu Button and force-closes the Menu. |
loading | bool | False | Marks only the primary pending, retains its focus, and blocks new primary activation. |
variant | "solid" | "outline" | "ghost" (CButtonVariant) | "solid" | Sets both Buttons' presentation strength. |
intent | "primary" | "neutral" | "success" | "warn" | "danger" (CButtonIntent) | "primary" | Sets both Buttons' semantic color role. |
size | "sm" | "md" | "lg" (CButtonSize) | "md" | Sets both Buttons and Menu item geometry. |
block | bool | False | Fills the containing inline size while the primary takes remaining space. |
loading_pos | "start" | "center" | "end" (CButtonLoadingPos) | "center" | Places the primary loading indicator. |
open | bool | False | Sets initial Menu visibility and the uncontrolled fallback. |
loop | bool | True | Wraps Menu arrow navigation and typeahead. |
placement | "top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end" (CMenuPlacement) | "bottom-end" | Sets the preferred logical placement relative to the full group. |
match_width | bool | False | Matches the full group width up to the Menu viewport-safe maximum. |
close_on_select | bool | True | Sets the root Menu default close policy. |
class_ | str | Mapping[str, bool] | Sequence[CClassValue] | None (CClassValue) | None | Adds root classes and merges them with attrs. |
style | str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None (CStyleValue) | None | Adds root styles; generated anchor ownership merges last. |
attrs | Mapping[str, object] | None | None | Adds copied allowed attributes to the labelled group root. |
primary_attrs | Mapping[str, object] | None | None | Adds copied allowed ARIA, Form, data, style, and native listener attributes to the primary Button. |
trigger_attrs | Mapping[str, object] | None | None | Adds copied allowed descriptive, data, style, and native listener attributes to the Menu Button. |
menu_attrs | Mapping[str, object] | None | None | Adds copied allowed descriptive, language, data, style, and native listener attributes to the root Menu surface. |
CSplitButton client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CSplitButton />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
open | boolean | null | Releases control from the latest committed state. null has the same effect. | Controls Menu visibility while supplied as a Boolean. |
disabled | boolean | Uses the server input. | Controls common disabledness; native fieldset disabledness remains authoritative. |
primaryDisabled | boolean | Uses the server input. | Controls primary-only disabledness. |
menuDisabled | boolean | Uses the server input. | Controls Menu-only disabledness and forced close. |
loading | boolean | Uses the server input. | Controls the focus-retaining primary pending guard. |
variant | "solid" | "outline" | "ghost" (CButtonVariant) | Uses the server input. | Controls both Buttons' presentation strength. |
intent | "primary" | "neutral" | "success" | "warn" | "danger" (CButtonIntent) | Uses the server input. | Controls both Buttons' semantic color role. |
size | "sm" | "md" | "lg" (CButtonSize) | Uses the server input. | Controls Button and Menu geometry. |
block | boolean | Uses the server input. | Controls full-inline group layout. |
loadingPosition | "start" | "center" | "end" (CButtonLoadingPos) | Uses the server input. | Controls primary loading-indicator placement. |
loop | boolean | Uses the server input. | Controls Menu navigation wrapping. |
placement | six logical placement strings (CMenuPlacement) | Uses the server input. | Controls requested full-root Menu placement. |
matchWidth | boolean | Uses the server input. | Controls clamped full-group width matching. |
closeOnSelect | boolean | Uses the server input. | Controls the root Menu default close policy. |
onOpenChange | function | Omission or null selects no visibility callback. | Receives Menu requests, forced closes, and deferred primary action-close notices. |
onAction | function | Omission or null selects no Menu action callback. | Receives valued Menu command and choice activations. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CSplitButton slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | yes | {} (CSplitButtonDefaultSlotData) | None. Must be structurally nonempty and help provide the final primary accessible name. |
start | no | {} (CSplitButtonStartSlotData) | Omitted. |
end | no | {} (CSplitButtonEndSlotData) | Omitted. |
loading | no | {} (CSplitButtonLoadingSlotData) | CSS spinner hidden from accessibility. |
menu | yes | {} (CSplitButtonMenuSlotData) | None. Requires a nonempty collection of existing CMenu declarations. |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CSplitButton events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onOpenChange | (requestedOpen: boolean, detail: CMenuOpenChangeDetail) => void (CMenuOpenChangeDetail) | A root Menu visibility request, forced close, or accepted primary action close occurs. | {reason, controlled, forced, source} (CMenuOpenChangeDetail) | Menu gestures use CMenu timing. A primary action notice runs in a cancelable zero-delay task after native activation; uncontrolled state already closed and controlled state waits. |
onAction | (value: string, detail: CMenuActionDetail) => void (CMenuActionDetail) | An enabled valued Menu command, checkbox, or radio activates. | {kind, item, event, path} (CMenuActionDetail) | Uses CMenu callback order and fires once. The primary action, links, and anonymous commands do not fire it. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CSplitButton CSS variables
Apply these variables to CSplitButton or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-split-button-divider-color | color | Boundary between the two native Buttons. | color-mix(in srgb, currentColor 32%, transparent) |
--cui-split-button-divider-width | length | Joined divider width and overlap. | 1px |
--cui-split-button-menu-inline-size | length | Menu Button inline target size. | Effective Button height. |
--cui-split-button-radius | length | Joined outer corners. | var(--cui-button-radius, 0.5rem) |
--cui-button-background | color | Both Button resting surfaces. | Variant- and intent-derived color. |
--cui-button-foreground | color | Both Button foregrounds. | Derived contrast color. |
--cui-button-border-color | color | Both Button borders. | Variant- and intent-derived color. |
--cui-button-hover-background | color | Enabled hover surfaces. | Derived color mix. |
--cui-button-active-background | color | Enabled active surfaces. | Derived stronger color mix. |
--cui-button-focus-color | color | Both focus-visible outlines. | Highlight |
--cui-button-radius | length | Source Button radius used by the joined fallback. | 0.5rem |
--cui-button-font-weight | font-weight | Both Button labels. | 600 |
--cui-button-gap | length | Primary content-region gaps. | 0.5rem |
--cui-button-disabled-opacity | number | Disabled Button content opacity. | 0.48 |
--cui-button-height | length | Both Button minimum block size. | Size-derived length. |
--cui-button-inline-padding | length | Primary inline padding. | Size-derived length. |
--cui-button-block-padding | length | Both Button block padding. | Size-derived length. |
--cui-button-font-size | length | Both Button font size. | Size-derived length. |
--cui-menu-background | color | Root and submenu surfaces. | Canvas |
--cui-menu-foreground | color | Menu item text. | CanvasText |
--cui-menu-muted-color | color | Menu descriptions, labels, and shortcuts. | color-mix(in srgb, current foreground 72%, transparent) |
--cui-menu-border-color | color | Menu surface and separator boundaries. | color-mix(in srgb, CanvasText 18%, transparent) |
--cui-menu-border-width | length | Menu surface boundary width. | 1px |
--cui-menu-radius | length | Menu surface corners. | 0.75rem |
--cui-menu-shadow | shadow | Root Menu elevation. | 0 0.75rem 2rem rgb(15 23 42 / 18%) |
--cui-menu-submenu-shadow | shadow | Nested Menu elevation. | 0 1rem 2.5rem rgb(15 23 42 / 22%) |
--cui-menu-inline-size | length | Preferred Menu width. | 14rem |
--cui-menu-min-inline-size | length | Minimum useful submenu corridor. | 10rem |
--cui-menu-max-inline-size | length | Viewport-safe Menu width. | calc(100dvi - 1rem) |
--cui-menu-max-block-size | length | Menu scroll limit. | min(24rem, calc(100dvb - 1rem)) |
--cui-menu-padding | length | Menu surface edge spacing. | 0.375rem |
--cui-menu-item-block-size | length | Menu item minimum height. | Size-derived. |
--cui-menu-item-padding-inline | length | Menu item inline spacing. | Size-derived. |
--cui-menu-item-gap | length | Menu item-region gap. | 0.625rem |
--cui-menu-item-radius | length | Menu item corners. | 0.5rem |
--cui-menu-hover-background | color | Menu pointer-hover fill. | color-mix(in srgb, CanvasText 8%, transparent) |
--cui-menu-focus-background | color | Focused Menu item fill. | light-dark(#175cd3, #84adff) |
--cui-menu-focus-foreground | color | Focused Menu item content. | light-dark(#ffffff, #101828) |
--cui-menu-focus-outline-color | color | Menu item focus-visible outline. | light-dark(#175cd3, #84adff) |
--cui-menu-danger-color | color | Destructive Menu item content. | light-dark(#b42318, #fda29b) |
--cui-menu-disabled-opacity | number | Disabled Menu content opacity. | 0.5 |
--cui-menu-offset | length | Root Menu anchor gap. | 0.375rem |
--cui-menu-submenu-offset | length | Nested Menu anchor gap. | 0.25rem |
--cui-menu-duration | time | Menu entry and exit duration. | 120ms |
--cui-menu-easing | easing | Menu entry and exit curve. | cubic-bezier(0.2, 0.8, 0.2, 1) |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CSplitButton attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
role | Root | "group" | Groups the dominant action and its related Menu Button. |
aria-label | Root | non-whitespace string | Uses the required group label. |
data-variant | Root | "solid" | "outline" | "ghost" (CButtonVariant) | Mirrors effective common presentation. |
data-intent | Root | five CButton intents (CButtonIntent) | Mirrors effective common semantic color. |
data-size | Root | "sm" | "md" | "lg" (CButtonSize) | Mirrors effective Button and Menu geometry. |
data-block | Root | present | absent | Present when the group fills available inline size. |
data-disabled | Root | present | absent | Mirrors the common disabled override. |
data-primary-disabled | Root | present | absent | Mirrors browser-effective primary disabledness. |
data-menu-disabled | Root | present | absent | Mirrors browser-effective Menu Button disabledness. |
data-loading | Root | present | absent | Mirrors effective primary pending state. |
data-loading-position | Root | "start" | "center" | "end" (CButtonLoadingPos) | Mirrors primary loading placement. |
data-open | Root | present | absent | Mirrors committed root Menu visibility. |
CSplitButton attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
id | Primary Button | root ID plus -primary | Uses the exact owned primary identity. |
type | Primary Button | "button" | "submit" | "reset" (CButtonType) | Preserves the authored native action. |
disabled | Primary Button | present | absent | Represents effective native disabledness and the no-JavaScript loading fallback. |
aria-busy | Primary Button | "true" | absent | Present only while primary work is pending. |
aria-disabled | Primary Button | "true" | absent | Present while disabled or loading. |
data-disabled | Primary Button | present | absent | Mirrors effective disabledness. |
data-loading | Primary Button | present | absent | Mirrors effective pending state. |
data-variant | Primary Button | three CButton variants (CButtonVariant) | Mirrors presentation strength. |
data-intent | Primary Button | five CButton intents (CButtonIntent) | Mirrors semantic color. |
data-size | Primary Button | three CButton sizes (CButtonSize) | Mirrors geometry. |
data-loading-position | Primary Button | three positions (CButtonLoadingPos) | Mirrors pending-indicator placement. |
CSplitButton attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
id | Menu Button | root ID plus -menu-trigger | Uses the exact owned Menu Button identity. |
type | Menu Button | "button" | Prevents Form submission in every state. |
aria-label | Menu Button | non-whitespace string | Uses menu_label as the explicit secondary action name. |
aria-haspopup | Menu Button | "menu" | Announces the popup kind. |
aria-controls | Menu Button | Menu surface IDREF | References the owned root Menu surface. |
aria-expanded | Menu Button | "true" | "false" | Mirrors logical root Menu state. |
disabled | Menu Button | present | absent | Mirrors effective native Menu disabledness. |
data-disabled | Menu Button | present | absent | Styles effective Menu disabledness. |
data-variant | Menu Button | three CButton variants (CButtonVariant) | Mirrors presentation strength. |
data-intent | Menu Button | five CButton intents (CButtonIntent) | Mirrors semantic color. |
data-size | Menu Button | three CButton sizes (CButtonSize) | Mirrors geometry. |
id | Root Menu surface | root ID plus -menu | Uses the exact owned Menu surface identity. |
popover | Root and submenu Menu surfaces | "manual" | Uses native top-layer presence with Citry dismissal. |
role | Root and submenu Menu surfaces | "menu" | Exposes application Menu semantics. |
aria-labelledby | Root Menu surface | Menu Button IDREF | Names the root Menu from its trigger. |
data-open | Root and submenu Menu surfaces | present | absent | Mirrors logical Menu visibility. |
data-placement | Root Menu surface | six logical placement strings (CMenuPlacement) | Mirrors the requested root placement. |
data-match-width | Root Menu surface | present | absent | Indicates clamped full-root width matching. |
data-size | Root Menu surface | three sizes (CButtonSize) | Mirrors effective Menu item geometry. |
CSplitButton attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
aria-describedby | Menu item root | description IDREF | absent | Uses CMenu's optional separate item description. |
aria-checked | Checkbox or radio item Button | "false" | "true" | "mixed" | Uses the effective CMenu choice state; radio items never use mixed. |
data-checked | Checkbox or radio item Button | "false" | "true" | "mixed" | Mirrors the effective CMenu choice state for styling. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CSplitButton selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="split-button"] | Root div | Labelled group and class, style, and attrs destination. |
[data-citry-ui-part="split-button-primary"] | Primary native Button | Dominant action and primary_attrs destination. |
[data-citry-ui-part="split-button-primary-start"] | Optional decorative wrapper | Primary logical-start content. |
[data-citry-ui-part="split-button-primary-content"] | Required content wrapper | Visible dominant action content. |
[data-citry-ui-part="split-button-primary-end"] | Optional decorative wrapper | Primary logical-end content. |
[data-citry-ui-part="split-button-primary-loading-indicator"] | Stable decorative wrapper | Primary pending indicator. |
[data-citry-ui-part="split-button-menu-trigger"] | Secondary native Button | Menu activation and trigger_attrs destination. |
[data-citry-ui-part="split-button-menu-indicator"] | Decorative span | Logical-down Menu indicator. |
[data-citry-ui-part="menu"] | Root or submenu Menu surface | Popover presence and collection focus. |
[data-citry-ui-part="menu-item"] | Command, link, checkbox, or radio root | Menu item styling. |
[data-citry-ui-part="menu-item-start"] | Decorative item wrapper | Item logical-start content. |
[data-citry-ui-part="menu-item-label"] | Visible item label | Layout and exact owned label target. |
[data-citry-ui-part="menu-item-description"] | Optional description | Supporting text and accessible description. |
[data-citry-ui-part="menu-item-end"] | Decorative item wrapper | Shortcut or logical-end content. |
[data-citry-ui-part="menu-choice-indicator"] | Decorative choice marker | Checkbox and radio state. |
[data-citry-ui-part="menu-group"] | Labelled group root | Generic command grouping. |
[data-citry-ui-part="menu-group-label"] | Visible group label | Exact group name and layout. |
[data-citry-ui-part="menu-radio-group"] | Radio group root | Exclusive choice grouping. |
[data-citry-ui-part="menu-separator"] | Horizontal separator | Collection division. |
[data-citry-ui-part="menu-submenu"] | Neutral submenu wrapper | Child trigger and surface ownership. |
[data-citry-ui-part="menu-submenu-trigger"] | Nested Menu Button | Submenu activation and placement anchor. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] |
CButtonType | Literal["button", "submit", "reset"] |
CButtonVariant | Literal["solid", "outline", "ghost"] |
CButtonIntent | Literal["primary", "neutral", "success", "warn", "danger"] |
CButtonSize | Literal["sm", "md", "lg"] |
CButtonLoadingPos | Literal["start", "center", "end"] |
CMenuPlacement | Literal["top-start", "top", "top-end", "bottom-start", "bottom", "bottom-end"] |
CSplitButtonDefaultSlotData
Empty dataclass: {}.
CSplitButtonStartSlotData
Empty dataclass: {}.
CSplitButtonEndSlotData
Empty dataclass: {}.
CSplitButtonLoadingSlotData
Empty dataclass: {}.
CSplitButtonMenuSlotData
Empty dataclass: {}.
CMenuOpenChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | "trigger" | "escape" | "outside" | "focus-outside" | "tab" | "action" | "native" | "disabled" | "ancestor" | - | Cause of the requested or forced visibility change. |
controlled | boolean | - | Whether a valid client Boolean owns desired Menu state. |
forced | boolean | - | Whether native or structural safety overrides owner refusal. |
source | Element | EventTarget | null | - | Browser source associated with the change. |
CMenuActionDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
kind | "command" | "checkbox" | "radio" | - | Activated semantic Menu item kind. |
item | Element | - | Activated Menu item root. |
event | Event | - | Native Menu activation event. |
path | list[str] | - | Canonical ancestor-submenu path from the SplitButton root Menu. |
Translation keys
-