Sortable
Use CSortable for a finite collection whose order matters. Each CSortableItem supplies stable identity, a plain accessible label, and visible content. The initial server order remains useful before JavaScript starts.
Reorder a list
Drag an Item by its handle. Keyboard users focus the same handle, press Space or Enter to pick it up, use arrow keys, Home, or End to move it, then press Space or Enter to drop. Escape cancels.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SortableAtAGlance(Component):
template = """
<c-CSortable name="release-priority">
<c-CSortableItem value="design" label="Design review">Design review</c-CSortableItem>
<c-CSortableItem value="accessibility" label="Accessibility pass">Accessibility pass</c-CSortableItem>
<c-CSortableItem value="implementation" label="Implementation">Implementation</c-CSortableItem>
<c-CSortableItem value="release" label="Release">Release</c-CSortableItem>
</c-CSortable>
"""
preview = SortableAtAGlance()
preview # noqa: B018
Values must be unique. order can provide a full initial permutation; otherwise declaration order wins. Disabled Items remain in order but cannot be moved.
Render rich items and custom handles
The default slot receives value, label, disabled, and zero-based index. The optional handle slot changes only the button contents. Citry UI keeps the native button, accessible name, focus behavior, and moving semantics.
Show code
# ruff: noqa: E501 - embedded Citry templates remain readable as authored HTML
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SortableRichItems(Component):
template = """
<c-CSortable label="Reorder sprint tasks">
<c-CSortableItem value="audit" label="Audit keyboard paths">
<c-fill name="handle"><span aria-hidden="true">β</span></c-fill>
<c-fill name="default"><strong>Audit keyboard paths</strong><br /><small>Accessibility Β· 3 points</small></c-fill>
</c-CSortableItem>
<c-CSortableItem value="tokens" label="Refine theme tokens">
<c-fill name="handle"><span aria-hidden="true">β</span></c-fill>
<c-fill name="default"><strong>Refine theme tokens</strong><br /><small>Design system Β· 2 points</small></c-fill>
</c-CSortableItem>
<c-CSortableItem value="locked" label="Publish release" c-disabled="True">
<strong>Publish release</strong><br /><small>Fixed until approval</small>
</c-CSortableItem>
</c-CSortable>
"""
preview = SortableRichItems()
preview # noqa: B018
Interactive controls may live in Item content because dragging begins only on the handle. Avoid making the handle slot itself interactive.
Control order from Alpine
Pass order and onOrderChange through $c-props. Controlled moves are requests: the component restores the accepted order until the owner supplies the requested permutation.
Show code
# ruff: noqa: E501 - Alpine expression remains readable in the public example
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SortableControlled(Component):
template = """
<section x-data="{order:['draft','review','ship'],last:'No request'}">
<c-CSortable $c-props="{
order,
onOrderChange:(next,detail)=>{order=next;last=`${detail.value}: ${detail.fromIndex + 1} β ${detail.toIndex + 1}`},
}">
<c-CSortableItem value="draft" label="Draft" />
<c-CSortableItem value="review" label="Review" />
<c-CSortableItem value="ship" label="Ship" />
</c-CSortable>
<output x-text="last">No request</output>
</section>
"""
preview = SortableControlled()
preview # noqa: B018
Omit client order, or set it to null, for uncontrolled behavior. An accepted move emits native input then change from the root and calls onOrderChange.
Arrange a sortable grid
Set layout="grid" for cards or layout="horizontal" for a single row. The keyboard uses visual inline direction in horizontal and grid layouts, including RTL. Pointer collision uses the nearest Item center.
Show code
# ruff: noqa: E501 - embedded Citry templates remain readable as authored HTML
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SortableGrid(Component):
template = """
<c-CSortable layout="grid" label="Arrange dashboard cards" c-style="{'--cui-sortable-columns':'repeat(2,minmax(0,1fr))'}">
<c-CSortableItem value="revenue" label="Revenue"><strong>Revenue</strong><br />β¬42,800</c-CSortableItem>
<c-CSortableItem value="orders" label="Orders"><strong>Orders</strong><br />318</c-CSortableItem>
<c-CSortableItem value="retention" label="Retention"><strong>Retention</strong><br />91%</c-CSortableItem>
<c-CSortableItem value="alerts" label="Alerts"><strong>Alerts</strong><br />4 open</c-CSortableItem>
</c-CSortable>
"""
preview = SortableGrid()
preview # noqa: B018
Use --cui-sortable-columns to tune the responsive grid. Do not combine this family with a partial virtual window because a partial DOM cannot expose the complete accepted order.
Submit the accepted order
Set name to submit one successful form entry per Item in accepted order. form can refer to an external Form ID. A disabled root submits no entries, and native reset restores the server order or requests it in controlled mode.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SortableForms(Component):
template = """
<form>
<c-CSortable name="priority" c-order="['security','quality','speed']">
<c-CSortableItem value="speed" label="Delivery speed" />
<c-CSortableItem value="quality" label="Product quality" />
<c-CSortableItem value="security" label="Security" />
</c-CSortable>
<button type="reset">Reset order</button>
<button type="submit">Save priorities</button>
</form>
"""
preview = SortableForms()
preview # noqa: B018
Application code still owns persistence. The component never sends a request or stores order outside the current page.
Accessibility and localization
The handle has a localized name containing the Item's plain label. A polite live region announces pickup, movement, drop, and cancellation with position and total. Explicit *_label inputs belong to the caller and remain fixed; catalog defaults switch with the active Citry client locale.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class SortableAccessibility(Component):
template = """
<c-CSortable label="Arrange deployment checks">
<c-CSortableItem value="backup" label="Verify backup" />
<c-CSortableItem value="approval" label="Security approval" c-disabled="True" />
<c-CSortableItem value="deploy" label="Deploy application" />
<c-CSortableItem value="observe" label="Observe health metrics" />
</c-CSortable>
"""
preview = SortableAccessibility()
preview # noqa: B018
Pointer dragging has a touch delay so ordinary scrolling remains available. Reduced-motion and forced-color preferences retain the complete interaction. Multi-container transfer and moving tree nodes between parents are outside the first family.
API reference
Inputs
CSortable server inputs
Server inputs are passed in a template through <c-CSortable ... /> or in Python through CSortable(...).
| Input | Type | Default | Effect |
|---|---|---|---|
id | str | None | generated | Sets the root ID and bases stable Item IDs. |
order | Sequence[str] | None | None | Sets a full unique initial permutation; declaration order wins when omitted. |
name | str | None | None | Submits one hidden native entry per Item in accepted order. |
form | str | None | None | Associates hidden inputs with an external Form ID. |
layout | CSortableLayout (CSortableLayout) | "vertical" | Selects vertical horizontal or responsive-grid collision and layout. |
disabled | bool | False | Disables all handles and form contribution. |
size | CSortableSize (CSortableSize) | "md" | Selects handle and Item density. |
label | str | "Reorder items" | Overrides the localized ordered-list accessible name. |
handle_label | str | "Move {item}" | Overrides each localized handle name and must retain item. |
instructions_label | str | "Press Space or Enter to pick up. Use arrow keys to move. Press Space or Enter to drop or Escape to cancel." | Overrides hidden keyboard instructions. |
picked_up_label | str | "Picked up {item}, position {position} of {total}" | Overrides pickup announcements and must retain item position and total. |
moved_label | str | "Moved {item} to position {position} of {total}" | Overrides movement announcements and must retain item position and total. |
dropped_label | str | "Dropped {item} at position {position} of {total}" | Overrides drop announcements and must retain item position and total. |
cancelled_label | str | "Cancelled moving {item}. Position restored to {position} of {total}" | Overrides cancellation announcements and must retain item position and total. |
class_ | CClassValue | None (CClassValue) | None | Adds classes to the root. |
style | CStyleValue | None (CStyleValue) | None | Adds styles to the root. |
attrs | Mapping[str, object] | None | None | Adds copied allowed root attributes without replacing owned semantics or runtime markers. |
CSortable client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CSortable />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
order | string[] | null | Omission or null releases control. | Controls the complete accepted permutation. |
layout | "vertical" | "horizontal" | "grid" | Uses the server value. | Reactively changes layout and keyboard axes. |
disabled | boolean | Uses the server value. | Reactively disables interaction and form entries. |
onOrderChange | function | No component callback runs. | Receives pointer keyboard and reset requests. |
CSortableItem server inputs
Server inputs are passed in a template through <c-CSortableItem ... /> or in Python through CSortableItem(...).
| Input | Type | Default | Effect |
|---|---|---|---|
value | str | required | Supplies stable nonempty unique identity and submitted value. |
label | str | required | Supplies the plain Item name used by handles and announcements. |
disabled | bool | False | Keeps the Item fixed while preserving it in the order. |
class_ | CClassValue | None (CClassValue) | None | Adds classes to the rendered Item. |
style | CStyleValue | None (CStyleValue) | None | Adds styles to the rendered Item. |
attrs | Mapping[str, object] | None | None | Adds copied allowed Item attributes. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CSortable slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | yes | {} (CSortableDefaultSlotData) | None; accepts only Item declarations. |
CSortableItem slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
default | no | {value, label, disabled, index} (CSortableItemSlotData) | Plain label text. |
handle | no | {value, label, disabled, index} (CSortableItemSlotData) | A neutral drag-grip glyph inside the owned Button. |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CSortable events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onOrderChange | (order: string[], detail: CSortableOrderChangeDetail) => void (CSortableOrderChangeDetail) | A completed pointer keyboard reset or client reconciliation proposes another order. | {order, previousOrder, value, fromIndex, toIndex, source, controlled, sourceEvent} (CSortableOrderChangeDetail) | Uncontrolled state commits first; controlled state requests and restores accepted order. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CSortable CSS variables
Apply these variables to CSortable or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-sortable-gap | length | Space between Items. | 0.625rem |
--cui-sortable-columns | grid-template-columns | Responsive grid tracks. | repeat(auto-fit, minmax(12rem, 1fr)) |
--cui-sortable-item-surface | color | Item surface. | Canvas |
--cui-sortable-item-border | complete border | Item and handle divider. | Adaptive 1px neutral |
--cui-sortable-item-radius | length | Item and placeholder corners. | 0.625rem |
--cui-sortable-item-shadow | box-shadow | Moving Item elevation. | Adaptive soft shadow |
--cui-sortable-handle-size | length | Minimum handle size. | 2.75rem |
--cui-sortable-focus | color | Handle focus and placeholder accent. | Highlight |
--cui-sortable-disabled-opacity | number | Disabled Item opacity. | 0.55 |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CSortable attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
data-layout | Root | CSortableLayout (CSortableLayout) | Reflects current layout and collision profile. |
data-size | Root | CSortableSize (CSortableSize) | Reflects density. |
data-disabled | Root and disabled Items | present | absent | Reflects effective unavailability. |
data-dragging | Root | present | absent | Marks any active pointer or keyboard move. |
data-moving | Item | present | absent | Marks the actively moved Item. |
data-value | Item | string | Exposes stable Item identity. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CSortable selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="sortable"] | Root div | Theme and reflected-state destination. |
[data-citry-ui-part="items"] | Ordered list | Named collection, layout, and accepted DOM order. |
[data-citry-ui-part="item"] | One Item | Stable Item customization. |
[data-citry-ui-part="handle"] | Native Button | Pointer touch keyboard and focus owner. |
[data-citry-ui-part="content"] | Item content div | Consumer presentation wrapper. |
[data-citry-ui-part="placeholder"] | Temporary list item | Proposed pointer drop position. |
[data-citry-ui-part="status"] | Polite live region | Reorder announcements. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CSortableLayout | Literal["vertical", "horizontal", "grid"] |
CSortableSize | Literal["sm", "md", "lg"] |
CSortableChangeSource | Literal["pointer", "keyboard", "reset", "client"] |
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, object] | Sequence[CStyleValue] |
CSortableDefaultSlotData
Empty dataclass: {}.
CSortableItemSlotData
| Field | Type | Default | Meaning |
|---|---|---|---|
value | str | - | Stable Item value. |
label | str | - | Plain accessible label. |
disabled | bool | - | Declared disabled state. |
index | int | - | Initial zero-based accepted index. |
CSortableOrderChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
order | list[str] | - | Requested or committed order. |
previousOrder | list[str] | - | Accepted order before the move. |
value | str | - | Moved Item value. |
fromIndex | int | - | Previous zero-based index. |
toIndex | int | - | Proposed zero-based index. |
source | CSortableChangeSource (CSortableChangeSource) | - | Pointer keyboard reset or client cause. |
controlled | bool | - | Whether client order owns accepted state. |
sourceEvent | object | None | - | Native source Event or null. |
Translation keys
Catalog keys used by this family. An explicit component input or slot listed in Override takes precedence over the catalog for that instance.
CSortable translation keys
| Key | Purpose | Variables | Override | Browser updates |
|---|---|---|---|---|
citry-ui-sortable-label | Names the collection. | None. | label | Stable $c-tr attribute. |
citry-ui-sortable-handle | Names each handle. | item: str | handle_label with {item} | Stable reactive $c-tr attribute. |
citry-ui-sortable-instructions | Explains keyboard operation. | None. | instructions_label | Server HTML; instructions do not change while a move is active. |
citry-ui-sortable-picked-up | Announces pickup. | item: str; position: str; total: str | picked_up_label | One-shot i18n.tr() live-region output. |
citry-ui-sortable-moved | Announces a proposed position. | item: str; position: str; total: str | moved_label | One-shot i18n.tr() live-region output. |
citry-ui-sortable-dropped | Announces accepted drop. | item: str; position: str; total: str | dropped_label | One-shot i18n.tr() live-region output. |
citry-ui-sortable-cancelled | Announces cancellation and restored position. | item: str; position: str; total: str | cancelled_label | One-shot i18n.tr() live-region output. |