# Citry Source: https://citry.dev/
# The complete frontend stack for Python.

Citry is a free, open source HTML-first frontend framework for Python web applications.

HTML, JS, CSS, event endpoints, server state, translations... one component holds all of it.

No NPM, no NodeJS build, no server-browser API hassle.

pip install citry
product_card.py
```citry from citry import Component, SlotInput class ProductCard(Component): class Kwargs: tags: list[str] likes: int = 0 accent: str = "#175cd3" class Slots: body: SlotInput footer: SlotInput | None = None class State(Kwargs): pass class Events: def like(self, state: ProductCard.State): return ProductCard( tags=state.tags, likes=state.likes + 1, ) def template_data(self, kwargs: Kwargs, slots: Slots): return { "likes": kwargs.likes, "tags": kwargs.tags, } def js_data(self, kwargs: Kwargs, slots: Slots): return {"likes": kwargs.likes} def css_data(self, kwargs: Kwargs, slots: Slots): return {"accent": kwargs.accent} template = """

{{ tr("product-card-no-tags") }}

No footer yet
""" js = """ $component(({ els, data }) => { const cardEl = els[0]; animateLikes(cardEl, data.likes); }); """ css = """ .card { border-left: 3px solid var(--accent); } .tag--active { color: var(--accent); } """ messages = """ product-card-no-tags = No tags yet. """ class Dependencies: js = ["https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"] css = ["https://unpkg.com/normalize.css@8.0.1/normalize.css"] html = str(ProductCard( tags=["new", "sale"], slots={"body": "Aurora Lamp"} )) ```

Development sponsored by

Watch the 50-minute Citry and Django code-along on YouTube
## One file holds the entire component end-to-end.

Inputs, slots, markup, translated messages, server events and state, browser behavior, and styles, all live together. No context switching.

Point at any marked line below to see what it does:

product_card.py
from citry import Component, SlotInputclass ProductCard(Component):    class Kwargs:        tags: list[str]        likes: int = 0        accent: str = "#175cd3"    class Slots:        body: SlotInput        footer: SlotInput | None = None    class State(Kwargs):        pass    class Events:        def like(self, state: ProductCard.State):            return ProductCard(                tags=state.tags,                likes=state.likes + 1,            )    def template_data(self, kwargs: Kwargs, slots: Slots):        return {            "likes": kwargs.likes,            "tags": kwargs.tags,        }    def js_data(self, kwargs: Kwargs, slots: Slots):        return {"likes": kwargs.likes}    def css_data(self, kwargs: Kwargs, slots: Slots):        return {"accent": kwargs.accent}    template = """      <article        class="card"        x-data="{ open: false }"      >        <c-slot name="body" />        <c-for each="tag in tags">          <c-Tag            c-label="tag"            $c-props="{ highlight: open }"            @click="open = !open"          />        </c-for>        <c-empty>          <p>{{ tr("product-card-no-tags") }}</p>        </c-empty>        <button type="button" @c-click="like">          Like <span x-text="likes">{{ likes }}</span>        </button>        <c-slot name="footer">          No footer yet        </c-slot>      </article>    """    js = """      $component(({ els, data }) => {        const cardEl = els[0];        animateLikes(cardEl, data.likes);      });    """    css = """      .card {        border-left: 3px solid var(--accent);      }      .tag--active {        color: var(--accent);      }    """    messages = """      product-card-no-tags = No tags yet.    """    class Dependencies:        js = ["https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"]        css = ["https://unpkg.com/normalize.css@8.0.1/normalize.css"]html = str(ProductCard(    tags=["new", "sale"],    slots={"body": "Aurora Lamp"}))

Point at a marked line to see what it does.

InputsDeclared inputs

Every input this component accepts, with its type and any default. Passing an unknown name, or leaving out a required one, is reported when the component renders rather than quietly producing a gap in the page.

SlotsOpenings the caller fills

Named places a caller passes markup into. body is required and footer is optional, so the contract covers content as well as data.

StateState that survives a call

Server-side state available across Python event handler calls. Travels between the server and the browser. Inheriting Kwargs makes the State carry the same fields.

EventsPython that runs on interaction

A public method here can be called from the browser using @c-event="like". like reads the current state and renders the updated component, which the browser then displays.

DataUse Python variables in templates, browser behavior, and CSS

template_data prepares template variables, js_data seeds Alpine variables from JSON, and css_data creates CSS variables scoped to this one instance.

Browser stateState that never leaves the page

x-data holds what only the browser cares about. Opening and closing the card needs no server, so it never asks one.

SlotWhere filled content lands

<c-slot> marks the spot the caller's content drops into, inside markup this component still controls.

Control flowA loop, a child, and the empty case

<c-for> repeats a child component, while <c-empty> runs when there are no tags at all. The child component <c-Tag> receives label as Python value, and highlight as Alpine (browser) value through $c-props. You can listen to children's Alpine events with regular @click.

Translated textFirst-class support for i18n and l10n

tr() translates text to this render's locale. Translation keys are defined as Fluent syntax in this same component file.

BindingsAlpine and Python, side by side

@click stays in the browser for instant feedback, while @c-click calls the Python handler set in the value, like.

FallbackWhat shows when nobody fills it

Content between the tags is the fallback for an optional slot, so a caller who skips it still gets something sensible.

ScriptAdvanced setup scoped to this component

Templates use js_data values directly. Add $component when an imperative library or other setup needs this instance's elements and data.

StyleStyles reading Python values

var(--accent) reads the custom property css_data produced, and it is scoped to this instance, so two cards on one page can differ without a second stylesheet.

MessagesWrite translation keys as Fluent syntax

With Fluent by Mozilla, you can define translation keys that can easily handle genders, counts, composition, or even formatting. Export the messages to translate the catalog to other locales.

AssetsThird-party scripts and styles

Libraries this component needs. Citry loads each script only once per page, however many components may use it.

RenderRendering is a function call

Rendering returns ordinary HTML. This makes components easy to integrate with web frameworks, or test with plain Python.

## Use with any web server
or standalone.

Citry's server-side events need a route on your application. Two lines of code and you're all set. If you don't need events, you can use Citry without a server.

See the [web framework integrations](/web-frameworks/) and [server events](/events/).
main.py
from contextlib import asynccontextmanager

from fastapi import FastAPI

from citry import citry
from citry.contrib.fastapi import mount

@asynccontextmanager
async def lifespan(_app: FastAPI):
    citry.initialize()
    yield

app = FastAPI(lifespan=lifespan)
mount(app, citry)
app.py
from flask import Flask

from citry import citry
from citry.contrib.flask import mount

app = Flask(__name__)
mount(app, citry, prefix="/citry")
citry.initialize()
urls.py
from django.urls import path

from citry import citry
from citry.contrib.django import urlpatterns as citry_urls

urlpatterns = [
    path("", home_view),
    *citry_urls(citry, prefix="/citry"),
]
asgi.py
from citry import citry
from citry.contrib.asgi import asgi_app

citry.initialize()
app = asgi_app(citry)
wsgi.py
from citry import citry
from citry.contrib.wsgi import wsgi_app

citry.initialize()
application = wsgi_app(citry)
## Catch mistakes early.

Citry was born out of frustration with Django's silent coerctions and leaky isolations.

In Citry, what you see (in your component) is what you get:

Read about [inputs and validation](/concepts/inputs-and-validation/), [error boundaries](/concepts/error-boundaries/), and [testing components](/advanced/testing/).
input.py
card = StatusCard(
    complete=18,
    total=25,
)

# Rendering is where the component's inputs are checked
str(card)

Rejected as the component is called

TypeError
An error occurred while rendering components StatusCard:
StatusCard.Kwargs.__init__() missing 1 required positional argument: 'title'
misspelled.py
card = StatusCard(
    titel="Deploy preview",
    complete=18,
    total=25,
)

str(card)

Rejected, and the name you meant is offered

TypeError
An error occurred while rendering components StatusCard:
StatusCard.Kwargs.__init__() got an unexpected keyword argument 'titel'. Did you mean 'title'?
template.py
class Greeting(Component):
    class Kwargs:
        name: str

    def template_data(self, kwargs, slots):
        return {"name": kwargs.name}

    template = "<p>Hello, {{ naem }}!</p>"

str(Greeting(name="Ada"))

Pointed at the line that asked for it

KeyError
An error occurred while rendering components Greeting:
Error in variable: KeyError: 'naem'

     1 | naem 
         ^^^^

In template of 'Greeting':

     1 | <p>Hello, {{ naem }}!</p>
                   ^^^^^^^^^^
isolation.py
class Child(Component):
    template = "<span>{{ user_name }}</span>"

class Parent(Component):
    def template_data(self, kwargs, slots):
        return {"user_name": "Ada"}

    template = "<div>{{ user_name }}<c-child /></div>"

str(Parent())

A child never inherits the parent's variables

KeyError
An error occurred while rendering components Parent > Child:
Error in variable: KeyError: 'user_name'

     1 | user_name 
         ^^^^^^^^^

In template of 'Child':

     1 | <span>{{ user_name }}</span>
               ^^^^^^^^^^^^^^^
unknown.py
class Page(Component):
    template = """
      <c-StatusCrad title="Deploy preview" />
    """

str(Page())

Named at the tag that asked for it

NotRegistered
An error occurred while rendering components Page:
No component registered as 'statuscrad'.

In template of 'Page':

     1 | 
     2 | <c-StatusCrad title="Deploy preview" />
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     3 | 
mismatched.py
class Broken(Component):
    template = "<div><span>Deploy preview</div>"

str(Broken())

The parser names the tag it expected to close

SyntaxError
In template Broken:
Parse error:  --> 1:26
  |
1 | <div><span>Deploy preview</div>
  |                          ^----^
  |
  = Mismatched tags: expected closing tag '</span>', found '</div>'
unsafe.py
class Danger(Component):
    template = "<i>{{ __import__('os').system('ls') }}</i>"

str(Danger())

Template expressions cannot reach the interpreter

SecurityError
An error occurred while rendering components Danger:
Error in variable: SecurityError: variable '__import__' is unsafe

     1 | __import__('os').system('ls') 
         ^^^^^^^^^^

In template of 'Danger':

     1 | <i>{{ __import__('os').system('ls') }}</i>
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
## Your editor understands the whole component.

Citry's VSCode extension connects all parts of the component - Py, HTML, JS, CSS. Surface errors or trace values across the languages. Add completions, diagnostics, and hover hints.

Install the [VS Code extension](/ide/vscode/).

Hover or focus any dotted symbol below. Ctrl-click / ⌘-click on a symbol to go to its definition.

editor_invite_panel.pyCitry
from typing import TypedDict
from citry import CitryRender, Component


class InvitePanel(Component):
    template = """
      <section class="invite-panel">
        <h2>{{  }}</h2>

        <template x-for=" in ">
          <c-
            ="member."
            ="<>
              <small =''>
                Available now
              </small>
            </>"
            ="{ : member. }"
          />
        </template>

        <p>{{  }}</p>

        <form
          @submit.prevent="('', { email })"
          :aria-busy="('invite')"
        >
          <input x-model="" type="email" />
          <button :disabled=" || ">
            Invite member
          </button>
          <small x-show="$error('')">
            Could not send invite.
          </small>
        </form>
      </section>
    """

    js = """
      ({
        props: { compact: { type: Boolean } },
        init: ({ , , ,  }) => {
          scope.email = "";
          effect(() => {
            scope.visibleMembers = props.compact
              ? data..slice(0, 3)
              : data.;
          });
        },
      });
    """

    class Kwargs:
        title: str
        members: list[]

    class Events:
        def invite(self, data: ) -> None:
            send_invite(data["email"])

    def template_data(self, kwargs, slots):
        return {"title": kwargs.}

    def js_data(self, kwargs, slots):
        return {
            "members": kwargs.,
            "inviting": False,
        }


class MemberChip(Component):
    class Kwargs:
        name: str
        status: CitryRender

    template = """
      <span class="member-chip">
        {{ name }}
        {{ status }}
      </span>
    """

    js = """
      $component({
        props: { online: { type: Boolean } },
      });
    """


class Member(TypedDict):
    name: str
    online: bool


class InviteIn(TypedDict):
    email: str

## Build fast with 70+ ready-made components.

Citry UI gives you accessible, themeable components for layout, forms, actions, navigation, feedback, and data display. Install here.

Citry UI components

Drag a component onto the canvas and watch it arrive ready to use.

## Grow without a rewrite.

A product that works starts running into different problems. None of them need a different framework.

Read about [caching](/advanced/caching/), [extensions](/advanced/extensions/), [internationalization](/i18n/), [CSRF protection](/security/#protect-event-posts-from-csrf), [strict CSP](/security/#choose-a-csp-compatibility-mode), [HTML fragments](/advanced/html-fragments/), [component libraries](/advanced/component-libraries/), and [performance](/advanced/performance/).

Use cache in two ways: Either cache single Component class, or cache a region in the template.

Connect any backend. Use version to retire old entries on deploy.

product_card.py
class ProductCard(Component):
    class Kwargs:
        product_id: int

    class Slots:
        pass

    # Cache every time this component is called
    class Cache:
        enabled = True
        ttl = 300
        version = 1

    template = """
      <div>
        {# Cache only this region #}
        <c-cache key="expensive">
          <c-ExpensiveUI />
        </c-cache>
      </div>
    """

Extend all components at once with extensions that hook into Citry's machinery.

Hooks cover the render lifecycle, components' JS and CSS scripts, and more. Extensions can be configured globally or per-component. Extensions can also add their own URL endpoints and CLI commands.

timing.py
from citry import Citry, Extension

class TimingExtension(Extension):
    name = "timing"

    def on_component_rendered(self, ctx):
        record(type(ctx.component).__name__)
        return None  # keep the original render

app = Citry(extensions=[TimingExtension])

Citry has first-class support for internationalisation (i18n) and localisation (l10n). Use Fluent messages beside components to define translatable text and its inputs. Put translations in locale catalogs, or install catalogs from third-party packages.

Also handles locale-aware formatting and parsing of numbers, dates, currencies, direction, and more.

account_card.py
from citry import Component
from citry.ext.i18n import make_context

class AccountCard(Component):
    class Kwargs:
        name: str

    template = """
      <h2>{{ tr("account-greeting", name=name) }}</h2>
    """

    messages = """
      # @param {str} $name - User name.
      account-greeting = Welcome, { $name }.
    """

context = make_context(app, locale=request.locale)
AccountCard(name=user.name).render(
    provides={"citry_i18n": context},
)

The web framework of your choice generates and validates the CSRF token:

  • Django's middleware protects Citry routes unchanged
  • Other hosts can plug in their own token check and browser token source.
security.py
class Profile(Component):
    class Events:
        # Optional for hosts with a custom token scheme.
        # Django's CsrfViewMiddleware needs no Citry setup.
        _csrf = check_csrf

        def save(self, data: ProfileIn):
            update_profile(data)

Citry can automatically pass the CSP nonce to all scripts and styles.

Citry has 2 CSP modes:

  • Strict - Uses Alpine CSP build, raises error on incompatible syntax. Use for production.
  • Warning - Uses regular Alpine, prints all incompatibilities, but doesn't block you. Use for development.

Optionally add verified SHA-384 integrity to the scripts.

csp.py
app = Citry(
    security_csp="strict",
    security_script_integrity="citry",
)

# Generate a new value for every response.
nonce = new_response_nonce()
result = Page().render().serialize_result(csp_nonce=nonce)

response = HTMLResponse(result.html)
response.headers["Content-Security-Policy"] = (
    f"script-src 'self' 'nonce-{nonce}'; "
    f"style-src 'self' 'nonce-{nonce}'"
)

Easily integrate with HTMX. Instead of rendering a full page, render only small HTML to update one region.

Fragments carry their own JS and CSS. Citry loads whatever that region needs. Duplicate assets are never loaded twice.

views.py
card = Card(title="Welcome")

# The browser gets the markup and whatever JS and CSS it still needs
card.render().serialize(deps_strategy="fragment")

Share and publish components across projects as component libraries. Install a library with Citry.register_library()

Ideal for design systems or publishing to registries.

acme_ui/badge.py
from citry import (
    ComponentLibrary,
    LibraryComponent,
    SlotInput,
    citry,
)

# Define library components
class AcmeBadge(LibraryComponent):
    class Kwargs:
        tone: str = "neutral"

    class Slots:
        default: SlotInput | None = None

# Create Library
acme_library = ComponentLibrary(
    name="acme",
    components=[AcmeBadge],
)

# Register library with Citry
citry.register_library(acme_library)

Stop re-rendering what doesn't change. Mark constants and literals with Const. Citry will recognize it and optimize the template graph.

Marking a value in Const is a promise that the value will not change between renders.

dashboard.py
from citry import Const

# The parts that never vary are rendered once and reused
Card(cols=Const(3))
## Built in public by people who care about Python and the web.

Citry is the successor to django-components (1.5k stars), distilling years of experience into an elegant and powerful framework.

This project would be nothing without its community. The people below have contributed to Citry or django-components:

GitHub avatar of dylanjcastillo
GitHub avatar of Antoliny0919
GitHub avatar of rbeard0330
GitHub avatar of a3lem
GitHub avatar of dalito
GitHub avatar of hanifbirgani
GitHub avatar of oliverhaas
GitHub avatar of GabDug
GitHub avatar of ryanhiebert
GitHub avatar of VojtechPetru
GitHub avatar of BradleyKirton
GitHub avatar of danjac
GitHub avatar of Real-Gecko
GitHub avatar of simkimsia
GitHub avatar of spapas
GitHub avatar of ekaj2
GitHub avatar of alexandreMartinEcl
GitHub avatar of ldurey
GitHub avatar of marcfargas
GitHub avatar of spollard
GitHub avatar of David-Guillot
GitHub avatar of housUnus
GitHub avatar of rafae56038
GitHub avatar of mands
GitHub avatar of hjalves
GitHub avatar of timothyis
GitHub avatar of ar4s
GitHub avatar of imankulov
GitHub avatar of rwblokzijl
GitHub avatar of franciscobmacedo
GitHub avatar of tanssinet
GitHub avatar of TheSteveBurgess
GitHub avatar of KPCOFGS
GitHub avatar of mikucz
GitHub avatar of zachbellay
GitHub avatar of daniboygg
GitHub avatar of batistadasilva
GitHub avatar of Yaso2Go
GitHub avatar of larsent
GitHub avatar of vb8448
GitHub avatar of lhole
GitHub avatar of mlissner
GitHub avatar of bfrangi
GitHub avatar of ralphbibera
GitHub avatar of PavelPancocha
GitHub avatar of KyeRussell
GitHub avatar of joeyjurjens
GitHub avatar of barakharyati
GitHub avatar of jonathan-s
## Fund the work Citry is built in the open with funding from its sponsors. They get the roadmap early, a direct line to the maintainer, and a say in what gets built next. ### [Sponsor Citry](https://github.com/sponsors/JuroOravec){: target="_blank" rel="noopener"}
GitHub avatar of JuroOravec

Juro Oravec

Creator and maintainer of Citry

## Discover frontend that brings joy.
Start the tutorial Explore examples
pip install citry
--- # Overview Source: https://citry.dev/docs/ # Build with Citry Welcome to Citry documentation! Citry is a fully typed frontend framework for Python with server events and Alpine.js. One component holds its server-rendered HTML, browser behavior, CSS, translations, and Python event handlers. No second frontend application or separate build. It is inspired by Vue and Livewire. New to Citry? [Install Citry](/getting-started/installation/), then [build your first component](/getting-started/your-first-component/). The first component runs with plain Python, without setting up a web framework. This documentation site is built with Citry too. ## Getting started
Watch the 50-minute Citry and Django code-along on YouTube
Walk through this end-to-end tutorial. You begin with reusable server-rendered HTML, then add browser behavior, FastAPI, Python event handlers, server-side state, forms. By the end of the tutorial you build an entire admin page containing a list of items and CRUD actions per row. Follow it in order, or start with the part you need: 1. **Render components from Python:** [install Citry](/getting-started/installation/), [build a component](/getting-started/your-first-component/), and [give it Python data](/getting-started/data-in-components/). 2. **Build a page from smaller pieces:** [compose components](/getting-started/build-page/) and [let them accept flexible content](/getting-started/add-slots/). 3. **Add behavior in the browser:** [use Alpine](/getting-started/browser-interactivity/) and [connect parent and child components](/getting-started/client-props-and-handlers/). 4. **Connect the browser to Python:** [serve the page with FastAPI](/getting-started/fastapi/), [call Python from a click](/getting-started/call-python/), [keep State between calls](/getting-started/state/), and [handle forms](/getting-started/forms/). 5. **Update the page from Python:** [render into one part of the page](/getting-started/server-rendered-updates/) and [combine the patterns in a CRUD page](/getting-started/build-crud-pages/). The server-backed steps use FastAPI so they can show complete, runnable code. Citry also integrates with Django, Flask, Starlette, and other [ASGI and WSGI applications](/web-frameworks/). ## Try it live - [Playground](/playground/) - Write and render Python components in the browser. - [Examples](/examples/) - Code-first cookbook. Copy or run in the browser. ## Citry UI [Citry UI](/ui-library/) is Citry's first-party styled component library. It provides accessible buttons, fields, forms, tabs, dialogs, comboboxes, tables, and a theme you can adapt to your application. Install the separate package: ```console uv add citry-ui ``` Then [register Citry UI](/ui-library/installation/) and choose a component from its catalog. ## VS Code [Install Citry from the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=citry-dev.citry) to add: - Syntax highlighting for Citry templates - Linting and diagnostics - Completion, hover information, and navigation - Safe formatting for inline templates, JavaScript, and CSS Install the Citry extension, then add the language server to the same Python environment as the project: ```console python -m pip install citry-lsp ``` Follow the [VS Code setup guide](/ide/vscode/) to connect the extension to your application. You can also run `citry check` from a terminal or CI, whether or not your editor has a dedicated Citry integration. ## Learn more - [Template syntax](/syntax/) explains how to insert Python values, set HTML attributes from Python, show or repeat content, use built-in tags, and add Alpine behavior. - [Components](/concepts/components/) explains how component classes accept inputs, prepare template data, compose other components, and render HTML. - [Registration](/concepts/registration/) explains how a component tag finds its Python class. - [Slots](/concepts/slots/) shows how a component can accept whole pieces of HTML as content. - [Client interactivity](/concepts/client-interactivity/) covers component browser data as Alpine variables, advanced setup with `$component`, `$c-props`, and browser communication between parents and children. - [Server events](/events/) covers Python handlers, State, forms, loading and error feedback, browser events, and page updates. - [Web frameworks](/web-frameworks/) shows how to mount Citry in FastAPI, Starlette, Django, Flask, ASGI, or WSGI applications. - [Troubleshooting](/guides/troubleshooting/) starts from what went wrong and helps you find the likely cause. When a project needs more control, read how to ship [component JavaScript and CSS](/advanced/js-and-css-dependencies/), return [HTML fragments](/advanced/html-fragments/), [cache rendered output](/advanced/caching/), and [test components](/advanced/testing/). ## Useful links - [Reference](/reference/) - Python, template, and browser APIs. - [Getting help](/community/help/) - Ask questions or report a problem. - [Release notes](/releases/) - Read what changed, migration guides. - [Compatibility](/about/compatibility/) - supported Python versions, OS, and more. - [Security](/security/) - template expressions, State, browser data, and deployment responsibilities. - [Benchmarks](/about/benchmarks/) Ready to build something? [Install Citry](/getting-started/installation/) and render your first component. --- # Install Citry Source: https://citry.dev/getting-started/installation/ # Install Citry Let's install Citry. You do not need a web framework or a server yet. ## Before you start Citry supports Python 3.10 through 3.14. Check the version you are about to use: ```sh python --version ``` If that command does not work, try `python3 --version` on macOS or Linux, or `py --version` on Windows. If the version is outside the supported range, install a supported Python version before continuing. The [Compatibility page](/about/compatibility/) has the full platform details. ## Installation Install Citry into your environment: ```sh python -m pip install citry ``` Or, inside an existing `uv` project: ```sh uv add citry ``` ## Add editor support If you use VS Code, we recommend installing the [Citry extension from the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=citry-dev.citry). It adds highlighting, completion, navigation, diagnostics, and formatting for Citry code inside Python files. Follow the [VS Code setup guide](/ide/vscode/) to connect it to the Citry environment and component registry for your project. ## Set up a coding agent If you use a coding agent, follow the optional [AI coding agents guide](/getting-started/ai-agents/) to add documentation pointers and project instructions. Citry's starter projects include these instruction files; you can also add them to an existing project. ## Check the installation Save this complete example as `hello.py`. It uses Citry's [`Component`](/reference/component/#citry-component) base class: ```citry from citry import Component class Hello(Component): template = """

Hello from Citry!

""" print(Hello()) ``` Run the file: ```sh python hello.py ``` (If you added Citry with `uv`, run `uv run python hello.py` instead.) The command should print `Hello from Citry!` inside an HTML `

` element. You have now confirmed that Python can import Citry and render a component. !!! note Citry adds an attribute to the opening tag, and its value can change each time. That extra text is expected. ## Troubleshoot If running `hello.py` reports `No module named 'citry'`, the install command and the file probably used different Python environments. If pip reports that no compatible package is available, check your Python version first. If pip tries to compile the core package and the build fails, see [Compatibility](/about/compatibility/#building-from-source) for the platform and Rust requirements. ## Next steps Citry is installed and ready to render HTML. Next, [build a reusable card](/getting-started/your-first-component/) with an option, content of your choice, and its own styles. --- # Your first component Source: https://citry.dev/getting-started/your-first-component/ # Your first component Let's build something you can see and reuse: a card with a colored top border. Each time you use it, you can choose a new color and put different content inside. The finished card runs with plain Python. You do not need to set up Django, FastAPI, or another web framework. See the finished result before you start. ## Before you start It helps to recognize basic HTML, but you can copy the CSS as-is even if styling is new to you. ## Create the card A component is a reusable piece of HTML. You define it once, then use it wherever you need the same structure. Save this as `component.py`: ### component.py ````citry from citry import Component, SlotInput class Card(Component): class Kwargs: accent: str class Slots: default: SlotInput def css_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, str]: return {"accent": kwargs.accent} template = """

""" css = """ .demo-card { max-width: 24rem; padding: 1rem 1.25rem; border: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-top: 0.25rem solid var(--accent); border-radius: 8px; background: Canvas; color: CanvasText; font-family: system-ui, sans-serif; } .demo-card__title { margin: 0 0 0.25rem; font-size: 1.1rem; } .demo-card__body { margin: 0; } """ ```` It may look like a lot for a first component, so we'll unpack it one piece at a time. ## Template The [`template`](/reference/component/#citry-component-template) is ordinary HTML with one Citry tag: ```citry-html
``` [``](/reference/builtins/#c-slot) marks the place where the card's content will appear. When you write this: ```citry-html

Hello from inside the card.

``` Citry puts the paragraph where `` appears. This unnamed space is called the **default slot**. ## Component inputs The two short classes near the top of `Card` describe the parts you can change each time you use it: - [`Kwargs`](/reference/component/#citry-component-kwargs) lists the `name=value` options you can choose when you use the Card. This card has one option, `accent`, which chooses the border color. - [`Slots`](/reference/component/#citry-component-slots) lists places where text or HTML can go. This card has one place, `default`, for everything written between `` and ``. The [`css_data()`](/reference/component/#citry-component-css-data) method sends the chosen accent to CSS as `--accent`. Neither `accent` nor `default` has a fallback value, so you need to provide both when you use the card. ## Styling The [`css`](/reference/component/#citry-component-css) block travels with the Card. Citry adds it to any page that renders the component, and `var(--accent)` picks up the color you chose. The `.demo-card` selector behaves like ordinary CSS: it styles every matching element on the page. Give component classes distinctive names so they do not accidentally style something else. ## Use the card in a template Here is the important part of the complete example page: ```citry-html

Welcome

Choose the accent color, then add any content you like.

``` The `accent` option makes the top border purple. The heading and paragraph go inside the card because they sit between its opening and closing tags. ## Use the card in Python You can create the same Card directly from Python. Save this as `render.py` next to `component.py`: ```python from component import Card card = Card( accent="#8250df", slots={"default": "Build something useful."}, ) print(card) ``` The `slots` dictionary is the Python way to fill the same default slot. Run the file: ```sh python render.py ``` Citry prints the card's HTML and the styles it needs. The result looks like this (shortened): ```html
Build something useful.
``` The real HTML contains a few extra attributes that Citry uses. You do not need to write or remember them. ## Multiple instances The rules in `Card.css` are shared by every Card on the page. The value from `css_data()` stays with the Card it came from, so one Card can be blue while another is orange. Save this as `two_cards.py` next to `component.py`: ```citry from citry import Component from component import Card class CardList(Component): template = """

Blue card: Prepare the first draft.

Orange card: Review the final copy.

""" print(CardList()) ``` Run `python two_cards.py`. Both cards use the same HTML and CSS, but each card keeps the color and text given to it. This is useful whenever several copies of a component should share the same layout but keep their own colors, sizes, or other CSS values. ## Input validation Both the accent color and the content are required. If you forget the color, Citry cannot finish the Card: ```python str(Card(slots={"default": "Where is my color?"})) # TypeError: Card.Kwargs.__init__() missing ... 'accent' ``` Add `accent` and the Card renders: ```python str( Card( accent="#8250df", slots={"default": "Now the card has everything it needs."}, ) ) ``` The same thing happens if you leave out the default slot. Citry checks these values when it turns the Card into HTML, so the error appears at `str()` or `print()`, not when Python first reaches `Card(...)`. !!! note `accent: str` helps your editor and type checker, but it does not reject `accent=123` while your program runs. If values come from a form, an API, or another source you do not control, read [Inputs and validation](/concepts/inputs-and-validation/) to add runtime checks. ## Next steps You now have a Card that: - asks for an accent color; - places text or HTML where `` appears; - lets several Cards use different colors; and - reports an error when required information is missing. You can [open the Card recipe](/examples/card/) to compare the component, complete page, and live result. To continue the guided journey, [use Python data in components](/getting-started/data-in-components/). --- # Use data in components Source: https://citry.dev/getting-started/data-in-components/ # Use data in components Your first Card accepted one color and a place for content. Now you will build a reading list whose heading, books, and item count all come from Python. If you have not built a component yet, start with [Your first component](/getting-started/your-first-component/). ## Build the reading list Save this as `reading_list.py`: ### Reading list with Python data ````citry from citry import Component class ReadingList(Component): class Kwargs: books: list[str] heading: str = "Reading list" empty_message: str = "Your list is empty." show_count: bool = True class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots): return { "books": kwargs.books, "heading": kwargs.heading, "empty_message": kwargs.empty_message, "show_count": kwargs.show_count, "total": len(kwargs.books), } template = """

{{ heading }}

{{ total }} {{ "book" if total == 1 else "books" }}

  • {{ book }}
  • {{ empty_message }}
""" reading_list = ReadingList( heading="Books for the weekend", books=["A Wizard of Earthsea", "Kindred", "Piranesi"], ) if __name__ == "__main__": print(reading_list) reading_list ```` The final `reading_list` line gives **Try live** the value to preview. Python ignores that bare expression when you run the file normally; the `__main__` block prints the same component in your terminal. Run it: ```sh python reading_list.py ``` The result contains this list: ```html

Books for the weekend

3 books

  • A Wizard of Earthsea
  • Kindred
  • Piranesi
``` Citry adds some attributes of its own, so the complete HTML will be a little longer. ## Component inputs `Kwargs` lists the named options people can give your component: ```python class Kwargs: books: list[str] heading: str = "Reading list" empty_message: str = "Your list is empty." show_count: bool = True ``` `books` has no default, so it is required. The other options already have useful values. This works without mentioning them: ```python ReadingList(books=["The Dispossessed"]) ``` It uses “Reading list” as the heading and shows the count. Pass `show_count=False` when you do not want the count: ```python ReadingList( books=["The Dispossessed"], show_count=False, ) ``` ## Template variables A `Kwargs` field is available to the template under the same name. That is why `{{ heading }}` inserts the heading and `books` is ready for the loop. Use [`template_data()`](/reference/component/#citry-component-template-data) when the template needs a new value that Python must work out. Here it adds `total`: ```python def template_data( self, kwargs: Kwargs, slots: Slots, ) -> dict[str, object]: return { "books": kwargs.books, "heading": kwargs.heading, "empty_message": kwargs.empty_message, "show_count": kwargs.show_count, "total": len(kwargs.books), } ``` The method returns every name this template uses. !!! note For a component that only needs its `Kwargs` fields, leave the `template_data()` out and Citry supplies those fields automatically. Like we did in [Your first component](/getting-started/your-first-component#create-the-card). ## Template syntax The template uses four small tools: - `c-if="show_count and total > 0"` leaves the count out when you hide it or when the list is empty. The value can be any Python expression, not only one name. - `{{ "book" if total == 1 else "books" }}` chooses the singular or plural word with a Python expression inside `{{ }}`. - `c-for="book in books"` makes one `
  • ` for every title. - `c-data-count="total"` evaluates `total` and writes an ordinary `data-count` HTML attribute. If `books` is empty, the `c-empty` item appears instead: ```html
  • Your list is empty.
  • ``` The [Control flow](/syntax/control-flow/) and [Dynamic attributes](/syntax/dynamic-attributes/) pages cover the other forms once you need them. ## Input validation If you leave out `books`, Citry tells you that the required option is missing when the component renders: ```python print(ReadingList()) # TypeError: ReadingList.Kwargs.__init__() missing ... 'books' ``` A misspelled option is also rejected: ```python print(ReadingList(books=[], heding="Typo")) # TypeError: ReadingList.Kwargs got unexpected keyword 'heding' ``` The annotations help your editor and type checker, but they do not check the value's type while the program runs. Validate values from forms, APIs, or other untrusted sources before passing them to the component. [Typing and validation](/concepts/inputs-and-validation/) explains the available choices. ## Next steps You now have a component that turns Python data into useful HTML. Next, [build a complete page from components](/getting-started/build-page/) and add a small render test. --- # Build a page from components Source: https://citry.dev/getting-started/build-page/ # Build a page from components Small components become much more useful when you put them together. In this step, you will use two reading lists inside one complete HTML page. Keep `reading_list.py` from [Use data in a component](/getting-started/data-in-components/) in the same folder. ## Create the page Save this as `page.py`: ```citry from citry import Component from reading_list import ReadingList class ReadingPage(Component): class Kwargs: current_books: list[str] next_books: list[str] class Slots: pass template = """ My reading shelf

    My reading shelf

    """ if __name__ == "__main__": page = ReadingPage( current_books=["A Wizard of Earthsea", "Kindred"], next_books=[], ) print(page) ``` Run it: ```sh python page.py ``` The page contains one list with two books and another with the message “Choose your next book.” ## Import components This line matters even though `ReadingList` is not mentioned in the Python code below it: ```python from reading_list import ReadingList ``` Importing the module creates the component class and tells Citry that the `` tag exists. If you remove the import, Citry cannot find that tag when it renders the page. Larger projects can discover component modules automatically. For now, an ordinary import keeps the setup visible. Read [Registration](/concepts/registration/) for tag names and engine ownership, then [Component discovery](/advanced/component-discovery/) for automatic imports and startup. ## Pass component inputs The first list receives two kinds of options: ```citry-html ``` `heading="Reading now"` passes those exact words. The `c-` prefix on `c-books` tells Citry to evaluate `current_books` from the page's Python data and pass the resulting list. Use a plain option for fixed text. Use the `c-` form when the value is a Python expression: ```citry-html ``` The child receives only the values you pass. It cannot silently read other variables from the page around it, which makes the component safe to reuse. ## Add a render check You can test the useful result without matching Citry's generated attributes. Save this as `check_page.py`: ```python from page import ReadingPage html = str( ReadingPage( current_books=["Kindred"], next_books=[], ) ) assert "My reading shelf" in html assert "Kindred" in html assert "Choose your next book." in html print("The page looks right.") ``` Run it: ```sh python check_page.py ``` This check describes what a reader should see, and it does not need another package. It will keep working when an unimportant generated attribute changes. [Testing components](/advanced/testing/) shows how to turn checks like this into pytest tests and add browser or framework coverage for larger projects. ## Next steps You now have a complete page made from smaller pieces, and you have checked its meaningful output. Next, [add flexible content with named areas and useful fallbacks](/getting-started/add-slots/). --- # Add flexible content Source: https://citry.dev/getting-started/add-slots/ # Add flexible content The reading lists on your page accept Python options. Sometimes the person using a component should be able to add whole pieces of HTML instead. You will build a panel with one area for its main content and another for an optional action. If no action is supplied, the panel shows a useful fallback. ## Create the panel Save this as `reading_panel.py`: ### Named slots and fallback content ````citry from citry import Component, SlotInput class ReadingPanel(Component): class Kwargs: title: str class Slots: default: SlotInput footer: SlotInput | None = None template = """

    {{ title }}

    No action needed.
    """ class PanelPage(Component): class Kwargs: pass class Slots: pass template = """

    Kindred

    A Wizard of Earthsea

    """ page = PanelPage() if __name__ == "__main__": print(page) page ```` Run it: ```sh python reading_panel.py ``` The first panel ends with “No action needed.” The second ends with a “Start reading” button. ## Mark slots Inside `ReadingPanel`, each [``](/reference/builtins/#c-slot) marks a place where another template may put content. Here is how what's on the outside gets inserted on the inside: ```citry-html
    ≪≪≪≪≪≪≪≪≪≪≪≪≪≪≪≪≪≪
    | | | |

    Kindred

    ≫≫≫≫≫≫≫≫≫≫≫≫≫≫≫≫≫
    ``` The `

    ` is plain content inside ``. Citry inserts it where `` appears. Because that slot has no name, it is the **default slot**. The footer uses a name to connect its two sides: ```citry-html

    ≪≪≪≪≪≪≪≪≪≪≪ No action needed. | |
    | | | | |

    A Wizard of Earthsea

    |
    | ≫≫≫≫≫≫≫≫≫≫≫
    ``` The matching `name="footer"` values tell Citry where the button belongs. The button replaces “No action needed.” If there is no `footer` fill, that text stays as the fallback. ## Declare accepted slots [`Slots`](/reference/component/#citry-component-slots) gives those two places names in Python: ```python class Slots: default: SlotInput footer: SlotInput | None = None ``` The default slot is required because it has no default value. The footer is optional, so the component can use the fallback from its template. [`SlotInput`](/reference/slots/#citry-slotinput) means the fill may contain rendered HTML, text, another component, or a function that produces content. You do not need to choose one of those forms when you declare the slot. ## Fill the slots When you only fill the default slot, put the content directly inside the component tag: ```citry-html

    Kindred

    ``` When you use a named fill, name every area explicitly, including `default`: ```citry-html

    A Wizard of Earthsea

    ``` Keeping the fills explicit makes it clear which content belongs in each area. Citry reports an error if named fills and loose body content are mixed inside the same component tag. ## Next steps You can now pass Python values through `Kwargs` and pass whole pieces of content through slots. The [Slots guide](/concepts/slots/) goes further into required, scoped, dynamic, and Python-supplied fills. Next, [add behavior that runs immediately in the browser](/getting-started/browser-interactivity/). --- # Add browser behavior Source: https://citry.dev/getting-started/browser-interactivity/ # Add browser behavior Everything you have built so far finishes in Python. Now you will add behavior that responds immediately in the browser, without a request or page reload. Citry uses [Alpine.js](https://alpinejs.dev/){: target="_blank" rel="noopener"} for these small interactions. In this step, Python gives each counter its name, `js_data()` seeds its browser state, and Alpine attributes update the visible count. ## Build independent counters Use [`js_data()`](/reference/component/#citry-component-js-data) to pass initial values from Python directly into the component's Alpine scope. A `$component()` callback is only needed when the component also has JavaScript setup to run. !!! note `js_data()` must return a dictionary that Citry can send as JSON. Use string keys and JSON-serializable values such as strings, finite numbers, booleans, `None`, lists, and nested dictionaries. Convert other Python objects to those types first. Save this example as `click_counters.py`: ### Independent click counters ````citry from citry import Component class ClickCounter(Component): class Kwargs: name: str class Slots: pass def js_data(self, kwargs: Kwargs, slots: Slots): return {"name": kwargs.name, "count": 0} template = """ """ class CounterPage(Component): class Kwargs: pass class Slots: pass template = """ Component data """ page = CounterPage() if __name__ == "__main__": print(page) page ```` Create the page: ```sh python click_counters.py > click_counters.html ``` Open `click_counters.html` in your browser. Both buttons begin at zero. Click Ada's button: Ada changes to one while Grace stays at zero. ## Follow the interaction Two Alpine attributes connect the button to that browser state: - [`@click="count += 1"`](https://alpinejs.dev/directives/on){: target="_blank" rel="noopener"} increases the count when a visitor selects the button. - [`x-text`](https://alpinejs.dev/directives/text){: target="_blank" rel="noopener"} writes the current name and count into their spans. The Alpine attributes tell Citry that this page needs its owned browser runtime. You do not need a separate JavaScript entry file or Alpine setup. ## Use JS data directly in Alpine Python sends the browser value through `js_data()`: ```python def js_data(self, kwargs: Kwargs, slots: Slots): return {"name": kwargs.name, "count": 0} ``` Citry seeds both top-level keys into the component's reactive Alpine scope, so the template can use `name` and `count` directly. Each rendered component gets a fresh nested value graph, even when identical JSON is sent only once. Add `$component` later when the component needs JavaScript setup beyond data seeding. Its `data` argument receives the same instance-local snapshot, and its `scope` is already seeded before the callback runs: ```js $component(({ data, scope }) => { console.log(data.name, scope.name); scope.reset = () => { scope.count = 0; }; }); ``` Ada and Grace each receive their own scope, so one click cannot change the other counter. The [Alpine runtime](/advanced/alpine-runtime/) page covers plugins, lifecycle, security policy, and advanced configuration. The [Client interactivity](/concepts/client-interactivity/) page covers everything available inside `$component`. Keep [Alpine in templates](/syntax/alpine/) nearby as a concise syntax guide. ## Next steps You can now combine Alpine attributes with Python-provided browser data. Next, [connect a parent and child component in the browser](/getting-started/client-props-and-handlers/). --- # Connect components in the browser Source: https://citry.dev/getting-started/client-props-and-handlers/ # Connect components in the browser A parent component can pass Python values to a child while it renders. After being rendered and loaded in the browser, components can pass JavaScript values in the client. You will build a choice button whose label follows its parent. Clicking the child button will change the parent's choice **in the browser**, and the new label will flow back down to the child. Start with [Add browser behavior](/getting-started/browser-interactivity/) if you have not used `js_data()` with Alpine in Citry yet. This chapter introduces [`$component`](/reference/browser-apis/#component) because the child needs to declare reactive client props and run setup logic. Components that only expose `js_data()` values to their own Alpine expressions do not need it. ## Build the parent and child Save this as `connected_components.py`: ### Reactive parent and child components ````citry from citry import Component class ChoiceButton(Component): class Kwargs: pass class Slots: pass template = """ {# 'x-text' shows current value, as given by parent #} """ js = """ $component({ // Declare the browser value ChoiceButton accepts // through '$c-props'. props: { label: { type: String, required: true }, }, init: ({ props, scope }) => { // Share the reactive prop with Alpine. scope.clientProps = props; }, }); """ class ChoicePicker(Component): class Kwargs: pass class Slots: pass template = """ {# 'choice' lives in the parent's 'x-data' #}

    Current choice:

    {# `$c-props` passes 'choice' down as browser data. #} {# `@click` allows parent to react to child's click. #}
    """ class ChoicePage(Component): class Kwargs: pass class Slots: pass template = """ Connect components """ page = ChoicePage() if __name__ == "__main__": print(page) page ```` Create the page and open it: ```sh python connected_components.py > connected_components.html ``` The parent and button both start with “Ocean.” Click the button and they both change to “Forest.” Click again and they return to “Ocean.” ## Child client inputs [`ChoiceButton.js`](/reference/component/#citry-component-js) declares one browser prop called `label`: ```js $component({ props: { label: { type: String, required: true }, }, init: ({ props, scope }) => { scope.clientProps = props; }, }); ``` Citry checks that the parent supplies a string. The `props` object stays [reactive](https://alpinejs.dev/advanced/reactivity){: target="_blank" rel="noopener"}, so the span can keep reading `clientProps.label` after the first render. This is separate from `Kwargs`. A `Kwargs` value comes from Python while Citry renders HTML. A client prop comes from browser state and can change without a new Python render. ## Pass a reactive value down The parent supplies the current choice with `$c-props`: ```citry-html ``` The expression runs where the parent wrote it, so `choice` refers to the parent's Alpine data. Whenever `choice` changes, Citry updates the child's `label` prop. Use `$c-props` for values that must remain reactive in the browser. Use an ordinary option such as `label="Ocean"` or a dynamic Python option such as `c-label="python_choice"` when the value belongs to the server render. ## Handle the child's click The click handler also sits on the child component tag: ```citry-html ``` The browser listens on the real button rendered by `ChoiceButton`, but the expression changes the parent component's `choice`. This lets a reusable child announce an interaction without needing to know what its parent will do next. The [Client interactivity](/concepts/client-interactivity/) guide covers multiple roots, slots, handler modifiers, and the complete browser-scope rules. ## Next steps So far every interaction has stayed in the browser. Next, [serve the page with FastAPI](/getting-started/fastapi/) so a later click can reach a Python handler. --- # Serve pages with FastAPI Source: https://citry.dev/getting-started/fastapi/ # Serve pages with FastAPI The components you have built so far can render without a web server. Now you will put the choice picker from the last step behind [FastAPI](https://fastapi.tiangolo.com/){: target="_blank" rel="noopener"}. Its browser behavior will keep working, and Citry will gain a place to receive the [server events](/events/) in the next steps. This tutorial uses FastAPI to keep the setup concrete. Citry comes with integrations for: - [FastAPI / Starlette](/web-frameworks/#fastapi-and-starlette) - [Django](/web-frameworks/#django) - [Flask](/web-frameworks/#flask) - Other [ASGI or WSGI applications](/web-frameworks/#bare-asgi-and-wsgi). You can switch to your framework after you finish this journey. ## Install the packages Inside an existing `uv` project, add [FastAPI](https://fastapi.tiangolo.com/){: target="_blank" rel="noopener"} and [Uvicorn](https://uvicorn.dev/){: target="_blank" rel="noopener"}: ```sh uv add fastapi uvicorn ``` Or, with pip: ```sh python -m pip install fastapi uvicorn ``` FastAPI provides the application and its routes. Uvicorn runs that application as a local web server. ## Create Citry instance Create a new folder for this small app. Inside it, save the following as `citry_setup.py`: ```citry import os from citry import Citry # New in this step: create one Citry instance for the whole app. secret = os.environ.get("CITRY_SECRET") if not secret: msg = "Set CITRY_SECRET before starting the app." raise RuntimeError(msg) citry_app = Citry(secret=secret) ``` The other files in this small app import the same [`Citry`](/reference/citry/#citry-citry) instance. That keeps its components, rendered pages, and mounted browser routes together. The secret lets Citry detect changes to data that travels through the browser. You will use that feature when you add [`State`](/reference/component/#citry-component-state). !!! warning Keep the secret outside of your source code so it does not end up in version control. Create a random development secret in your current terminal: ```sh export CITRY_SECRET="$( python -c 'import secrets; print(secrets.token_urlsafe(32))' )" ``` In PowerShell, use: ```powershell $env:CITRY_SECRET = python -c "import secrets; print(secrets.token_urlsafe(32))" ``` Use a stable secret from your deployment's secret store in production. All workers for the same app need the same value. ## Create the page Save this next file as `components.py`: Look for the `New in this step` comments. Compared with the browser-only version, this file now: - imports the shared `citry_app` and assigns it to every component; and - exposes `TutorialPage`, the full page that the FastAPI route will render. ```citry # New in this step: every component uses the app's Citry instance. from citry_setup import citry_app from citry import Component class ChoiceButton(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ """ js = """ $component({ props: { label: { type: String, required: true }, }, init: ({ props, scope }) => { scope.clientProps = props; }, }); """ class ChoicePicker(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """

    Current choice:

    """ # New in this step: FastAPI will return this complete page. class TutorialPage(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ Reading room

    Reading room

    """ ``` ## Connect components with Citry The complete file above repeats one new line on every component. Here is the same change with the unchanged parts folded away: ```citry class ChoiceButton(Component): citry = citry_app ... class ChoicePicker(Component): citry = citry_app ... class TutorialPage(Component): citry = citry_app ... ``` [`Component.citry`](/reference/component/#citry-component-citry) tells each class which Citry instance owns it. `ChoiceButton` and `ChoicePicker` keep the browser behavior you already built. `TutorialPage` gives the FastAPI route one complete page to return. ## Create the FastAPI app Save this as `app.py` beside the other two files: This whole file is new. Its `New in this step` comments mark the three connections between FastAPI and Citry. ```citry from collections.abc import AsyncIterator from contextlib import asynccontextmanager from citry_setup import citry_app from components import TutorialPage from fastapi import FastAPI from fastapi.responses import HTMLResponse from citry.contrib.fastapi import mount # New in this step: initialize Citry before serving requests. @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncIterator[None]: citry_app.initialize() yield app = FastAPI(lifespan=lifespan) # New in this step: render a Citry page from a regular route. @app.get("/") def home() -> HTMLResponse: page = str(TutorialPage()) return HTMLResponse(page) # New in this step: add Citry's browser and event routes. mount(app, citry_app) ``` ## Load the components The two local imports in the file above connect the page to the same Citry instance: ```python from citry_setup import citry_app from components import TutorialPage ``` Importing `TutorialPage` runs the class definitions in `components.py`. Those classes register with `citry_app`, so they are present before the application starts serving requests. ## Initialize Citry when FastAPI starts FastAPI calls the [lifespan](https://fastapi.tiangolo.com/advanced/events/){: target="_blank" rel="noopener"} function once when the application starts: ```python @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncIterator[None]: citry_app.initialize() yield app = FastAPI(lifespan=lifespan) ``` [`citry_app.initialize()`](/reference/citry/#citry-citry-initialize) prepares the registered components before requests can arrive. The `yield` hands control back to FastAPI so it can run the application. ## Define FastAPI endpoint `home` is a regular FastAPI route. Inside it, we build `TutorialPage`, render it to a string, and returns that string as HTML: ```python @app.get("/") def home() -> HTMLResponse: page = str(TutorialPage()) return HTMLResponse(page) ``` A larger application can render a Citry page from its existing routes in the same way. `HTMLResponse` tells FastAPI and the browser that the returned string is an HTML document. Returning the plain string directly would make FastAPI encode it as a JSON string. ## Mount Citry's routes The final line gives Citry its own routes inside the FastAPI application: ```python mount(app, citry_app) ``` [`mount(app, citry_app)`](/reference/contrib/#citry-contrib-fastapi-mount) uses `/citry` by default. The rendered page uses those routes to load Citry's browser code. Passing the same `citry_app` keeps those routes connected to the components you initialized. In the next lessons will use these routes to reach components' server-side event handlers. ## Start the app Run Uvicorn from the folder containing the three files: ```sh uv run uvicorn app:app --reload ``` In `app:app`, the first `app` names `app.py` and the second names the FastAPI object inside that file. `--reload` restarts the local server when you save a change. If you installed with pip, run: ```sh python -m uvicorn app:app --reload ``` Visit `http://127.0.0.1:8000/`. You should see the choice picker inside the “Reading room” page. Click its button and watch “Ocean” change to “Forest,” just as it did without a server. You can also visit `http://127.0.0.1:8000/citry/citry.js`. Seeing JavaScript at that address confirms that the Citry routes are mounted. The [Web frameworks](/web-frameworks/) guide shows the matching setup for Django, Flask, Starlette, and bare ASGI or WSGI apps. ## Next steps The page and Citry now share one running server. Next, [call Python from a click](/getting-started/call-python/). --- # Call Python from a click Source: https://citry.dev/getting-started/call-python/ # Call Python from a click Your FastAPI app can render the page. Now you will add a button that reaches a Python handler through Citry's mounted routes. Python will load the choice picker's options without reloading the page. Start with [Serve the page with FastAPI](/getting-started/fastapi/) if the app is not already running. ## Build the page Replace `components.py` with this version: Look for the `New in this step` comments. This version: - adds the database stand-in `load_choices_from_database()` - adds the Python handler `ChoicePicker.Events.load_choices()` - changes the picker so it starts empty and fills itself from the handler's browser event. ```citry from citry_setup import citry_app from citry import Component from citry.ext.events import actions # New in this step: stand in for a database query. def load_choices_from_database() -> list[str]: return ["Ocean", "Forest"] class ChoiceButton(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ """ js = """ $component({ props: { label: { type: String, required: true }, }, init: ({ props, scope }) => { scope.clientProps = props; }, }); """ class ChoicePicker(Component): citry = citry_app class Kwargs: pass class Slots: pass # New in this step: handle the button click in Python. class Events: # Method name matches @c-click. def load_choices(self): choices = load_choices_from_database() # Tell the client to dispatch a custom browser event # with the loaded choices. return actions.Dispatch( "choice-picker:loaded", {"choices": choices}, ) template = """
    {# New in this step: ask Python for the choices. #} Loading...

    No choices loaded yet.

    Current choice:

    {# New in this step: cycle through the loaded choices. #}
    """ class TutorialPage(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ Reading room

    Reading room

    """ ``` Keep `citry_setup.py` and `app.py` unchanged. Uvicorn should reload the app after you save the file. Open `http://127.0.0.1:8000/` and click “Load choices.” The empty picker fills with the two choices. Its existing child button can then move between them in the browser. ## Send the click to Python This is the line that turns an ordinary button into a server-handled action: ```citry-html ``` The Alpine `@click` from the previous lesson runs JavaScript in the browser. Citry uses the `@c-*` prefix for events that are meant to be [handled by the server](/events/bindings/). When you click on the button with `@c-click`: 1. Alpine detects the `click` event and hands it over to Citry JS client. 2. Citry JS client sends request to the server (the FastAPI app). The payload includes which component (`ChoicePicker`) and which event was triggered (`load_choices`). 3. On the server, the routes that were installed when we mounted Citry onto FastAPI will pick up this request (`/citry/...`). 4. Citry's routing passes the request and event data to `load_choices()` event handler on the `ChoicePicker` component. Because the page and Citry routes share the application, this request stays on the same origin. !!! warning **DO NOT** blindly trust data in event handlers - anyone can send events. Your application still needs to authenticate people and check their permissions inside handlers. See [Security](/security/) for the complete trust boundaries. ## Server event handlers The handler lives in the component's nested [`Events`](/reference/component/#citry-component-events) class: ```python class Events: def load_choices(self): choices = load_choices_from_database() return actions.Dispatch( "choice-picker:loaded", {"choices": choices}, ) ``` Public methods in `Events` are handlers the browser can call. Here, `load_choices` matches the name on the button. The small function above the components stands in for your application's data access: ```python def load_choices_from_database() -> list[str]: return ["Ocean", "Forest"] ``` The page starts without those choices. This function runs only after the button calls the handler. In a real application, this is where you might query a database or call another Python service. ## Handler response The handler returns an [`actions.Dispatch(...)`](/reference/events/#citry-ext-events-actions-dispatch) action: ```python return actions.Dispatch( "choice-picker:loaded", {"choices": choices}, ) ``` This tells the browser to [fire an event](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events){: target="_blank" rel="noopener"} named `choice-picker:loaded`. The second argument becomes that event's [`detail`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail){: target="_blank" rel="noopener"}, so the returned list is available as `$event.detail.choices`. Values in `detail` must be JSON-serializable because they travel from Python to browser code. !!! note `@c-click` is a convenience over [`sendEvent()`](/reference/browser-apis/#send-event): as such, it starts the handler but does not expose the handler's Promise result. That is why this example returns [`Dispatch`](/reference/events/#citry-ext-events-actions-dispatch) (to trigger a browser event) rather than [`actions.Data`](/reference/events/#citry-ext-events-actions-data) which would have returned data from a Promise. Component JavaScript can use `sendEvent()` when it owns the call and needs Data as a one-off return value. The picker defines a small Alpine method for storing the result: ```javascript loadChoices(newChoices) { this.choices = newChoices; this.choice = newChoices[0]; } ``` Its listener calls that method when the matching event arrives: ```citry-html @choice-picker:loaded="loadChoices($event.detail.choices)" ``` The existing `$c-props` connection then passes that selected label to `ChoiceButton`. No HTML needs to be replaced. !!! note When the server returns `Dispatch` action, the browser fires a bubbling DOM event from that component's first live root. Here, the first root is the same element that carries `@choice-picker:loaded`, so the listener receives the event. Placement matters for an ordinary `@event` listener. The first root or one of its ancestors can hear the bubbling event. A descendant **CANNOT**, because DOM events do not bubble downward. If a component has several roots, a listener on another root will not hear it either. For a multi-root component, prefer [`$onEvent`](/reference/browser-apis/#on-event) in an Alpine expression or the `onEvent` function in [`$component`](/reference/browser-apis/#component). Both listen for server events targeting the current component instance. Their callbacks receive `detail` directly, without `$event.detail`. See [event actions](/events/actions/#choose-where-to-listen-for-dispatch) for the complete delivery rules. ## Loading state The full button also uses the Citry-specific Alpine magic [`$loading('load_choices')`](/reference/browser-apis/#loading): ```citry-html Loading... ``` `$loading('load_choices')` is true while that handler call is in progress. During a slower real query, the button becomes disabled and the loading message appears. Both return to normal when the call finishes. ## Process response After Python supplies the list, another Alpine method finds the next choice: ```javascript setNextChoice() { const choices = this.choices; const oldChoiceIndex = choices.indexOf(this.choice); const nextChoiceIndex = (oldChoiceIndex + 1) % choices.length; this.choice = choices[nextChoiceIndex]; } ``` The child button calls that method with an ordinary browser click: ```citry-html ``` This uses Alpine's `@click`, so moving from “Ocean” to “Forest” does not make another server call. The page now uses Python when it needs data and browser code when the interaction can stay local. ## Each server call starts fresh This first handler is stateless. Each click calls `load_choices_from_database()` again and returns the same set. Python does not remember that the page loaded it before. Alpine can still move between those choices in the browser. Here, stateless means the component has no Citry State carried from one call to the next. A real handler can still read persistent application data from a database, session, or another service. That makes this pattern a good fit for refreshing a result or asking the server for the latest version of some data. The next lesson adds State so Python can continue from an earlier call. Continue with [event bindings](/events/bindings/) for loading and errors, or [event actions](/events/actions/) for the other ways a handler can update the page. ## Next steps The browser can now reach Python and receive a result. Next, [keep a value between calls](/getting-started/state/). --- # Events state Source: https://citry.dev/getting-started/state/ # Events state The last handler ran the same database query on every click. This time Python will remember server-side state across calls, so we can count how many times we called the endpoint. Continue from [Call Python from a click](/getting-started/call-python/). Keep `citry_setup.py` and `app.py` unchanged. ## Add server-side state Replace `components.py` with this version: The `New in this step` comments point to four changes: - `load_choices_from_database` returns one of the two choice batches - a starting counter `Kwargs.batches_loaded` - the [`State`](/reference/component/#citry-component-state) declaration and stateful handler - the count shown in the browser ```citry from citry_setup import citry_app from citry import Component from citry.ext.events import actions # New in this step: simulate two database query results. # Based on value from State, we return one of these two # batches of choices. CHOICE_BATCHES = ( ("Ocean", "Forest"), ("History", "Science"), ) def load_choices_from_database(batch: int) -> list[str]: choices = CHOICE_BATCHES[batch % len(CHOICE_BATCHES)] return list(choices) class ChoiceButton(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ """ js = """ $component({ props: { label: { type: String, required: true }, }, init: ({ props, scope }) => { scope.clientProps = props; }, }); """ class ChoicePicker(Component): citry = citry_app # New in this step: set the counter for the first render. class Kwargs: batches_loaded: int = 0 class Slots: pass # New in this step: carry the counter between Python calls. class State: batches_loaded: int = 0 class Events: def load_choices(self, state): # New in this step: read and advance the signed State. # NOTE: State is passed to the event handler. # You can mutate the State, and it will be # signed and sent back to the client. choices = load_choices_from_database(state.batches_loaded) state.batches_loaded += 1 return actions.Dispatch( "choice-picker:loaded", { "choices": choices, "batches_loaded": state.batches_loaded, }, ) template = """ {# New in this step: pass Python `batches_loaded` to JS. #} {# `c-x-data` sets the first browser value. #} {# The listener applies later values returned by Python. #}
    Loading... {# New in this step: show the counter from Python. #}

    Sets loaded: {{ batches_loaded }}

    No choices loaded yet.

    Current choice:

    """ class TutorialPage(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ Reading room

    Reading room

    """ ``` Open `http://127.0.0.1:8000/` and click “Load choices” twice. The first call loads “Ocean” and “Forest.” The second loads “History” and “Science,” while “Sets loaded” moves from one to two. Reload the whole page and the sequence starts over. ## Declare State The server needs one value from the previous call: how many choice sets have already been loaded. Use [`State`](/reference/component/#citry-component-state) to store that info: ```python class Kwargs: batches_loaded: int = 0 class State: batches_loaded: int = 0 ``` [`Kwargs`](/reference/component/#citry-component-kwargs) and [`State`](/reference/component/#citry-component-state) answer two different questions: - `Kwargs` - Component input when rendered as `Comp(...)` or `` - `State` - Data private to event handlers preserved across calls. `Kwargs.batches_loaded` gives the first render its counter. The same-named `State.batches_loaded` gives the first Python call its counter and says that the value must come back on later calls. How Citry builds the initial State: 1. Pass kwargs to state with matching names, so `Kwargs.batches_loaded -> State.batches_loaded`. 2. Uses State defaults for any gaps. 3. Remaining unfilled fields raise error. With ``, both declarations use their own `0` default. If a caller passes `batches_loaded=3`, that value starts both the render and its State. The following would fail, because `other_field` has no default and is not on `Kwargs`: ```python class Kwargs: batches_loaded: int = 0 class State: batches_loaded: int = 0 other_field: str ``` Defining [`state_data()`](/events/state/#choose-what-survives-in-state) replaces this automatic step. It receives the resolved kwargs and slots, then returns the initial State as a `State` instance or a dictionary. Use it when State needs a renamed or transformed value, or a small value derived from a richer input. `ChoicePicker` does not need it because its names already match. This would be a valid way of manually constructing `State`: ```python class Kwargs: resume_id: int class State: batches_loaded: int def state_data(self, kwargs: Kwargs, slots): batches_loaded = resume_batches_from_db(kwargs.resume_id) return {"batches_loaded": batches_loaded or 0} ``` Coming back to ``, every kwarg is also State. When the two shapes are the same, you can inherit the fields and their defaults instead of repeating them: ```python class Kwargs: batches_loaded: int = 0 class State(Kwargs): pass ``` The following lessons use this shorthand. Keep the declarations separate when some render inputs should not travel through the browser. The choices themselves do not need to be in State. Python can load them again, and Alpine already holds the selected choice for the browser interaction. ## State decides server behavior The example now has two possible database results: ```python CHOICE_BATCHES = ( ("Ocean", "Forest"), ("History", "Science"), ) def load_choices_from_database(batch: int) -> list[str]: choices = CHOICE_BATCHES[batch % len(CHOICE_BATCHES)] return list(choices) ``` Batch zero returns “Ocean” and “Forest.” Batch one returns “History” and “Science.” The `% len(CHOICE_BATCHES)` part wraps back to the first set after the last one. The handler reads the current counter, loads that batch, and then advances the counter for next time: ```python class Events: def load_choices(self, state): choices = load_choices_from_database( state.batches_loaded ) state.batches_loaded += 1 return actions.Dispatch( "choice-picker:loaded", { "choices": choices, "batches_loaded": state.batches_loaded, }, ) ``` On the first click, the handler: - receives `state.batches_loaded=0` - loads batch zero - sets `state.batches_loaded=1` Citry then sends the updated State back with the response. On the second click, the handler: - receives `state.batches_loaded=1` - loads batch one - sets `state.batches_loaded=2` The dispatched event also includes the new count because the page displays it. State carries the value to the next Python call; the event payload makes the value available to Alpine right now. ## Update the browser values The picker starts its Alpine data from the value Python rendered: ```citry-html
    ...
    ``` The `c-` prefix makes `x-data` a [dynamic attribute](/syntax/dynamic-attributes/). Citry evaluates the Python expression and writes an ordinary Alpine `x-data` attribute. A picker created with another starting count will therefore show that count in both places. When `choice-picker:loaded` arrives, its listener updates all three browser values: ```citry-html @choice-picker:loaded=" choices = $event.detail.choices; choice = choices[0]; batchesLoaded = $event.detail.batches_loaded; " ``` The new list and selected choice stay in Alpine. The counter is named `batches_loaded` in Python and `batchesLoaded` in Alpine, following each language's usual style: ```text batchesLoaded = $event.detail.batches_loaded; ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ Alpine var data from Python ``` The `batches_loaded` counter travels in State because Python needs it to choose the next batch. Reloading the page creates a new picker at its starting value of zero. ## State secrets Signed State lets the server detect whether its browser-carried value was changed. It does not hide the value, sign a person in, or decide what that person may do. !!! warning **DO NOT** put passwords, API keys, or other secrets in State. Check permissions inside each event handler just as you would in an ordinary web route. In production, keep `CITRY_SECRET` stable and give every worker the same value so they can read one another's signed State. ## Next steps State is useful for values a component carries from one call to the next. Next, [handle and validate forms](/getting-started/forms/) whose values come from named browser controls. --- # Handle and validate forms Source: https://citry.dev/getting-started/forms/ # Handle and validate forms Buttons are only one way to call Python. A Citry event can also receive the named values from a form. You will build an email form, reject the wrong domain in Python, and show the field error beside the input without clearing what the reader typed. Continue from [Keep a value between calls](/getting-started/state/). Keep `citry_setup.py` and `app.py` unchanged. ## Add the form Replace `components.py` with this version. Here we replace the ChoicePicker with a sign-in form: ```citry from citry import Component from citry.ext.events import EventError, actions from citry_setup import citry_app # New in this step: describe the named values sent by the form. class SignupIn: email: str # New in this step: Sign up form with server-side validation class SignupForm(Component): citry = citry_app class Kwargs: pass class Slots: pass class Events: # Validate the form and return field errors def submit(self, data: SignupIn): email = data.email.strip() if not email.endswith("@example.com"): raise EventError( "Please fix the email address.", fields={"email": "Use an @example.com address."}, ) return actions.Dispatch( "signup:sent", {"email": email}, ) template = """ """ class TutorialPage(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ Join the reading room

    Join the reading room

    {# New in this step: place the form on the page. #}
    """ ``` Open `http://127.0.0.1:8000/` and enter `ada@elsewhere.test`. The address is valid enough for the browser, so the form reaches Python. Citry then shows “Use an `@example.com` address.” below the input. Change the value to `ada@example.com` and submit again. The page reports that the address was accepted. ## Submit form to event handler The form calls the `submit` Python event handler instead of performing the browser's usual full-page submission: ```citry-html
    ...
    ``` The `.prevent` modifier stops the usual page navigation. Citry collects the form's named controls and sends them to Python. Here, `name="email"` gives the typed value its field name. The browser checks that the input looks like an email address and is not empty. The application-specific `@example.com` rule still belongs in Python. ## Declare form data On the server, `SignupIn` names the fields expected by the handler: ```python class SignupIn: email: str class Events: def submit(self, data: SignupIn): email = data.email.strip() ``` The input's `name="email"` matches `SignupIn.email`, so the handler can read `data.email`. A larger form can add more named controls and matching fields to the input class. The Python type hint `SignupIn` describes the expected input shape. The [Forms guide](/events/forms/) covers larger input shapes and validation patterns. ## Reject input in Python The handler checks the cleaned address and raises [`EventError`](/reference/events/#citry-ext-events-eventerror) when the domain is wrong: ```python if not email.endswith("@example.com"): raise EventError( "Please fix the email address.", fields={"email": "Use an @example.com address."}, ) ``` The first string is the overall error message, available as [`$error('submit')?.message`](/reference/browser-apis/#error) if you want a message for the whole form. The [`fields`](/reference/events/#citry-ext-events-eventerror-fields) mapping adds messages for specific inputs. Its `email` key matches both `SignupIn.email` and the input's `name="email"`. ## Show handler error in UI The span beside the input reads that field message from `$error('submit')`: ```citry-html ``` Before an error occurs, `$error('submit')` returns `null` and the span stays hidden. After the failed call, `fieldErrors.email` contains `"Use an @example.com address."` The form itself remains in place, so the input keeps the address that needs fixing. Naming the handler matters when one component contains several forms: a successful call clears only that handler's error. ## Show handler loading in UI The submit button reads the same handler name through [`$loading('submit')`](/reference/browser-apis/#loading): ```citry-html ``` While `submit` is running, the button is disabled and its label changes to “Sending.” This prevents an accidental second submission and tells the person that the first one is still being handled. ## Handle success in the browser A valid address returns another browser event: ```python return actions.Dispatch( "signup:sent", {"email": email}, ) ``` The form's root listens for that event and keeps the returned address in Alpine: ```citry-html
    ...

    Accepted .

    ``` The listener is on the `SignupForm` root on purpose. [`Dispatch`](/reference/events/#citry-ext-events-actions-dispatch) fires the bubbling event from that first root, so the root receives it. Moving `@signup:sent` inside the `
    ` would not work because the event does not bubble down into descendants. When root placement is awkward, use [`$onEvent`](/reference/browser-apis/#on-event) or the `onEvent` member from [`$component`](/reference/browser-apis/#component) to listen by component instance instead. The [event actions guide](/events/actions/#choose-where-to-listen-for-dispatch) explains the complete targeting rules. Using `$component` would have looked like this: ```js $component(({ onEvent, scope }) => { // Set initial Alpine state, replaces root x-data scope.acceptedEmail = ''; // Update Alpine state on server event onEvent('signup:sent', (detail) => { scope.acceptedEmail = detail.email; }); }); ``` The success path updates Alpine data, so it does not need new HTML from Python. The next lesson will keep the same form and change only that success result. ## Next steps An event does not have to stop at an error or browser event. Next, [replace part of the page from Python](/getting-started/server-rendered-updates/). --- # Update page from Python Source: https://citry.dev/getting-started/server-rendered-updates/ # Update page from Python The form currently reports success by changing a line of text in the browser. For the final step, Python will render a new `Confirmation` component into the result area. The form itself will stay where it is. Only the chosen part of the page will change. Continue from [Handle and validate a form](/getting-started/forms/). Keep `citry_setup.py` and `app.py` unchanged. ## Return a rendered update Replace `components.py` with the finished version: The file stays focused on the form. The `New in this step` comments mark the complete change: - a new `Confirmation` component - form's new success action and result area - two places where the page collects component CSS and JavaScript ```citry from citry import Component from citry.ext.events import EventError, actions from citry_setup import citry_app # New in this step: render this component after a valid form. class Confirmation(Component): citry = citry_app class Kwargs: email: str class Slots: pass def js_data(self, kwargs: Kwargs, slots: Slots): return {"email": kwargs.email} template = """
    Request received

    We will write to {{ email }}.

    Preparing confirmation...

    """ css = """ .confirmation { border: 2px solid #2f855a; border-radius: 0.5rem; padding: 1rem; } """ class SignupIn: email: str class SignupForm(Component): citry = citry_app class Kwargs: pass class Slots: pass class Events: def submit(self, data: SignupIn): email = data.email.strip() if not email.endswith("@example.com"): raise EventError( "Please fix the email address.", fields={"email": "Use an @example.com address."}, ) # New in this step: replace the result area's contents. return actions.Render( Confirmation(email=email), target="#signup-result", swap="inner", ) template = """
    """ class TutorialPage(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ Join the reading room {# New in this step: place collected component CSS. #}

    Join the reading room

    {# New in this step: place collected component JS. #} """ ``` Open `http://127.0.0.1:8000/`. Try `ada@elsewhere.test` once more to confirm that the field error still works. Then submit `ada@example.com`. The placeholder inside the result area becomes a bordered confirmation. Its status changes to “Confirmation ready for ada@example.com.” The email form remains above it. ## Confirmation fragment `Confirmation` is an ordinary component. Focusing only on its input and visible HTML, it looks like this: ```citry class Confirmation(Component): class Kwargs: email: str template = """
    Request received

    We will write to {{ email }}.

    Preparing confirmation...

    """ ``` The complete class above also includes its Citry instance, `Slots`, browser data, and CSS. This smaller excerpt shows the first connection: `Confirmation(email=email)` supplies the value that `{{ email }}` inserts. ## Render action The successful handler now returns an [`actions.Render`](/reference/events/#citry-ext-events-actions-render) action: ```python return actions.Render( Confirmation(email=email), target="#signup-result", swap="inner", ) ``` `Confirmation(email=email)` builds a fresh component for this response. Citry renders it as a browser update, including the information needed for that component's CSS and browser data. The validation path still raises the same `EventError`; only a successful submission reaches this return statement. ## Swap target selector The form contains a stable result area: ```citry-html

    Your confirmation will appear here.

    ``` [`target="#signup-result"`](/reference/events/#citry-ext-events-actions-render-target) points to that element with a CSS selector. [`swap="inner"`](/reference/events/#citry-ext-events-actions-render-swap) replaces its contents while keeping the `
    ` itself. The form, input, and button remain where they are. `aria-live="polite"` also lets assistive technology announce the new confirmation without moving keyboard focus. Use a selector that identifies the intended area without matching unrelated elements. ## Confirmation browser data The confirmation sends its email address to browser code through [`js_data()`](/reference/component/#citry-component-js-data): ```python def js_data(self, kwargs: Kwargs, slots: Slots): return {"email": kwargs.email} ``` Citry seeds the top-level `email` key directly into this component's Alpine scope. The template can use it without a `$component` callback: ```citry-html

    Preparing confirmation...

    ``` When the new component starts, Alpine replaces “Preparing confirmation...” with the finished status. Use `$component` only when a component also needs imperative JavaScript setup, client prop declarations, effects, or cleanup. !!! note The value returned by `js_data()` must be JSON-serializable. ## Confirmation CSS `Confirmation` also owns the border that makes the new result visible: ```css .confirmation { border: 2px solid #2f855a; border-radius: 0.5rem; padding: 1rem; } ``` ## Asset and data loading order There are two steps when component assets and browser data reach the page: ### 1. First response - Full page When you first loaded the entire page, `TutorialPage`, Citry automatically collected its CSS and JavaScript assets and inserted them into the HTML. You can customize where to insert the assets with [``](/reference/builtins/#c-css) and [``](/reference/builtins/#c-js). Adding the tags makes the positions explicit. Read more about [asset placement](/advanced/asset-placement/). ```citry-html ... ... ``` ### 2. Second response - Fragment `Confirmation` is created later. Its CSS and browser data arrive in the **second** response when the server responds to the form submission, so it never sees the `` and `` tags from the first render. The second response inserts `Confirmation` into the page. Citry collects the inserted fragment's assets, loads what is still missing, and seeds its Alpine scope before the new component starts. That is why the green border appears and the status changes after the confirmation enters the page. See [Event actions](/events/actions/) for the other results a handler can return, and [HTML fragments](/advanced/html-fragments/) for a deeper look at rendering and inserting partial HTML. ## Next steps You now know how to build a single page with a view events. Next, let's build a [CRUD admin table](/getting-started/build-crud-pages/) to learn how to manage a page with tens to hundreds of components. --- # Build CRUD pages Source: https://citry.dev/getting-started/build-crud-pages/ # Build CRUD pages Imagine a CRUD admin table view. Each row is one record, and each row has buttons for editing or deleting the row. Each row also has its own loading, errors, and success message. Meanwhile, the controls above and below the table may need to move together. You will build that shape in three layers: - one `TaskRow` instance per task; - one `onEvent` for each row's result; - one `TaskList` that renders `TaskRow` and manages the list controls. Continue from [Update page from Python](/getting-started/server-rendered-updates/). Keep `citry_setup.py` and `app.py` unchanged. ## Build the page Replace `components.py` with this version: ```citry from dataclasses import dataclass from citry import Component from citry.ext.events import EventError, actions from citry_setup import citry_app @dataclass class Task: id: int title: str completed: bool = False # This represents the "database" of tasks. # In a real app, this would be stored in a database. TASKS = [ Task(id=1, title="Review the draft", completed=True), Task(id=2, title="Send the invitation"), Task(id=3, title="Publish the notes"), ] def load_tasks(*, hide_completed: bool = False) -> list[Task]: if hide_completed: return [task for task in TASKS if not task.completed] return TASKS class RenameTaskIn: title: str class TaskRow(Component): citry = citry_app class Kwargs: task_id: int title: str class Slots: pass # Remember the task ID in State so we don't have # to send it with each event. class State: task_id: int class Events: # Update the task title in TASKS. # Return a message to display in the UI. def save(self, data: RenameTaskIn, state: "TaskRow.State"): title = data.title.strip() if len(title) < 3: raise EventError( "Give the task a longer title.", fields={"title": "Use at least three characters."}, ) # Perform a "database" update. for task in TASKS: if task.id == state.task_id: task.title = title break return actions.Dispatch( "TaskRow:saved", {"taskId": state.task_id, "title": title}, ) def template_data(self, kwargs: Kwargs, slots: Slots): return { "task_id": kwargs.task_id, "title": kwargs.title, } template = """
  • """ js = """ // Display a message when this row's task title // is successfully saved. $component(({ onEvent, scope }) => { scope.saveStatus = ''; onEvent('TaskRow:saved', (detail) => { scope.saveStatus = `Saved task ${detail.taskId}: ${detail.title}`; }); }); """ class TaskRows(Component): citry = citry_app class Kwargs: tasks: list[Task] class Slots: pass def template_data(self, kwargs, slots): return {"tasks": kwargs.tasks} template = """ """ class FilterTasksIn: hide_completed: bool class TaskFilterToggle(Component): citry = citry_app class Kwargs: pass class Slots: pass template = """ """ js = """ $component({ props: { hideCompleted: { type: Boolean, required: true }, loading: { type: Boolean, required: true }, }, init: ({ props, scope }) => { scope.clientProps = props; }, }); """ class TaskList(Component): citry = citry_app class Kwargs: tasks: list[Task] class Slots: pass class Events: def filter_tasks(self, data: FilterTasksIn): visible_tasks = load_tasks( hide_completed=data.hide_completed, ) return [ actions.Dispatch( "TaskList:filter-changed", {"hideCompleted": data.hide_completed}, ), actions.Render( TaskRows(tasks=visible_tasks), target="#task-rows", swap="inner", ), ] def template_data(self, kwargs, slots): return { "tasks": kwargs.tasks, } template = """
    """ js = """ $component(({ onEvent, scope }) => { scope.hideCompleted = false; onEvent( 'TaskList:filter-changed', (detail) => { scope.hideCompleted = detail.hideCompleted; }, ); }); """ class TutorialPage(Component): citry = citry_app class Kwargs: pass class Slots: pass def template_data(self, kwargs, slots): return {"tasks": load_tasks()} template = """ Task list

    Task list

    """ ``` Open `http://127.0.0.1:8000/`. Each row can save independently. Try a two-character title in one row to keep its validation error visible, then save another row. The first error stays in place. `TaskList` renders the same `TaskFilterToggle` component above and below the rows. Hide completed tasks with either control. The server sends back the two unfinished tasks, and both controls change to "Show all tasks." ## Preserve browser state The list renders the same component class several times: ```citry-html ``` Each `` is a separate component instance with its own [`State`](/reference/component/#citry-component-state), call queue, loading counters, and handler errors. `#c-key="task.id"` tells Citry which row is which, so Citry can safely [morph them](https://alpinejs.dev/plugins/morph){: target="_blank" rel="noopener"}. Morphing means that when you re-fetch the list with changed order or items, any browser state of the old list keeps working (eg a text field keeps the end user's input). Without the key, rows are matched by position. !!! note Use a stable application identifier for the key. A database primary key, slug, or other domain ID is suitable. **DO NOT** use a Citry component ID, it changes with each render. ## Pass inputs to event handlers The `save` handler needs the new title and the ID of the task to update. There are three good ways to give it that ID. ### Keep the ID in State This lesson keeps the ID in [`State`](/reference/component/#citry-component-state): ```citry class RenameTaskIn: title: str class TaskRow(Component): class State: task_id: int ``` In this component, Citry starts `state.task_id` from the matching `Kwargs.task_id`, which the list passes with `c-task_id`. When the form is submitted, `RenameTaskIn` carries the edited title, while `State` remembers which task this row belongs to. The form does not need a hidden field for the ID. ### Submit the ID as a form field The second option is to send both values with the form: ```python class RenameTaskIn: task_id: int title: str ``` ```citry-html ``` The handler would then use `data.task_id`, and `TaskRow` would not need a `State` class. This keeps everything `save` needs together in `data`, but every form must now carry its own `task_id` field. ### Pass the ID from Alpine The third option is to keep the ID in Alpine and add it when the form calls `save`: ```citry-html
    ``` `@c-submit` combines the form's named controls with the object passed to `save`, so `RenameTaskIn` receives both `title` and `task_id`. This version also needs no `State` class or hidden input. ## Error and loading state Each row is an isolated component environment: - Own event handlers - Own State - Own Alpine data context Inside the row, call [`$loading()`](/reference/browser-apis/#loading) and [`$error()`](/reference/browser-apis/#error) to get loading/error state scoped to this row: ```citry-html

    ``` The handler name `'save'` picks errors specifically coming from the event handler named `save`. A success clears the error. When one component contains several handlers, `$error()` returns its newest retained error for a component-wide banner. `$error('save')` remains the better choice beside one form. `$error()` and `$loading()` are also accessible inside the component callback [`$component`](/reference/browser-apis/#component) as `error()` and `loading()`. Note, these functions return [Alpine reactive objects](https://alpinejs.dev/advanced/reactivity){: target="_blank" rel="noopener"}, and the accessing logic must be wrapped in `effect()`: ```js $component(({ error, loading, effect }) => { // Same data as the ones in `x-data` and `:disabled` scope.text = ''; scope.disabled = false; // Call the functions inside effect() so the changes propagate effect(() => { scope.text = error('save')?.fieldErrors?.title || ''; scope.disabled = loading('save'); }); }); ``` ## Event actions are isolated Every row uses the same event handler `save`, and so returns the same event name: ```python return actions.Dispatch( "TaskRow:saved", {"taskId": state.task_id, "title": title}, ) ``` Despite this, the dispatched event `TaskRow:saved` **DOES NOT** leak across the rows. When you use [`onEvent`](/reference/browser-apis/#on-event), Citry smartly passes that Dispatch action to the component instance whose handler returned it. The row listens with its instance-scoped `onEvent` helper: ```js $component(({ onEvent, scope }) => { scope.saveStatus = ''; onEvent('TaskRow:saved', (detail) => { scope.saveStatus = `Saved task ${detail.taskId}: ${detail.title}`; }); }); ``` ## Bypass Dispatch event isolation The recommended pattern is to use `onEvent` and keep the events coming from `Dispatch` isolated. But if you need, you can use Alpine's [`@event`](https://alpinejs.dev/directives/on){: target="_blank" rel="noopener"} listeners or browser's [`addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener){: target="_blank" rel="noopener"} to listen for the events. The browser events triggered by `Dispatch` are regular browser events. For example, `Dispatch("taskrow:saved", ...)` can be heard by `@taskrow:saved="..."`. Regular event bubbling rules apply - the listener hears the event on the dispatching root or an ancestor, but not on a descendant or a sibling. ## Filter feature end-to-end One task in the example is already complete. Filtering therefore needs a server request: Python chooses which tasks still match, then rerenders the list with two rows instead of three. `TaskFilterToggle` owns the button markup. It reads the reactive values its parent passes through `clientProps`: ```citry-html ``` `TaskList` renders that component twice. Each copy receives the same list filter and calls the same list-owned handler: ```citry-html ``` The `$c-props` expressions run in `TaskList`, so both controls read its `hideCompleted` value and loading state. When `TaskFilterToggle` is clicked, this triggers the `@c-click` listener, which calls the server-side `filter_tasks`. The `@c-click` belongs to `TaskList`. The browser listens for the click on the real button rendered by `TaskFilterToggle`. The `filter_tasks` event handler uses the requested filter to select tasks on the server. It then returns two actions in order: ```python # TaskList.Events.filter_tasks def filter_tasks(self, data: FilterTasksIn) visible_tasks = load_tasks( hide_completed=data.hide_completed, ) return [ actions.Dispatch( "TaskList:filter-changed", {"hideCompleted": data.hide_completed}, ), actions.Render( TaskRows(tasks=visible_tasks), target="#task-rows", swap="inner", ), ] ``` The [`Render`](/reference/events/#citry-ext-events-actions-render) action replaces the contents of `#task-rows` with a `TaskRows` component containing only the matching rows. It leaves `TaskList` and both filter controls in place. The [`Dispatch`](/reference/events/#citry-ext-events-actions-dispatch) action updates that existing list scope and tells both controls whether the filter is now active. `TaskList` registers an instance-scoped listener in its `$component` callback: ```js $component(({ onEvent, scope }) => { scope.hideCompleted = false; onEvent( 'TaskList:filter-changed', (detail) => { scope.hideCompleted = detail.hideCompleted; }, ); }); ``` The callback updates `TaskList`'s `scope.hideCompleted` - this exposes `hideCompleted` as an Alpine template variable (for this instance only). This is the same `hideCompleted` that's passed down to `TaskFilterToggle`'s `$c-props`. The reactive props then update both `TaskFilterToggle` instances. If the next click comes from the other control, it now sends `false` and asks the server for all three tasks again. Citry removes the scoped subscription when the list instance leaves the page. This page has one `TaskList` and therefore one `#task-rows` update target. In a real-life application, you might want to update only individual rows instead of re-rendering entire list. ## Dispatch vs Data actions The filter controls send the events to the server using `@c-click`. `@c-click` only declares what server event handler to send the event to, but it can't handle the server response. That's why the server responds with a [`Dispatch`](/reference/events/#citry-ext-events-actions-dispatch) action - this works around the `@c-click`'s limitation: 1. `@c-click` triggers a server event. 2. Server-side handler receives and processes the event. 3. The server returns a `Dispatch` action to trigger a browser event `'TaskList:filter-changed'`. 4. In the browser, separate [`onEvent()`](/reference/browser-apis/#on-event) callback is registered in `TaskList` to listen for this event. 5. Inside the `onEvent()`, we can access the data sent with this event. There is a simpler way: When you want to trigger a server event AND get data back from the server, you can instead use [`sendEvent()`](/reference/browser-apis/#send-event) together with [`actions.Data`](/reference/events/#citry-ext-events-actions-data). `sendEvent()` returns a Promise that resolves to the data returned by [`actions.Data`](/reference/events/#citry-ext-events-actions-data): ```js const result = await sendEvent( 'filter_tasks', { hide_completed: !hideCompleted }, ); // result === { hideCompleted: true } ``` And the Python action would have look like: ```python actions.Data( {"hideCompleted": data.hide_completed}, ) ``` In the browser, the code would look like this: Replace `onEvent` in `TaskList`'s JavaScript with `sendEvent`: ```js $component(({ sendEvent, scope }) => { scope.hideCompleted = false; scope.onFilterTasks = () => { const result = await sendEvent( 'filter_tasks', { hide_completed: !scope.hideCompleted }, ); scope.hideCompleted = result.hideCompleted; }; }); ``` And in the template, replace `@c-click` with regular `@click` on `TaskFilterToggle`: ```citry-html ``` Bottom-line: Use Dispatch when `@c-*` starts the call and browser listeners must react. A handler may return both when it supports both call styles. ## Keep building Congratulations, you've reached the end of the tutorial. You're now ready to get building! :) You've now have the core patterns for editable tables, kanban columns, search results, and other repeated interactive components: From here: - Use [Examples](/examples/) when you want working code for a specific task. - Read [Docs](/getting-started/installation/) when you want a concept or guided workflow. - Read [Reference](/reference/) when you need the exact API for a class, method, or return action. - Install [Citry UI](/ui-library/), a library of reusable UI components. - Install [Citry linter](/ide/vscode/) for your IDE, to get syntax highlight, diagnostics, and more. --- # AI coding agents Source: https://citry.dev/getting-started/ai-agents/ # AI coding agents You can use a coding agent with Citry by giving it the documentation index and your project's setup instructions. You do not need to install a Citry skill. ## Point the agent to `llms.txt` Citry has an agent-friendly version of this website which is in plain markdown at [llms.txt](/llms.txt). An agent can retrieve the pages needed for its task without loading the whole site. Test it out on a simple task: ```text Context: Use https://citry.dev/llms.txt for Citry documentation. Read this project's README for setup and test commands. Task: Add a filter to the project list using a Python Citry Event. Check the result in a browser and run the project tests. ``` ## Using agent instructions in projects For a hands-on example, see the [Citry starter projects](https://github.com/citry-dev/citry/tree/main/examples/starters){: target="_blank" rel="noopener"}. Each project includes `AGENTS.md` with project-specific paths, documentation pointers, and verification commands. Their `CLAUDE.md` imports that file for Claude Code. ## Set up an existing project Create `AGENTS.md` in the project root, or merge this section into the file you already maintain: ```markdown ## Citry Read README.md for project setup and verification commands. Use https://citry.dev/llms.txt to find relevant Citry guides and API references. Fetch the linked Markdown pages as needed. Check APIs against the installed Citry version and preserve the project's dependency constraints. Run the project tests after changes. For browser behavior, also exercise the affected interaction in a browser. ``` Add your component directories, test and run commands, etc. ### Codex Put `AGENTS.md` at the project root and start a new Codex session in that project. See [Codex's AGENTS.md guide](https://learn.chatgpt.com/docs/agent-configuration/agents-md){: target="_blank" rel="noopener"} for how global and nested instructions combine. ### Claude Code Create `CLAUDE.md` beside `AGENTS.md` with this import, or add the import to your existing `CLAUDE.md`: ```text @AGENTS.md ``` Claude Code supports file imports in its project instructions. See [Claude Code's memory guide](https://code.claude.com/docs/en/memory){: target="_blank" rel="noopener"}. ### Other agents Use your tool's project-instructions setting to include `AGENTS.md`, or ask the agent to read it at the start of the task. You can also paste the snippet directly into a prompt. ## `llms-full.txt` Start with [llms.txt](/llms.txt). It is an index of guides and API references, with links to their Markdown versions. An agent can retrieve the pages needed for its task without loading the whole site. [llms-full.txt](/llms-full.txt) combines documentation into one text export. Use it when your tool works better with an attached document or needs a local copy. It is much larger, and a saved copy can become outdated. ## Versioning Check the installed version in the same environment that runs the app: ```console python -c "from importlib.metadata import version; print(version('citry'))" ``` Compare the result with the version shown by the documentation. If an example uses an API absent from your installation, inspect the installed package and the [release notes](/releases/) before changing dependencies. Keep the project's lockfile and compatibility requirements in mind. ## Check the setup Ask the agent to identify the installed Citry version, locate a guide relevant to your task, and state the project's verification command before editing. If it cannot retrieve documentation, provide the relevant Markdown pages directly. After the change, review its test results and exercise browser interactions that matter to your application. --- # Template basics Source: https://citry.dev/syntax/ # Template basics A Citry template turns component data into the page people see. You can insert names and calculated values, show or hide content, repeat an element for every item in a collection, set HTML attributes from data, and build a page from smaller components. Citry keeps those jobs close to ordinary HTML. `{{ ... }}` inserts a Python value, `c-*` attributes let Python decide how an element renders, and `` tags place components and built-in behavior in the page. ```citry-html

    {{ heading }}

    {{ book }}

    No books yet.

    ``` Citry evaluates the Python expressions before the HTML reaches the browser. This example inserts the heading, adds the `has-books` class, and creates one paragraph for every book. When part of the page should respond immediately to a click, keystroke, or other browser action, use [Alpine](https://alpinejs.dev/){: target="_blank" rel="noopener"} in the component's HTML. Its `x-data`, `x-show`, and `@click` attributes run after the page loads, without asking Python to render the page again. Start with [Alpine in templates](/syntax/alpine/). ## The syntax at a glance Core features: - `{{ expression }}`: insert a [Python value](/syntax/expressions/) - `c-title="heading"`: set a Python value as [attribute or component input](/syntax/dynamic-attributes/) - `c-if`, `c-for`: [conditions and loops](/syntax/control-flow/) - `x-*`, `@event`, `:name`: [Alpine behavior](/syntax/alpine/) - ``, ``: [components](/concepts/components/) and [built-in tags](/reference/builtins/) - `{# ... #}`, ``: [comments or literal text](/syntax/comments/) - `c-body="<>..."`: [markup through an attribute](/syntax/nested-templates/) Citry also has attributes for browser and server interaction: - `$c-props`, `@c-*`, and `:c-*` are covered in [Client interactivity](/concepts/client-interactivity/) and [Events](/events/) - `#c-key` and `#c-ignore` are the template's [template flags](/syntax/dynamic-attributes/#c-template-flags). They guide how an event response updates existing HTML. See also [Event actions](/events/actions/). ## Self-closing tags Opening and closing tags must match (case-insensitive). Standard void elements such as `` and `
    ` do not need closing tags. Other tags may use the compact self-closing form too: ```citry-html ``` When rendered, `` becomes ``. Citry component syntax always starts with the exact lowercase `c-` prefix. The component name after it is case-insensitive, so `` and `` find the same registration. Structural tags such as ``, ``, and `` must use their lowercase spelling; `` is an error, not an alias. ## Expressions The `{{ ... }}` Python expressions are allowed only outside of tags: ```citry-html {# ✅ Valid #}

    {{ content }}

    {# ❌ Invalid #}

    {# ❌ Invalid #}

    {# ❌ Invalid #} <{{ tag }} title="Some title"> ``` ## Python attributes To use Python in HTML attributes, prefix the name with `c-`: ```citry-html {# Static title #}

    {# Dynamic title #}

    ``` Static HTML attributes are literal strings. Citry strips the `c-` prefix from the dynamic attributes, so: ```citry-html

    ``` becomes: ```citry-html

    ``` !!! note Attribute values may use double quotes, single quotes, or HTML's unquoted form. Always quote dynamic `c-*` expressions so spaces and operators stay inside the value. ## Boolean attributes A value-less HTML attribute is a bare boolean attribute. On a component tag, the same spelling passes the Python value `True`: ```citry-html ``` ## Other HTML comments, declarations such as ``, and processing instructions remain part of the output. Citry does not use Django or Jinja block syntax, so text such as `{% include "menu.html" %}` stays literal too. ## Choose the next page Start with [Expressions](/syntax/expressions/) if you want to insert or compute a value. Continue to [Attributes](/syntax/dynamic-attributes/) when that value belongs on an HTML element or needs to become a Python input to a component. --- # Expressions Source: https://citry.dev/syntax/expressions/ # Expressions Use `{{ ... }}` to evaluate a Python expression inside a template: ```citry-html

    Hello, {{ user.name }}

    Your total is {{ price * quantity }}.

    ``` Citry evaluates both expressions on the server. [Dynamic attributes](/syntax/dynamic-attributes/) use the same expressions, but without the braces: ```citry-html

    Hello, {{ user.name }}

    ``` ## Placement The `{{ ... }}` Python expressions are allowed only outside of tags: ```citry-html {# ✅ Valid #}

    {{ content }}

    {# ❌ Invalid #}

    {# ❌ Invalid #}

    {# ❌ Invalid #} <{{ tag }} title="Some title"> ``` ## Template variables By default, every field in a component's [`Kwargs`](/reference/component/#citry-component-kwargs) is available by name: ```citry from citry import Component class Greeting(Component): class Kwargs: name: str template = "

    Hello, {{ name }}

    " ``` Use [`template_data`](/reference/component/#citry-component-template-data) when the template needs a value you first have to prepare in Python. Here Python counts the items and exposes `count`: ```citry from citry import Component class Cart(Component): class Kwargs: items: list[str] def template_data(self, kwargs: Kwargs, slots): return {"count": len(kwargs.items)} template = "

    {{ count }} items

    " ``` Overriding `template_data()` replaces the default mapping. In this example, `count` is available but `items` is not. Return both when you need both: ```python return { "items": kwargs.items, "count": len(kwargs.items), } ``` Missing variable raises `KeyError`. !!! note If [sandboxing](#sandbox) is disabled, missing variable instead raises `NameError`. ## Python expressions You can use the familiar expression forms that produce a value: ```citry-html {{ user.name.upper() }} {{ items[0] }} {{ names[1:3] }} {{ "Member" if user.is_active else "Guest" }} {{ f"{user.name}: {score}" }} {{ any_score > 0 and account.is_active }} ``` Literals, calls, attribute access, indexing, slicing, arithmetic, comparisons, boolean operations, and conditional expressions all work. An expression must produce a value. Python statements such as `import`, `return`, `del`, `def`, and an assignment with `=` are not allowed. Async expressions and `yield` are not supported either. A Python string may contain `}}`; Citry still finds the real end of the expression correctly: ```citry-html

    {{ "A string containing }} is fine" }}

    ``` Citry expressions are Python, not Django or Jinja expressions. There are no template filters, and `|` keeps its Python meaning as the [bitwise OR operator](https://docs.python.org/3/reference/expressions.html#binary-bitwise-operations){: target="_blank" rel="noopener"}. !!! note Comprehensions, lambdas, and assignment expressions with `:=` work too, but usually make a template harder to scan. Prepare complicated values in `template_data()` instead. A `:=` assignment changes the render context, so a name it creates can affect expressions that render later in the same context. ## Python builtins not available Functions such as `len()`, `range()`, `str()`, and `sum()` are not added to a template automatically. This fails with `KeyError: 'len'`: ```citry-html {{ len(items) }} items ``` Compute the value in `template_data()`, as the `Cart` example above does. You can deliberately expose a function too: ```python return { "len": len, "items": kwargs.items, } ``` The template can then call `len(items)`, because both names are available to it. ## Expression results Expression results follow these rules: | Type | Result | |--|--| | `None` | Empty string | | Ordinary values | Converted to text and HTML-escaped | | Composed components
    ([`Component()`](/reference/component/#citry-component), [`CitryElement`](/reference/rendering/#citry-citryelement)) | Behaves as part of template | | Rendered components
    ([`Component().render()`](/reference/rendering/#citry-citryelement-render), [`CitryRender`](/reference/rendering/#citry-citryrender)) | Behaves as part of template | | [`Slot`](/reference/slots/#citry-slot) | Behaves as part of template | | [`Markup`](/reference/rendering/#citry-markup) or an object with `__html__()` | Inserted as trusted HTML | Serializing a component turns it into a regular string. If you then try to insert it into a template, it gets HTML-escaped: ```citry table = str( Table(headers=headers, rows=rows) ) class Page(Component): def template_data(self, kwargs, slots): return {"table": table} template = "{{ table }}" page = str(Page()) print(page) # '<table>...' ``` ## Bypass HTML escape HTML escaping includes quotes, apostrophes, `<`, `>`, and `&`. [`Markup`](/reference/rendering/#citry-markup) and `__html__()` bypass the escaping that normally protects the page from untrusted content. `citry.Markup` is exactly [`markupsafe.Markup`](https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup){: target="_blank" rel="noopener"}, re-exported unchanged. `Markup(value)` trusts the complete value. It does not sanitize, validate, or escape anything, so use it only when the complete value is trusted HTML. Dynamic values must be added through `Markup.format()`, which escapes ordinary strings. Passing an interpolated string to the constructor trusts the dynamic part too: ```python from citry import Markup user_title = '' # Wrong: the constructor trusts the interpolated user value. unsafe_title = Markup(f"

    {user_title}

    ") # Right: Markup.format() escapes the user value. safe_title = Markup("

    {}

    ").format(user_title) ``` Citry also trusts the result of an object's `__html__()` method. Return `Markup` and compose dynamic values through its escaping operations: ```python from citry import Markup class MetaTag: name: str content: str def __html__(self) -> Markup: return Markup('').format( self.name, self.content, ) ``` To make Ruff's [`S704`](https://docs.astral.sh/ruff/rules/unsafe-markup-use/){: target="_blank" rel="noopener"} rule recognize the Citry import path, add this to your `pyproject.toml`: ```toml [tool.ruff.lint.flake8-bandit] extend-markup-names = ["citry.Markup"] ``` This setting extends S704's recognized constructors; enable S704 through your Ruff lint selection if it is not already enabled. ## Comments in expressions Inside an expression, `#` starts an ordinary Python comment: ```citry-html
    {{ user.name # show the person's name }}
    ``` The comment ends either at the end of the line, or at the end of the expression region (closing quote or `}}`). See [Comments and literal text](/syntax/comments/). ## Sandbox All Python expressions run in a security sandbox, whether it's `{{ ... }}` or `c-` attributes. The sandbox blocks following: What | How --|-- Private attributes `_abc` | Access blocked Dunder attributes `__abc` | Access blocked Unsafe functions such as `eval`, `exec`, and `open` | Calling blocked `str.format()` and `str.format_map()` | Calling blocked (use an f-string instead) A blocked operation raises [`SecurityError`](/reference/rendering/#citry-securityerror). Read [Security](/security/) for the complete sandbox contract and the settings that control it. --- # Attributes Source: https://citry.dev/syntax/dynamic-attributes/ # Attributes ## `c-` Dynamic attributes An ordinary attribute contains a fixed value. Add `c-` to turn the value into a Python expression that *generates* the value: ```citry-html ``` Citry evaluates each value as a Python [expression](/syntax/expressions/) and removes the `c-` from the attribute name. The browser receives: ```html ``` Do not add `{{ }}` around the expression. Write `c-title="user.name"`, not `c-title="{{ user.name }}"`. A dynamic attribute needs a non-empty value. `c-title` and `c-title=""` are errors. The bare `c-else` and `c-empty` [control-flow markers](/syntax/control-flow/) are the two exceptions. ### HTML elements On an HTML element, Citry turns the result into an HTML attribute. `True` renders a bare attribute, while `False` and `None` leave it out: ```citry-html ``` Attribute names and values are HTML-escaped. The value can opt-out of HTML-escaping, see [Bypass HTML escape](/syntax/expressions/#bypass-html-escape). ### ARIA values If `aria-pressed` renders without a value or disappears, check the type of the expression's result. A Python `True` renders a bare attribute; `False` or `None` omits it. For a boolean ARIA state such as `aria-pressed`, return the strings `"true"` and `"false"`, including when the state is false: ```citry-html ``` For a Python value, use the `c-` expression form shown here. An ordinary attribute such as `aria-pressed="{{ selected }}"` keeps that text literally. ### Components On a component tag, an attribute is the [component's Python input](/concepts/inputs-and-validation/). A static value is a string, while a `c-*` value keeps its Python type: ```citry-html ``` Which is equivalent to Python: ```python UserBadge( label="User Name", user=user, enabled=feature_enabled, ) ``` How to read the above: - `label` receives the literal string `"User Name"` - `user` receives the Python object - `enabled` receives the Python boolean `False` and `None` are still passed to the component; they are not omitted. ### Alpine Use the same rule to dynamically generate Alpine expressions. Here Python provides the attribute's JavaScript source as a string: ```citry-html
    ``` When the JavaScript can be written directly, prefer a normal Alpine attribute such as `:class="{ open: isOpen }"`. Read [Alpine in templates](/syntax/alpine/) for browser-side attributes and [Client interactivity](/concepts/client-interactivity/) for values that cross component boundaries. ### Escape the c- prefix Citry removes exactly one leading `c-`. When you need to generate an attribute with the `c-` prefix, you can either: Add one more `c-`, so `c-c-feature` -> `c-feature`: ```citry-html
    ``` Or apply the attribute through [`c-bind`](#c-bind-spread), which preserves the keys: ```citry-html
    ``` ## Props and events Some attributes on a component tag never become Python inputs nor HTML attributes. Following serve to pass data across components: ```citry-html ``` ### `$c-props` Define Alpine runtime variables that will be passed from the parent component to the child. The value of `$c-props` is an Alpine expression (similar to `x-init`). Inside the value you can reference other Alpine variables define in the scope: ```citry-html
    ``` The Alpine expression in `$c-props` must return a JavaScript object. This object must match child's `props` declaration. See [Client interactivity](/concepts/client-interactivity#pass-client-props-down). Only component tags can have `$c-props`. `$c-props` on non-component tags raises an error. After dynamic attributes and spreads resolve, the actual target component must also register `$component(...)`; this includes the selected target of ``. A final `None` or `False` removes `$c-props` and does not require a registration. ### `@event` Alpine's [event bindings](https://alpinejs.dev/directives/on){: target="_blank" rel="noopener"} allow you to listen for browser events that originate from the child component. ```citry-html
    {# Inside ActionButton #} ``` Just like with regular Alpine, you can access `$event` inside the expression: ```citry-html ``` Read more on [Alpine events in Citry](/concepts/client-interactivity/#send-events-up-from-a-component-tag). ### `@c-event` Alpine's event handlers run in the browser. You can instead trigger [server event handlers](/events/) by prefixing the event name with `c-`, eg `@c-click`. So: - `@click` - Regular Alpine `click` event handler - `@c-click` - Send event to the server The value of `@c-click` attributes is strict: - It MUST name the server-side event handler, eg `submit` - It MAY send extra arguments, eg `submit({ title })` Read more about [Binding events in templates](/events/bindings/). ```citry-html
    {# No arguments #} {# With arguments #}
    ``` ## Class and style ### Class For an HTML `c-class`, its value may contain: - string - the class string itself, `"btn btn-sm"` - mapping - keys are class names, values are truthy == include / falsy == omit - lists / tuples - containing other strings, mappings, or lists A mapping includes each class whose value is truthy: ```citry-html
    ``` A later false mapping entry removes an earlier class of the same name: ```citry-html
    ``` If the structured value contains no classes, Citry omits the attribute. ```citry-html
    ``` `class` and `style` have special merging behavior - you can define both `c-class` and `class` and they merge: ```citry-html
    ``` ### Style For an HTML `c-style`, you may also pass a string, mapping, or nested sequence. Write CSS property names in kebab-case: ```citry-html

    Important

    ``` Across merged style values, `None` leaves an earlier property unchanged and `False` removes it. ```citry-html
    ``` An empty structured style is omitted: ```citry-html
    ``` `class` and `style` have special merging behavior - you can define both `c-style` and `style` and they merge: ```citry-html
    ``` ### Components are exempt The merging rules above only apply to plain HTML elements (including [``](/reference/builtins/#c-element)). On a component tag, `class` and `style` are ordinary component inputs. A component decides whether, and where, to place an input on its own HTML: ```citry-html {# On an HTML element the two values merge #}
    {# `Card` receives "card" as an ordinary input #} ``` See [passing HTML attributes through a component](/concepts/client-interactivity/#pass-arbitrary-html-attributes-explicitly). ## c-bind spread Use `c-bind` to apply several values at once: ```citry-html ``` The browser receives: ```html ``` `c-bind` accepts any Python mapping. Mapping keys must be strings and, on HTML elements, valid attribute names. The value of `c-bind` itself is always an expression. When `c-bind` evaluates to `None`, it does nothing. Any other non-mapping value raises `TypeError`. Keys are used exactly as written: a key named `c-title` stays `c-title`. Only a directly authored dynamic attribute loses one `c-` prefix: ```citry-html Order: Ada """ js = """ $component(({ scope, els }) => { const collection = els[0].querySelector('[data-citry-ui-part="form-collection"]'); const list = collection.querySelector(':scope > [data-citry-ui-part="items"]'); const parking = document.createElement('fieldset'); const parked = document.createElement('ol'); const maximum = list.children.length; parking.disabled = true; parking.hidden = true; parking.append(parked); collection.append(parking); const items = () => [...list.querySelectorAll(':scope > [data-citry-form-collection-item]')]; const sync = () => { const current = items(); collection.dataset.count = String(current.length); current.forEach((item, index) => { item.toggleAttribute('data-first', index === 0); item.toggleAttribute('data-last', index === current.length - 1); for (const button of item.querySelectorAll('[data-citry-form-collection-action]')) { const fixed = item.hasAttribute('data-citry-form-collection-item-disabled'); const action = button.dataset.citryFormCollectionAction; const unavailable = fixed || (action === 'move-up' && index === 0) || (action === 'move-down' && index === current.length - 1); button.disabled = unavailable; button.dataset.citryInitiallyDisabled = String(unavailable); } }); const add = collection.querySelector('[data-citry-ui-part="add"]'); add.disabled = current.length >= maximum || parked.children.length === 0; add.dataset.citryInitiallyDisabled = String(add.disabled); scope.collectionStatus = `Order: ${current.map(item => item.dataset.label).join(', ') || 'No items'}`; }; scope.collectionStatus = 'Order: Ada'; scope.applyCollectionAction = (detail) => { // Static docs accept the named request locally; a real server returns a keyed rerender. detail.sourceEvent.preventDefault(); const button = detail.sourceEvent.target.closest('[data-citry-form-collection-action]'); let item = button?.closest('[data-citry-form-collection-item]'); if (detail.action === 'move-up' && item?.previousElementSibling) item.previousElementSibling.before(item); else if (detail.action === 'move-down' && item?.nextElementSibling) item.nextElementSibling.after(item); else if (detail.action === 'remove' && item) parked.append(item); else if (detail.action === 'add') { item = parked.lastElementChild; if (item) list.append(item); } sync(); requestAnimationFrame(() => { const focusTarget = item?.isConnected && !parked.contains(item) ? item.querySelector('button:not(:disabled), input:not(:disabled)') : collection.querySelector('[data-citry-ui-part="add"]'); focusTarget?.focus(); }); }; sync(); }); """ preview = FormCollectionServerActions() preview # noqa: B018 ```` The docs preview accepts those named requests locally because this static page has no application endpoint. In an application, let the named submit continue and return the updated keyed collection from the server. Defaults encode `add`, `remove:`, `move-up:`, and `move-down:`. Override each value when your server protocol differs. For example, `remove_value="delete:member-17"` makes the Remove button submit `team_action=delete:member-17`. The colon has no Citry-specific meaning; the whole string is simply the application-defined value of the activated submit button. ## Handle requests in Alpine Without `action_name`, controls use `type=button`. Pass `onAction` through `$c-props` to receive `{action, value, index, toIndex, sourceEvent}` and update application state or send a Citry Event. ### Apply client collection requests [Open the rendered preview](/ui-library/components/form-collection/_previews/client-actions/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FormCollectionClientActions(Component): template = """
    Order: Mobile, Office
    """ js = """ $component({ init: ({ scope, els, i18n }) => { const collection = els[0].querySelector('[data-citry-ui-part="form-collection"]'); const list = collection.querySelector(':scope > [data-citry-ui-part="items"]'); const parking = document.createElement('fieldset'); const parked = document.createElement('ol'); const bindings = []; let nextSequence = 3; parking.disabled = true; parking.hidden = true; parking.append(parked); collection.append(parking); const bindActionLabel = (button, action, label) => { let binding = null; if (action === 'move-up') { button.setAttribute('aria-label', `Move ${label} up`); binding = i18n?.bind({ message: 'citry-ui-form-collection-move-up', values: () => ({ item: label }), onChange: value => button.setAttribute('aria-label', value), }); } else if (action === 'move-down') { button.setAttribute('aria-label', `Move ${label} down`); binding = i18n?.bind({ message: 'citry-ui-form-collection-move-down', values: () => ({ item: label }), onChange: value => button.setAttribute('aria-label', value), }); } else { button.setAttribute('aria-label', `Remove ${label}`); binding = i18n?.bind({ message: 'citry-ui-form-collection-remove', values: () => ({ item: label }), onChange: value => button.setAttribute('aria-label', value), }); } if (binding) bindings.push(binding); }; const makeAction = (action, symbol, label) => { const button = document.createElement('button'); button.type = 'button'; button.dataset.citryFormCollectionAction = action; button.textContent = symbol; bindActionLabel(button, action, label); return button; }; const makeItem = () => { const sequence = nextSequence; nextSequence += 1; const value = `phone-${sequence}`; const label = `Phone ${sequence}`; const labelId = `client-phone-${sequence}-label`; const item = document.createElement('li'); item.className = 'cui-form-collection__item'; item.dataset.value = value; item.dataset.label = label; item.dataset.citryFormCollectionItem = ''; item.dataset.citryUiPart = 'item'; const group = document.createElement('div'); group.setAttribute('role', 'group'); group.setAttribute('aria-labelledby', labelId); const header = document.createElement('header'); header.dataset.citryUiPart = 'item-header'; const itemLabel = document.createElement('div'); itemLabel.id = labelId; itemLabel.dataset.citryUiPart = 'item-label'; itemLabel.textContent = label; const actions = document.createElement('div'); actions.dataset.citryUiPart = 'item-actions'; actions.append( makeAction('move-up', '↑', label), makeAction('move-down', '↓', label), makeAction('remove', '\u00d7', label), ); header.append(itemLabel, actions); const content = document.createElement('div'); content.dataset.citryUiPart = 'item-content'; const field = document.createElement('label'); const input = document.createElement('input'); input.name = `phones[${value}]`; field.append('Number ', input); content.append(field); group.append(header, content); item.append(group); return item; }; const items = () => [...list.querySelectorAll(':scope > [data-citry-form-collection-item]')]; const sync = () => { const current = items(); collection.dataset.count = String(current.length); current.forEach((item, index) => { item.toggleAttribute('data-first', index === 0); item.toggleAttribute('data-last', index === current.length - 1); for (const button of item.querySelectorAll('[data-citry-form-collection-action]')) { const fixed = item.hasAttribute('data-citry-form-collection-item-disabled'); const action = button.dataset.citryFormCollectionAction; const unavailable = fixed || (action === 'move-up' && index === 0) || (action === 'move-down' && index === current.length - 1); button.disabled = unavailable; button.dataset.citryInitiallyDisabled = String(unavailable); } }); const add = collection.querySelector('[data-citry-ui-part="add"]'); add.disabled = false; add.dataset.citryInitiallyDisabled = 'false'; scope.collectionStatus = `Order: ${current.map(item => item.dataset.label).join(', ') || 'No items'}`; }; scope.collectionStatus = 'Order: Mobile, Office'; scope.applyCollectionAction = (detail) => { // The static preview owns this local record set; production owners rerender their own state. detail.sourceEvent.preventDefault(); const button = detail.sourceEvent.target.closest('[data-citry-form-collection-action]'); let item = button?.closest('[data-citry-form-collection-item]'); if (detail.action === 'move-up' && item?.previousElementSibling) item.previousElementSibling.before(item); else if (detail.action === 'move-down' && item?.nextElementSibling) item.nextElementSibling.after(item); else if (detail.action === 'remove' && item) parked.append(item); else if (detail.action === 'add') { item = parked.lastElementChild || makeItem(); list.append(item); } sync(); requestAnimationFrame(() => { const focusTarget = item?.isConnected && !parked.contains(item) ? item.querySelector('button:not(:disabled), input:not(:disabled)') : collection.querySelector('[data-citry-ui-part="add"]'); focusTarget?.focus(); }); }; sync(); return () => bindings.forEach(binding => binding.dispose()); }, }); """ preview = FormCollectionClientActions() preview # noqa: B018 ```` The component deliberately does not clone existing component DOM. The owner must add, remove, or reorder records and render the resulting keyed Items. The preview emulates that owner locally. It creates stable records for new phone fields and keeps removed Items connected while applying the same callback details, so Add remains unbounded and edits survive reorder and restoration on this static page. ## Limit available actions `min_items` and `max_items` guard Remove and Add controls. Root `disabled` disables mutation controls without silently disabling consumer fields. `removable`, `movable`, and Item `disabled` refine one group. ### Keep required and fixed groups [Open the rendered preview](/ui-library/components/form-collection/_previews/limits/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FormCollectionLimits(Component): template = """ """ preview = FormCollectionLimits() preview # noqa: B018 ```` If the entire form group must stop submitting, disable its actual native controls or an application-owned ancestor fieldset too. ## Preserve edits and choose focus Citry keyed rerenders can retain surviving native controls, their browser-owned edits, selection, and focus while Items reorder. After adding or removing an Item, the application chooses the new focus target because it owns the new record and business policy. ### Label repeated shipping addresses [Open the rendered preview](/ui-library/components/form-collection/_previews/accessibility/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FormCollectionAccessibility(Component): template = """
    """ preview = FormCollectionAccessibility() preview # noqa: B018 ```` The fieldset, legend, grouped Items, and native buttons provide the semantic baseline. Action labels come from Citry UI catalog messages; application Item labels and fields retain their own locale and direction. ## API reference ### Inputs #### CFormCollection server inputs Server inputs are passed in a template through `` or in Python through `CFormCollection(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `label` | `str` | required | Supplies the visible native legend and collection name. | | `id` | `str | None` | generated | Sets the fieldset ID and bases stable Item label IDs. | | `description` | `str | None` | `None` | Adds plain described-by guidance below the legend. | | `action_name` | `str | None` | `None` | When supplied actions are named submit buttons; otherwise they are client-only Buttons. | | `add_value` | `str` | `"add"` | Sets the Add submit-button value. | | `allow_add` | `bool` | `True` | Includes the Add control. | | `allow_remove` | `bool` | `True` | Includes permitted Item Remove controls. | | `allow_reorder` | `bool` | `True` | Includes permitted Move controls. | | `min_items` | `int` | `0` | Rejects fewer rendered Items and disables Remove at the minimum. | | `max_items` | `int | None` | `None` | Rejects more rendered Items and disables Add at the maximum. | | `disabled` | `bool` | `False` | Disables collection mutation controls without disabling consumer fields. | | `size` | `CFormCollectionSize` ([`CFormCollectionSize`](#form-collection-interface-size)) | `"md"` | Selects collection spacing density. | | `add_label` | `str` | `"Add item"` | Overrides the localized Add text. | | `remove_label` | `str` | `"Remove {item}"` | Overrides localized Remove names and must retain item. | | `move_up_label` | `str` | `"Move {item} up"` | Overrides localized Move up names and must retain item. | | `move_down_label` | `str` | `"Move {item} down"` | Overrides localized Move down names and must retain item. | | `class_` | `CClassValue | None` ([`CClassValue`](#form-collection-interface-class-value)) | `None` | Adds classes to the fieldset. | | `style` | `CStyleValue | None` ([`CStyleValue`](#form-collection-interface-style-value)) | `None` | Adds styles to the fieldset. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed fieldset attributes. |
    #### CFormCollection client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `disabled` | `boolean` | Uses the server value. | Reactively disables mutation controls. | | `onAction` | `function` | No component callback runs. | Receives Add Remove Move up and Move down requests. |
    #### CFormCollectionItem server inputs Server inputs are passed in a template through `` or in Python through `CFormCollectionItem(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | required | Supplies unique stable Item identity and default action-value suffix. | | `label` | `str` | required | Supplies the visible group heading and action-name interpolation. | | `remove_value` | `str | None` | generated | Overrides the default remove colon value action protocol. | | `move_up_value` | `str | None` | generated | Overrides the default move-up colon value action protocol. | | `move_down_value` | `str | None` | generated | Overrides the default move-down colon value action protocol. | | `removable` | `bool` | `True` | Includes this Item's Remove control when root removal is allowed. | | `movable` | `bool` | `True` | Includes this Item's Move controls when root reorder is allowed. | | `disabled` | `bool` | `False` | Disables this Item's collection actions only. | | `class_` | `CClassValue | None` ([`CClassValue`](#form-collection-interface-class-value)) | `None` | Adds classes to the Item group. | | `style` | `CStyleValue | None` ([`CStyleValue`](#form-collection-interface-style-value)) | `None` | Adds styles to the Item group. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed Item-group attributes. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CFormCollection slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CFormCollectionDefaultSlotData`](#form-collection-interface-cform-collection-default-slot-data)) | Empty collection. |
    #### CFormCollectionItem slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{value, label, index, count, is_first, is_last, disabled}` ([`CFormCollectionItemSlotData`](#form-collection-interface-cform-collection-item-slot-data)) | None; contains the actual repeated fields. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CFormCollection events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onAction` | `(detail: CFormCollectionActionDetail) => void` ([`CFormCollectionActionDetail`](#form-collection-interface-cform-collection-action-detail)) | An enabled collection action Button is activated. | `{action, value, index, toIndex, sourceEvent}` ([`CFormCollectionActionDetail`](#form-collection-interface-cform-collection-action-detail)) | Reports a request and never mutates the collection DOM. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CFormCollection CSS variables Apply these variables to `CFormCollection` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-form-collection-gap` | `length` | Space between Item groups and Add. | `0.875rem` | | `--cui-form-collection-item-surface` | `color` | Item group surface. | `Canvas` | | `--cui-form-collection-item-border` | `complete border` | Item and header boundary. | `Adaptive 1px neutral` | | `--cui-form-collection-item-radius` | `length` | Item corners. | `0.75rem` | | `--cui-form-collection-action-gap` | `length` | Gap between mutation controls. | `0.375rem` | | `--cui-form-collection-focus` | `color` | Mutation-control focus ring. | `Highlight` | | `--cui-form-collection-disabled-opacity` | `number` | Disabled collection and control opacity. | `0.55` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CFormCollection attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-count` | Root | `nonnegative integer string` | Reflects rendered Item count. | | `data-size` | Root | `CFormCollectionSize` ([`CFormCollectionSize`](#form-collection-interface-size)) | Reflects spacing density. | | `data-disabled` | Root and Item | `present | absent` | Reflects unavailable collection actions. | | `data-first` | Item | `present | absent` | Marks first current Item. | | `data-last` | Item | `present | absent` | Marks last current 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. #### CFormCollection selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="form-collection"]` | Native fieldset root | Semantic theme and reflected-state destination. | | `[data-citry-ui-part="legend"]` | Native legend | Visible collection name. | | `[data-citry-ui-part="description"]` | Optional paragraph | Collection guidance. | | `[data-citry-ui-part="items"]` | Ordered list | Current server Item order. | | `[data-citry-ui-part="item"]` | Grouped list item | Stable repeated group. | | `[data-citry-ui-part="item-header"]` | Header | Groups label and actions. | | `[data-citry-ui-part="item-label"]` | Heading | Visible Item group name. | | `[data-citry-ui-part="item-actions"]` | Action container | Move and Remove controls. | | `[data-citry-ui-part="item-content"]` | Content div | Actual repeated fields. | | `[data-citry-ui-part="add"]` | Native Button | Add request. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CFormCollectionSize` | `Literal["sm", "md", "lg"]` | | `CFormCollectionAction` | `Literal["add", "remove", "move-up", "move-down"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CFormCollectionDefaultSlotData` Empty dataclass: `{}`. #### `CFormCollectionItemSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Stable Item identity. | | `label` | `str` | - | Plain application-owned Item label. | | `index` | `int` | - | Zero-based current server index. | | `count` | `int` | - | Current rendered Item count. | | `is_first` | `bool` | - | Whether this is the first Item. | | `is_last` | `bool` | - | Whether this is the last Item. | | `disabled` | `bool` | - | Initial effective collection-action disabled state. |
    #### `CFormCollectionActionDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `action` | `CFormCollectionAction` ([`CFormCollectionAction`](#form-collection-interface-action)) | - | Requested mutation. | | `value` | `str | None` | - | Item value or null for Add. | | `index` | `int | None` | - | Current Item index or null for Add. | | `toIndex` | `int | None` | - | Requested adjacent destination or null. | | `sourceEvent` | `object` | - | Native click Event. |
    ### 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. #### CFormCollection translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-form-collection-add` | Labels the Add control. | `None.` | `add_label` | Stable `$c-tr` text. | | `citry-ui-form-collection-remove` | Names an Item Remove control. | `item: str` | `remove_label` with `{item}` | Stable reactive `$c-tr` attribute. | | `citry-ui-form-collection-move-up` | Names an Item Move up control. | `item: str` | `move_up_label` with `{item}` | Stable reactive `$c-tr` attribute. | | `citry-ui-form-collection-move-down` | Names an Item Move down control. | `item: str` | `move_down_label` with `{item}` | Stable reactive `$c-tr` attribute. |
    --- # Listbox Source: https://citry.dev/ui-library/components/listbox/ # Listbox Use `CListbox` when the choices should remain visible while people compare and select them. Use Select or MultiSelect when the choices should open from a compact form control. ## Listbox at a glance ### Listbox at a glance [Open the rendered preview](/ui-library/components/listbox/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListboxAtAGlance(Component): template = """ Atlas research 12 collaborators Active Aurora field notes 7 collaborators Archived studies """ preview = ListboxAtAGlance() preview # noqa: B018 ```` ## Select one value ### Select one value [Open the rendered preview](/ui-library/components/listbox/_previews/single-selection/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SingleSelection(Component): template = """ Compact Comfortable Spacious """ preview = SingleSelection() preview # noqa: B018 ```` ## Select several values ### Select several values [Open the rendered preview](/ui-library/components/listbox/_previews/multiple-selection/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class MultipleSelection(Component): template = """ Temperature Humidity Pressure Wind speed """ preview = MultipleSelection() preview # noqa: B018 ```` ## Group related options ### Group options [Open the rendered preview](/ui-library/components/listbox/_previews/groups/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GroupedOptions(Component): template = """ Prague Lisbon Kyoto Wellington """ preview = GroupedOptions() preview # noqa: B018 ```` ## Control selection ### Control selection [Open the rendered preview](/ui-library/components/listbox/_previews/controlled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledListbox(Component): template = """
    Draft Ready for review Approved

    Current:

    """ js = """ Alpine.store('listboxExample', {value: 'draft'}); """ preview = ControlledListbox() preview # noqa: B018 ```` ## Disabled collections and options ### Disable Listboxes and Options [Open the rendered preview](/ui-library/components/listbox/_previews/disabled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DisabledListbox(Component): template = """ Europe United States — unavailable Asia Pacific Standard Strict """ preview = DisabledListbox() preview # noqa: B018 ```` ## Keyboard navigation Down and Up move between enabled Options. Home and End jump to the collection edges, printable text performs buffered typeahead, Enter or Space selects, and Escape clears a non-mandatory selection. ### Navigate a Listbox [Open the rendered preview](/ui-library/components/listbox/_previews/keyboard/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class KeyboardListbox(Component): template = """ Brno Budapest Kraków Prague Vienna """ preview = KeyboardListbox() preview # noqa: B018 ```` ## Customize Listbox ### Customize Listbox [Open the rendered preview](/ui-library/components/listbox/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CustomizedListbox(Component): css = """ .copper-listbox { --cui-listbox-radius: 1.1rem; --cui-listbox-selected-background: light-dark(#7c2d12, #fed7aa); --cui-listbox-selected-foreground: light-dark(#fff7ed, #431407); --cui-listbox-border-color: light-dark(#c2410c, #fdba74); --cui-listbox-option-padding: 0.7rem 0.8rem; } """ template = """ Burnished copper Warm and tactile Deep slate Soft linen """ preview = CustomizedListbox() preview # noqa: B018 ```` ## Accessibility and behavior The named collection uses `role="listbox"`; Options use `role="option"`, and visible group labels name `role="group"` collections. One enabled Option is in the Tab order. Focus and selection remain separate, and disabled Options are skipped by keyboard navigation. `CListbox` is a persistent application selection surface, not a form control. Use Select or MultiSelect when native form submission, reset, validity, or a compact popup is required. ## API reference ### Inputs #### CListbox server inputs Server inputs are passed in a template through `` or in Python through `CListbox(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `label` | `str` | required | Supplies the visible accessible Listbox name. | | `value` | `str | None | Sequence[str]` ([`CListboxValue`](#listbox-interface-clistbox-value)) | `None` | Sets initial single or multiple selection. | | `multiple` | `bool` | `False` | Enables independent multiple selection. | | `mandatory` | `bool` | `False` | Prevents user interaction from clearing the final selected Option. | | `disabled` | `bool` | `False` | Disables focus and selection throughout the collection. | | `loop` | `bool` | `False` | Wraps arrow navigation at collection edges. | | `variant` | `"plain" | "soft" | "outline"` ([`CListboxVariant`](#listbox-interface-clistbox-variant)) | `"outline"` | Selects surface treatment. | | `size` | `"sm" | "md" | "lg"` ([`CListboxSize`](#listbox-interface-clistbox-size)) | `"md"` | Selects Option geometry. | | `class_` | `CClassValue | None` ([`CClassValue`](#listbox-interface-clistbox-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#listbox-interface-clistbox-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted root attributes without replacing owned state structure or runtime. | | `listbox_attrs` | `Mapping[str, object] | None` | `None` | Adds trusted attributes to the role listbox surface without replacing owned semantics or focus. |
    #### CListbox client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string | string[] | null` | Releases control to committed selection. | Controls single or multiple selection while supplied. | | `mandatory` | `bool` | Uses the server value. | Reactively prevents the final user-selected value from clearing. | | `disabled` | `bool` | Uses the server value. | Reactively disables collection interaction. | | `loop` | `bool` | Uses the server value. | Reactively changes arrow wrapping. | | `variant` | `"plain" | "soft" | "outline"` ([`CListboxVariant`](#listbox-interface-clistbox-variant)) | Uses the server value. | Reactively changes surface treatment. | | `size` | `"sm" | "md" | "lg"` ([`CListboxSize`](#listbox-interface-clistbox-size)) | Uses the server value. | Reactively changes Option geometry. | | `onValueChange` | `((value: string | string[] | null, detail: CListboxValueChangeDetail) => void) | undefined` | No component callback runs. | Receives selection and structural-recovery requests. |
    #### CListboxOption server inputs Server inputs are passed in a template through `` or in Python through `CListboxOption(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | required | Supplies stable unique Option identity. | | `disabled` | `bool` | `False` | Prevents focus and selection for this Option. | | `text_value` | `str | None` | `None` | Overrides normalized visible label text for typeahead. | | `class_` | `CClassValue | None` ([`CClassValue`](#listbox-interface-clistbox-class-value)) | `None` | Adds classes to the concrete Option. | | `style` | `CStyleValue | None` ([`CStyleValue`](#listbox-interface-clistbox-style-value)) | `None` | Adds inline styles to the concrete Option. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted Option attributes without replacing semantics identity focus or state. |
    #### CListboxOption client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `disabled` | `bool` | Uses the server value. | Reactively disables this Option. | | `textValue` | `string | null` | Uses the server value or visible label. | Reactively changes typeahead text. |
    #### CListboxGroup server inputs Server inputs are passed in a template through `` or in Python through `CListboxGroup(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `label` | `str` | required | Supplies the visible accessible group name. | | `class_` | `CClassValue | None` ([`CClassValue`](#listbox-interface-clistbox-class-value)) | `None` | Adds classes to the group. | | `style` | `CStyleValue | None` ([`CStyleValue`](#listbox-interface-clistbox-style-value)) | `None` | Adds inline styles to the group. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted group attributes without replacing owned semantics or label relationship. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CListbox slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CListboxDefaultSlotData`](#listbox-interface-clistbox-default-slot-data)) | None. Accepts direct CListboxOption or CListboxGroup declarations. |
    #### CListboxOption slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{value}` ([`CListboxOptionDefaultSlotData`](#listbox-interface-clistbox-option-default-slot-data)) | None. Supplies visible accessible label content. | | `start` | no | `{value, selected, disabled}` ([`CListboxOptionStateSlotData`](#listbox-interface-clistbox-option-state-slot-data)) | Omitted. Decorative leading content. | | `description` | no | `{value}` ([`CListboxOptionDescriptionSlotData`](#listbox-interface-clistbox-option-description-slot-data)) | Omitted. Supplies separately described supporting text. | | `end` | no | `{value, selected, disabled}` ([`CListboxOptionStateSlotData`](#listbox-interface-clistbox-option-state-slot-data)) | Omitted. Decorative trailing content. |
    #### CListboxGroup slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CListboxGroupDefaultSlotData`](#listbox-interface-clistbox-group-default-slot-data)) | None. Accepts one or more direct CListboxOption declarations. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CListbox events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string | string[] | null, detail: CListboxValueChangeDetail) => void` ([`CListboxValueChangeDetail`](#listbox-interface-clistbox-value-change-detail)) | Enabled pointer or keyboard selection request or settled structural recovery. | `{value, previousValue, option, selected, controlled, source, sourceEvent}` ([`CListboxValueChangeDetail`](#listbox-interface-clistbox-value-change-detail)) | Commits immediately when uncontrolled and waits for owner acceptance when controlled. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CListbox CSS variables Apply these variables to `CListbox` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-listbox-gap` | `length` | Gap between label and collection. | `0.375rem` | | `--cui-listbox-max-block-size` | `length` | Maximum scrollable collection height. | `18rem` | | `--cui-listbox-background` | `color` | Collection background. | `variant-derived Canvas surface` | | `--cui-listbox-foreground` | `color` | Collection foreground. | `CanvasText` | | `--cui-listbox-muted-color` | `color` | Disabled and secondary foreground. | `light #475467; dark #a4a7ae` | | `--cui-listbox-border-color` | `color` | Outline border. | `light #d0d5dd; dark #535862` | | `--cui-listbox-hover-background` | `color` | Enabled hover surface. | `7% CanvasText mix` | | `--cui-listbox-selected-background` | `color` | Selected Option surface. | `light #dbeafe; dark #1e3a5f` | | `--cui-listbox-selected-foreground` | `color` | Selected Option foreground. | `light #1849a9; dark #d1e9ff` | | `--cui-listbox-focus-color` | `color` | Roving focus outline. | `Highlight` | | `--cui-listbox-radius` | `length` | Collection corner radius. | `0.625rem` | | `--cui-listbox-option-padding` | `length` | Option block and inline padding. | `size-derived` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CListbox attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Collection div | `listbox` | Declares the persistent selection widget. | | `role` | Option div | `option` | Declares each selectable value. | | `role` | Group div | `group` | Groups related Options under a visible label. | | `aria-labelledby` | Collection Option or Group div | `IDREF` | Connects each semantic owner to its visible label. | | `aria-selected` | Option div | `true | false` | Reflects effective selection. | | `aria-disabled` | Collection or Option div | `true | false` | Reflects effective unavailability. | | `aria-multiselectable` | Collection div | `true` | Present only in multiple mode. | | `tabindex` | Option div | `0 | -1` | Implements one enabled roving Tab stop. | | `data-selected` | Option div | `present-or-absent` | Mirrors selected styling state. | | `data-active` | Option div | `present-or-absent` | Mirrors roving focus identity. | | `data-disabled` | Root or Option div | `present-or-absent` | Mirrors effective unavailability. | | `data-value` | Option div | `string` | Exposes canonical Option identity. | | `data-multiple` | Root div | `present-or-absent` | Mirrors multiple selection mode. | | `data-mandatory` | Root div | `present-or-absent` | Mirrors final-selection protection. | | `data-variant` | Root div | `plain | soft | outline` | Mirrors effective surface treatment. | | `data-size` | Root div | `sm | md | lg` | Mirrors effective geometry. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CListbox selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="listbox-root"]` | Root div | Stable root attrs and state surface. | | `[data-citry-ui-part="listbox-label"]` | Label span | Visible collection label. | | `[data-citry-ui-part="listbox"]` | Collection div | Semantic and scrolling selection surface. | | `[data-citry-ui-part="listbox-option"]` | Option div | Stable Option attrs focus and state surface. | | `[data-citry-ui-part="listbox-indicator"]` | Indicator span | Decorative selected-state mark. | | `[data-citry-ui-part="listbox-option-start"]` | Start span | Decorative leading content wrapper. | | `[data-citry-ui-part="listbox-option-copy"]` | Copy span | Stable label and description layout wrapper. | | `[data-citry-ui-part="listbox-option-label"]` | Label span | Visible Option name and default typeahead source. | | `[data-citry-ui-part="listbox-option-description"]` | Description span | Separately described supporting text. | | `[data-citry-ui-part="listbox-option-end"]` | End span | Decorative trailing content wrapper. | | `[data-citry-ui-part="listbox-group"]` | Group div | Stable semantic grouping surface. | | `[data-citry-ui-part="listbox-group-label"]` | Group label span | Visible group name. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CListboxValue` | `str | None | Sequence[str]` | | `CListboxVariant` | `Literal["plain", "soft", "outline"]` | | `CListboxSize` | `Literal["sm", "md", "lg"]` | | `CListboxChangeSource` | `Literal["pointer", "keyboard", "structure"]` |
    #### `CListboxDefaultSlotData` Empty dataclass: `{}`. #### `CListboxGroupDefaultSlotData` Empty dataclass: `{}`. #### `CListboxOptionDefaultSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Canonical Option identity. |
    #### `CListboxOptionStateSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Canonical Option identity. | | `selected` | `bool` | - | Server-rendered initial selected state. | | `disabled` | `bool` | - | Server-rendered initial Option disabled state. |
    #### `CListboxOptionDescriptionSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Canonical Option identity. |
    #### `CListboxValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str | list[str] | None` | - | Requested next effective value. | | `previousValue` | `str | list[str] | None` | - | Prior effective value. | | `option` | `HTMLElement | None` | - | Changed Option or None for structural recovery. | | `selected` | `bool` | - | Whether the Option is requested selected. | | `controlled` | `bool` | - | Whether the client value currently controls selection. | | `source` | `"pointer" | "keyboard" | "structure"` ([`CListboxChangeSource`](#listbox-interface-clistbox-source)) | - | Request source. | | `sourceEvent` | `Event | None` | - | Native source event or None for structure. |
    ### Translation keys - --- # MultiSelect Source: https://citry.dev/ui-library/components/multi-select/ # MultiSelect Use `CMultiSelect` when people choose several fixed values and the collection should remain compact until opened. Selected values appear as noninteractive chips. A native multiple Select preserves repeated-value form submission and reset behavior. ## MultiSelect at a glance ### MultiSelect at a glance [Open the rendered preview](/ui-library/components/multi-select/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class MultiSelectAtAGlance(Component): template = """ Workspaces Choose every workspace that should receive this observation. """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CMultiSelectOption("atlas", "Atlas research", "12 collaborators"), CMultiSelectOption("aurora", "Aurora field notes", "7 collaborators"), CMultiSelectOption("archive", "Archived studies", disabled=True), ] } preview = MultiSelectAtAGlance() preview # noqa: B018 ```` ## Submit repeated values ### Submit a MultiSelect [Open the rendered preview](/ui-library/components/multi-select/_previews/forms/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class MultiSelectForm(Component): template = """
    Reviewers Save Reset
    """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CMultiSelectOption("maya", "Maya Chen"), CMultiSelectOption("noah", "Noah Williams"), CMultiSelectOption("ines", "Inês Silva"), ] } preview = MultiSelectForm() preview # noqa: B018 ```` ## Group related options ### Group options [Open the rendered preview](/ui-library/components/multi-select/_previews/groups/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class GroupedMultiSelect(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CMultiSelectOption("oslo", "Oslo", group="Europe"), CMultiSelectOption("prague", "Prague", group="Europe"), CMultiSelectOption("kyoto", "Kyoto", group="Asia"), CMultiSelectOption("seoul", "Seoul", group="Asia"), ] } preview = GroupedMultiSelect() preview # noqa: B018 ```` ## Control selection ### Control selection [Open the rendered preview](/ui-library/components/multi-select/_previews/controlled/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class ControlledMultiSelect(Component): template = """

    Current:

    """ js = "Alpine.store('multiSelectExample', {value:['email']});" def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CMultiSelectOption("email", "Email"), CMultiSelectOption("push", "Push"), CMultiSelectOption("sms", "SMS"), ] } preview = ControlledMultiSelect() preview # noqa: B018 ```` ## Read-only and disabled states ### MultiSelect states [Open the rendered preview](/ui-library/components/multi-select/_previews/states/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class MultiSelectStates(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CMultiSelectOption("active", "Active"), CMultiSelectOption("paused", "Paused")]} preview = MultiSelectStates() preview # noqa: B018 ```` ## Close after each choice By default the popup stays open so several values can be toggled efficiently. Use `close_on_select` for workflows that should close after every change. ### Close after selection [Open the rendered preview](/ui-library/components/multi-select/_previews/close-on-select/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class CloseOnSelectMultiSelect(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CMultiSelectOption("courier", "Courier"), CMultiSelectOption("pickup", "Pickup"), CMultiSelectOption("locker", "Parcel locker"), ] } preview = CloseOnSelectMultiSelect() preview # noqa: B018 ```` ## Variants and sizes ### MultiSelect variants and sizes [Open the rendered preview](/ui-library/components/multi-select/_previews/variants/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class MultiSelectVariants(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CMultiSelectOption("one", "One"), CMultiSelectOption("two", "Two")]} preview = MultiSelectVariants() preview # noqa: B018 ```` ## Keyboard behavior Enter, Space, Down, or Up opens the Listbox. Down and Up move the highlight; Home and End jump to its edges; printable text performs buffered typeahead; Enter or Space toggles the highlighted value; Escape closes; and Tab closes while ordinary page navigation continues. ### Navigate MultiSelect [Open the rendered preview](/ui-library/components/multi-select/_previews/keyboard/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class KeyboardMultiSelect(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CMultiSelectOption("earth", "Earth"), CMultiSelectOption("mars", "Mars"), CMultiSelectOption("jupiter", "Jupiter"), ] } preview = KeyboardMultiSelect() preview # noqa: B018 ```` ## Customize MultiSelect ### Customize MultiSelect [Open the rendered preview](/ui-library/components/multi-select/_previews/customization/) ````citry import citry_ui from citry import Component, citry from citry_ui import CMultiSelectOption citry.register_library(citry_ui) class CustomizedMultiSelect(Component): css = """ .brand-select { --cui-multi-select-radius: 1rem; --cui-multi-select-selected-background: #53389e; --cui-multi-select-selected-foreground: white; --cui-multi-select-focus-color: #7f56d9; inline-size: min(100%, 22rem); } """ template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CMultiSelectOption("botany", "Botany"), CMultiSelectOption("astronomy", "Astronomy")]} preview = CustomizedMultiSelect() preview # noqa: B018 ```` ## Accessibility and forms The visible Button uses the select-only combobox pattern and keeps DOM focus while `aria-activedescendant` identifies the highlighted Option. A native multiple Select remains the repeated form value, validity, and reset truth. Before client initialization, that native control is the visible fallback. Use `CListbox(multiple=True)` for a persistent collection, `CSelect` for one compact value, and `CCombobox` when users need text filtering or custom input. ## API reference ### Inputs #### CMultiSelect server inputs Server inputs are passed in a template through `` or in Python through `CMultiSelect(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `options` | `Sequence[CMultiSelectOption]` | required | Supplies the nonempty ordered stable collection. | | `placeholder` | `str` | required | Supplies author-localized empty-value text. | | `name` | `str | None` | `None` | Sets the native form field name. | | `form` | `str | None` | `None` | Associates the native value proxy with a Form ID. | | `id` | `str | None` | `None` | Sets native proxy identity and generated relationships. | | `value` | `Sequence[str] | None` | `None` | Sets initial selected stable values in collection order. | | `open` | `bool` | `False` | Sets initial popup visibility when eligible. | | `required` | `bool | None` | `None` | Enables native required validity outside Field. | | `disabled` | `bool | None` | `None` | Disables selection and form contribution. | | `readonly` | `bool | None` | `None` | Preserves submission while preventing changes. | | `invalid` | `bool | None` | `None` | Adds owner-supplied invalid presentation. | | `loop` | `bool` | `False` | Wraps open Listbox arrow navigation. | | `close_on_select` | `bool` | `False` | Closes the popup after each accepted toggle. | | `placement` | `"bottom-start" | "bottom-end" | "top-start" | "top-end"` ([`CMultiSelectPlacement`](#multi-select-interface-placement)) | `"bottom-start"` | Sets preferred logical popup placement. | | `match_width` | `bool` | `True` | Matches the popup inline size to the control within viewport limits. | | `variant` | `"outline" | "filled" | "plain"` ([`CMultiSelectVariant`](#multi-select-interface-variant)) | `"outline"` | Selects control treatment. | | `size` | `"sm" | "md" | "lg"` ([`CMultiSelectSize`](#multi-select-interface-size)) | `"md"` | Selects control and Option geometry. | | `class_` | `CClassValue | None` ([`CClassValue`](#multi-select-interface-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#multi-select-interface-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted nonconflicting root attributes. | | `trigger_attrs` | `Mapping[str, object] | None` | `None` | Adds trusted relationships events and accessible naming to the combobox Button. | | `listbox_attrs` | `Mapping[str, object] | None` | `None` | Adds trusted nonconflicting Listbox attributes. |
    #### CMultiSelect client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string[] | null` | Releases control to committed selection. | Controls the selected collection while supplied; an empty array is controlled empty. | | `open` | `boolean | null` | Releases control to committed visibility. | Controls popup visibility while supplied. | | `required` | `bool` | Uses the server or Field fallback. | Reactively changes required validity. | | `disabled` | `bool` | Uses the server or Field fallback. | Reactively disables selection. | | `readonly` | `bool` | Uses the server or Field fallback. | Reactively prevents changes while preserving submission. | | `invalid` | `bool` | Uses the server or Field fallback. | Reactively changes invalid presentation. | | `loop` | `bool` | Uses the server value. | Reactively changes arrow wrapping. | | `closeOnSelect` | `bool` | Uses the server value. | Reactively changes whether a toggle closes the popup. | | `placement` | `CMultiSelectPlacement` | Uses the server value. | Reactively changes preferred placement. | | `matchWidth` | `bool` | Uses the server value. | Reactively changes popup sizing. | | `variant` | `CMultiSelectVariant` | Uses the server value. | Reactively changes treatment. | | `size` | `CMultiSelectSize` | Uses the server value. | Reactively changes geometry. | | `onValueChange` | `((value: string[], detail: CMultiSelectValueChangeDetail) => void) | undefined` | No component callback runs. | Receives toggle reset and structural requests. | | `onOpenChange` | `((open: boolean, detail: CMultiSelectOpenChangeDetail) => void) | undefined` | No component callback runs. | Receives visibility requests and forced-close notices. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CMultiSelect events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string[], detail: CMultiSelectValueChangeDetail) => void` ([`CMultiSelectValueChangeDetail`](#multi-select-interface-cmulti-select-value-change-detail)) | Enabled toggle reset or structural recovery. | `{value, previousValue, option, selected, controlled, source, sourceEvent}` ([`CMultiSelectValueChangeDetail`](#multi-select-interface-cmulti-select-value-change-detail)) | Commits immediately when uncontrolled and waits for owner acceptance when controlled. | | `onOpenChange` | `(open: boolean, detail: CMultiSelectOpenChangeDetail) => void` ([`CMultiSelectOpenChangeDetail`](#multi-select-interface-cmulti-select-open-change-detail)) | Visibility request or nonrejectable safety close. | `{open, reason, controlled, forced, source}` ([`CMultiSelectOpenChangeDetail`](#multi-select-interface-cmulti-select-open-change-detail)) | Controlled requests notify without changing visibility; forced safety closes always hide. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CMultiSelect CSS variables Apply these variables to `CMultiSelect` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-multi-select-background` | `color` | Control and popup surface. | `Canvas` | | `--cui-multi-select-foreground` | `color` | Primary foreground. | `CanvasText` | | `--cui-multi-select-placeholder-color` | `color` | Empty-value foreground. | `scheme-aware muted` | | `--cui-multi-select-muted-color` | `color` | Description and disabled foreground. | `scheme-aware muted` | | `--cui-multi-select-border-color` | `color` | Outline border. | `scheme-aware subtle border` | | `--cui-multi-select-hover-background` | `color` | Highlighted Option surface. | `CanvasText mix` | | `--cui-multi-select-selected-background` | `color` | Selected Option surface. | `scheme-aware blue` | | `--cui-multi-select-selected-foreground` | `color` | Selected Option foreground. | `scheme-aware blue text` | | `--cui-multi-select-chip-background` | `color` | Selected-value chip surface. | `CanvasText mix` | | `--cui-multi-select-chip-foreground` | `color` | Selected-value chip foreground. | `CanvasText` | | `--cui-multi-select-focus-color` | `color` | Focus outline. | `Highlight` | | `--cui-multi-select-radius` | `length` | Control and popup corners. | `0.625rem` | | `--cui-multi-select-control-padding` | `length` | Control padding. | `size-derived` | | `--cui-multi-select-option-padding` | `length` | Option padding. | `size-derived` | | `--cui-multi-select-max-block-size` | `length` | Popup scroll boundary. | `18rem` | | `--cui-multi-select-offset` | `length` | Anchor gap. | `0.25rem` | | `--cui-multi-select-shadow` | `shadow` | Popup elevation. | `scheme-aware shadow` | | `--cui-multi-select-duration` | `time` | Indicator rotation motion. | `120ms` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CMultiSelect attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Control Button | `combobox` | Declares the select-only popup control. | | `role` | Listbox div | `listbox` | Declares the popup collection. | | `aria-multiselectable` | Listbox div | `true` | Declares independent multiple selection. | | `role` | Option div | `option` | Declares each value. | | `aria-expanded` | Control Button | `true | false` | Reflects popup visibility. | | `aria-controls` | Control Button | `IDREF` | Targets the Listbox. | | `aria-activedescendant` | Control Button | `IDREF or absent` | Identifies the highlighted open Option. | | `aria-required` | Control Button | `true or absent` | Mirrors effective required state. | | `aria-disabled` | Control Button | `true or absent` | Mirrors effective unavailability. | | `aria-readonly` | Control Button | `true or absent` | Mirrors read-only interaction. | | `aria-invalid` | Control Button | `true or absent` | Mirrors effective invalid presentation. | | `aria-selected` | Option div | `true | false` | Reflects effective selection. | | `data-open` | Root div | `present-or-absent` | Mirrors effective visibility. | | `data-empty` | Root div | `present-or-absent` | Mirrors no selected value. | | `data-required` | Root div | `present-or-absent` | Mirrors effective required state. | | `data-readonly` | Root div | `present-or-absent` | Mirrors read-only interaction. | | `data-invalid` | Root div | `present-or-absent` | Mirrors effective invalid presentation. | | `data-close-on-select` | Root div | `present-or-absent` | Mirrors close-after-toggle behavior. | | `data-match-width` | Root div | `present-or-absent` | Mirrors popup width matching. | | `data-variant` | Root div | `outline | filled | plain` | Mirrors effective treatment. | | `data-size` | Root div | `sm | md | lg` | Mirrors effective geometry. | | `data-value` | Option div | `string` | Exposes stable identity. | | `data-selected` | Option div | `present-or-absent` | Mirrors selection. | | `data-highlighted` | Option div | `present-or-absent` | Mirrors active descendant. | | `data-disabled` | Root or Option div | `present-or-absent` | Mirrors effective unavailability. | | `data-placement` | Popup div | `CMultiSelectPlacement` | Reflects preferred logical placement. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CMultiSelect selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="root"]` | Root div | Stable root attrs and state surface. | | `[data-citry-ui-part="control"]` | Combobox Button | Visible control and focus owner. | | `[data-citry-ui-part="values"]` | Values span | Selected chips or placeholder. | | `[data-citry-ui-part="placeholder"]` | Placeholder span | Empty-selection copy. | | `[data-citry-ui-part="chip"]` | Chip span | Noninteractive selected-value label. | | `[data-citry-ui-part="indicator"]` | Indicator span | Decorative popup-state mark. | | `[data-citry-ui-part="popup"]` | Manual popover div | Top-layer scrolling surface. | | `[data-citry-ui-part="listbox"]` | Listbox div | Semantic collection. | | `[data-citry-ui-part="group"]` | Group div | Related Options. | | `[data-citry-ui-part="group-label"]` | Group label span | Visible group name. | | `[data-citry-ui-part="option"]` | Option div | Value semantics and state. | | `[data-citry-ui-part="option-label"]` | Option label span | Accessible Option name. | | `[data-citry-ui-part="option-description"]` | Option description span | Supporting description. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CMultiSelectPlacement` | `Literal["bottom-start", "bottom-end", "top-start", "top-end"]` | | `CMultiSelectVariant` | `Literal["outline", "filled", "plain"]` | | `CMultiSelectSize` | `Literal["sm", "md", "lg"]` | | `CMultiSelectChangeSource` | `Literal["pointer", "keyboard", "reset", "structure"]` | | `CMultiSelectOpenReason` | `Literal["trigger", "keyboard", "selection", "escape", "tab", "outside", "focus-outside", "reset", "native", "ancestor"]` |
    #### `CMultiSelectOption`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Stable unique form value. | | `label` | `str` | - | Visible accessible Option name. | | `description` | `str | None` | - | Optional separately described supporting text. | | `disabled` | `bool` | - | Prevents user selection. | | `group` | `str | None` | - | Optional contiguous visible group label. |
    #### `CMultiSelectValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string[]` | - | Requested copied value collection. | | `previousValue` | `string[]` | - | Previous copied effective collection. | | `option` | `HTMLElement | None` | - | Activated Option or None for reset and structure. | | `selected` | `bool` | - | Resulting selected state for the activated Option. | | `controlled` | `bool` | - | Whether client value owns selection. | | `source` | `CMultiSelectChangeSource` | - | Request source. | | `sourceEvent` | `Event | None` | - | Native source event when present. |
    #### `CMultiSelectOpenChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `open` | `bool` | - | Requested or forced visibility. | | `reason` | `CMultiSelectOpenReason` | - | Visibility reason. | | `controlled` | `bool` | - | Whether client open owns visibility. | | `forced` | `bool` | - | Whether safety made the close nonrejectable. | | `source` | `EventTarget | None` | - | Native source or safety owner. |
    ### Translation keys - --- # Native Select Source: https://citry.dev/ui-library/components/native-select/ # Native Select Use `CNativeSelect` for one choice from a finite server-owned list. It renders one native Select element, so keyboards, touch pickers, autofill, forms, validation, and reset keep their browser behavior. ## Native Select at a glance ### Native Select at a glance [Open the rendered preview](/ui-library/components/native-select/_previews/at-a-glance/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectGroup, CNativeSelectOption citry.register_library(citry_ui) class NativeSelectAtAGlance(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "habitats": [ CNativeSelectGroup( "Coastal", [ CNativeSelectOption("kelp", "Kelp forest"), CNativeSelectOption("reef", "Coral reef"), CNativeSelectOption("mangrove", "Mangrove nursery"), ], ), CNativeSelectGroup( "Open ocean", [ CNativeSelectOption("pelagic", "Pelagic zone"), CNativeSelectOption("abyss", "Abyssal plain"), ], ), ], } template = """
    Primary habitat Choose the habitat represented by this dive.
    Unverified station Match this station to a surveyed habitat.
    """ css = """ :where(.ocean-glance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 1rem; max-width: 54rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.ocean-glance > *) { padding: 1rem; border: 1px solid light-dark(#9ccbd3, #315f6a); border-radius: 0.875rem; background: light-dark(#f1fbfc, #10272d); } :where(.ocean-glance__deep) { --cui-native-select-background: #142f36; --cui-native-select-border-color: #57828c; --cui-native-select-focus-color: #7ddbea; } """ preview = NativeSelectAtAGlance() preview # noqa: B018 ```` ## Compose a labelled Select Put Native Select inside `CField` when it needs a label, description, error, or composed state. Pass options as `CNativeSelectOption` and `CNativeSelectGroup` records. ### Compose Native Select in templates and Python [Open the rendered preview](/ui-library/components/native-select/_previews/compose-select/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelect, CNativeSelectOption citry.register_library(citry_ui) class ComposeNativeSelect(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 options = [ CNativeSelectOption("north", "North transect"), CNativeSelectOption("central", "Central transect"), CNativeSelectOption("south", "South transect"), ] return { "options": options, "python_select": CNativeSelect( options=options, id="python-transect", name="python_transect", value="central", ), } template = """
    Template-composed transect
    {{ python_select }}
    """ css = """ :where(.ocean-compose) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.ocean-compose__label) { display: block; margin-block-end: 0.5rem; font-weight: 650; } """ preview = ComposeNativeSelect() preview # noqa: B018 ```` Outside `CField`, provide a native label or accessible name yourself. `CNativeSelect` has no slots or child content. ## Build options and groups Option values are stable form and morph identities. They must be unique and nonempty. Groups preserve order, cannot nest, and may disable all their options. ### Use flat options, groups, and disabled choices [Open the rendered preview](/ui-library/components/native-select/_previews/options-and-groups/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectGroup, CNativeSelectOption citry.register_library(citry_ui) class NativeSelectOptions(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "regions": [ CNativeSelectOption("harbor", "Research harbor"), CNativeSelectGroup( "Continental shelf", [ CNativeSelectOption("bank", "Emerald Bank"), CNativeSelectOption("canyon", "Bluefin Canyon"), CNativeSelectOption("closure", "Seasonal closure", disabled=True), ], ), CNativeSelectGroup( "Weather hold", [CNativeSelectOption("offshore", "Offshore station")], disabled=True, ), ], } template = """
    Expedition region Closed choices remain visible but unavailable.
    """ css = """ :where(.ocean-options) { max-width: 34rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = NativeSelectOptions() preview # noqa: B018 ```` Labels are plain text. Put rich rows or search in `CSelect`, `CMultiSelect`, or `CListbox` rather than native options. Remote data and virtualization still need application ownership or a later dedicated collection family. ## Prompt and require a choice `placeholder` inserts the first empty-value option. It is also required for a conforming required single Select. ### Compare optional and required destinations [Open the rendered preview](/ui-library/components/native-select/_previews/placeholder-and-required/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class NativeSelectPlaceholder(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "destinations": [ CNativeSelectOption("lagoon", "Lagoon station"), CNativeSelectOption("shelf", "Shelf station"), CNativeSelectOption("slope", "Continental slope"), ], } template = """ Required destination Choose a destination before departure. Optional backup
    Validate route Reset
    """ css = """ :where(.ocean-placeholders) { display: grid; gap: 1rem; max-width: 36rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.ocean-placeholders__actions) { display: flex; flex-wrap: wrap; gap: 0.75rem; } """ preview = NativeSelectPlaceholder() preview # noqa: B018 ```` An empty string selects an existing placeholder. Without a placeholder, `None` leaves native initial selection to the first enabled option. ## Choose a variant `outline`, `filled`, and `plain` change the closed-control treatment without changing the native picker. ### Compare Native Select variants [Open the rendered preview](/ui-library/components/native-select/_previews/variants/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class NativeSelectVariants(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "variants": ("outline", "filled", "plain"), "depths": [ CNativeSelectOption("surface", "Surface"), CNativeSelectOption("twilight", "Twilight zone"), CNativeSelectOption("midnight", "Midnight zone"), ], } template = """
    {{ variant.title() }}
    """ css = """ :where(.ocean-variants) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = NativeSelectVariants() preview # noqa: B018 ```` ## Choose a size `sm`, `md`, and `lg` adjust visual padding and text size. This `size` is not the native listbox-size attribute, which the component rejects. ### Compare Native Select sizes [Open the rendered preview](/ui-library/components/native-select/_previews/sizes/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class NativeSelectSizes(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "sizes": ("sm", "md", "lg"), "vessels": [ CNativeSelectOption("tern", "Tern"), CNativeSelectOption("albatross", "Albatross"), CNativeSelectOption( "bathyscaphe", "Bathyscaphe for the long continental-slope transect", ), ], } template = """
    {{ size.upper() }} vessel control
    """ css = """ :where(.ocean-sizes) { display: grid; gap: 1rem; max-width: 34rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = NativeSelectSizes() preview # noqa: B018 ```` ## Use Field and Form states Required, disabled, and invalid keep their native differences. Native Select does not simulate read-only behavior: a Field requesting read-only rejects this control instead of presenting an editable control as locked. ### Compare survey states [Open the rendered preview](/ui-library/components/native-select/_previews/field-states/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class NativeSelectStates(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "stations": [ CNativeSelectOption("alpha", "Station Alpha"), CNativeSelectOption("beta", "Station Beta"), CNativeSelectOption("gamma", "Station Gamma"), ], } template = """
    Required station Closed station Unverified station Confirm the station with bridge control. Survey locked by Form
    """ css = """ :where(.ocean-states) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = NativeSelectStates() preview # noqa: B018 ```` ## Control browser selection Supply client `value` through `$c-props` to control current selection. Mirror the native `input` event to accept user choices. Omit the prop to release control without replacing a valid browser-owned selection. ### Control and release a vessel assignment [Open the rendered preview](/ui-library/components/native-select/_previews/controlled-selection/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class ControlledNativeSelect(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "vessels": [ CNativeSelectOption("calypso", "Calypso"), CNativeSelectOption("nautilus", "Nautilus"), CNativeSelectOption("aronnax", "Aronnax"), ], } template = """
    Survey vessel
    Release Assign Calypso Clear
    """ css = """ :where(.ocean-controlled) { display: grid; gap: 1rem; max-width: 36rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.ocean-controlled__actions) { display: flex; flex-wrap: wrap; gap: 0.75rem; } """ preview = ControlledNativeSelect() preview # noqa: B018 ```` Client `null` selects the placeholder when present, otherwise it means no selection. Invalid or disabled controlled values report once and follow the documented fallback. Native Select adds no value-change callback or custom DOM event. ## Keep the platform picker Citry UI styles the closed root. The browser or operating system owns the open picker, including its layout, scrolling, dismissal, touch behavior, and assistive-technology presentation. ### Use native focus, events, and external Form ownership [Open the rendered preview](/ui-library/components/native-select/_previews/native-picker/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class NativePickerBoundary(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "currents": [ CNativeSelectOption("north", "North Equatorial Current"), CNativeSelectOption("counter", "Equatorial Countercurrent"), CNativeSelectOption("south", "South Equatorial Current"), ], } template = """
    Open native picker counter
    """ css = """ :where(.ocean-picker) { display: grid; gap: 0.75rem; max-width: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.ocean-picker__actions) { display: flex; align-items: center; gap: 1rem; } :where(.ocean-picker output) { font-family: ui-monospace, monospace; } """ preview = NativePickerBoundary() preview # noqa: B018 ```` Listen to native `input`, `change`, focus, and invalid events directly. Consumers may call native methods such as `focus()` and, where supported, `showPicker()` on the root ref. The component does not promise the open picker's DOM or styling. ## Customize the theme Override public variables on an ancestor or one Select. Use the stable part selector for targeted rules. ### Theme two expedition controls [Open the rendered preview](/ui-library/components/native-select/_previews/theme-customization/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNativeSelectOption citry.register_library(citry_ui) class NativeSelectThemes(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "instruments": [ CNativeSelectOption("ctd", "CTD profiler"), CNativeSelectOption("sonar", "Multibeam sonar"), CNativeSelectOption("rov", "Remotely operated vehicle"), ], } template = """
    Lagoon instrument
    Trench instrument
    """ css = """ :where(.ocean-themes) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.ocean-themes > div) { padding: 1rem; border-radius: 1rem; } :where(.ocean-themes__lagoon) { --cui-native-select-background: #f3feff; --cui-native-select-foreground: #103f49; --cui-native-select-border-color: #5ca4b0; --cui-native-select-hover-border-color: #236b78; --cui-native-select-focus-color: #087e8b; --cui-native-select-placeholder-color: #52717a; --cui-native-select-radius: 1rem; --cui-native-select-indicator-size: 0.5rem; background: #dff4f6; } :where(.ocean-themes__trench) { --cui-native-select-background: #10262d; --cui-native-select-foreground: #e0f7fa; --cui-native-select-border-color: #527b86; --cui-native-select-hover-border-color: #83bbc6; --cui-native-select-focus-color: #76e4f7; --cui-native-select-placeholder-color: #a1bdc3; --cui-native-select-radius: 0.25rem; background: #08191e; } :where(.ocean-themes__root[data-citry-ui-part="native-select"]:focus-visible) { outline-style: double; } """ preview = NativeSelectThemes() preview # noqa: B018 ```` `class_` and `style` target the native root. Unlayered consumer CSS overrides the low-specificity defaults; named layers follow the site-wide Citry UI layer ordering contract. ## Accessibility and trust Keep a visible label even when placeholder text is present. Native Select adds no role, focus proxy, or keyboard handler. Labels, values, names, IDs, and autocomplete hints render as plain text, including trusted-string subclasses. `attrs`, `class_`, `style`, and option/group `attrs` remain trusted code surfaces for unowned native, ARIA, data, and Alpine attributes. Use `attrs={"form": "survey"}` for an external native Form owner. That Form element and ID must remain stable for one Select initialization; rerender the Select when ownership changes. Dynamic `form` bindings and duplicate case-insensitive spellings are rejected. ## API reference ### Inputs #### CNativeSelect server inputs Server inputs are passed in a template through `` or in Python through `CNativeSelect(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `options` | `Sequence[CNativeSelectItem]` ([`CNativeSelectItem`](#native-select-interface-native-select-item)) | required | Sets the finite ordered options and one-level groups; canonical option values must be unique. | | `name` | `non-empty str | None` | `None` | Sets the native submitted name; an unnamed Select contributes no `FormData` entry. | | `id` | `str | None` | generated | Uses the Field control ID when composed, otherwise sets or generates native identity. | | `value` | `str | None` | `None` | Sets initial and reset selection; `None` or `""` selects an existing placeholder, while `None` without one uses native first-option selection. | | `placeholder` | `non-empty str | None` | `None` | Inserts the enabled first empty-value option and enables native required support. | | `required` | `bool | None` | `None` | Sets native required state when standalone and requires `placeholder`; omit it inside `CField`, which owns the state. | | `disabled` | `bool | None` | `None` | Sets local disabled state when standalone; disabled `CForm` always wins. | | `invalid` | `bool | None` | `None` | Sets application invalid state when standalone; omit it inside `CField`. | | `autocomplete` | `str | None` | `None` | Sets the native autofill hint. | | `variant` | `"outline" | "filled" | "plain"` ([`CNativeSelectVariant`](#native-select-interface-native-select-variant)) | `"outline"` | Selects presentation. | | `size` | `"sm" | "md" | "lg"` ([`CNativeSelectSize`](#native-select-interface-native-select-size)) | `"md"` | Selects visual padding and text size; this is not native Select `size`. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#native-select-interface-native-select-class-value)) | `None` | Adds native-root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#native-select-interface-native-select-style-value)) | `None` | Adds native-root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native Form ownership, ARIA, data, and trusted Alpine attributes not owned by explicit inputs. |
    #### CNativeSelect client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string | null` | Releases control and preserves the semantic native selection. | Controls current selection; `null` selects the placeholder or no selection, and `""` selects an existing placeholder. | | `required` | `boolean` | Uses the server or Field value. | Controls required state when standalone; true requires a placeholder and `CField` owns the input when composed. | | `disabled` | `boolean` | Uses the server, Field, or Form value. | Controls local disabled state when standalone; disabled `CForm` always wins. | | `invalid` | `boolean` | Uses the server or Field value. | Controls application invalid state; native invalidity still combines with it. | | `variant` | `"outline" | "filled" | "plain"` ([`CNativeSelectVariant`](#native-select-interface-native-select-variant)) | Uses the server input. | Controls presentation. | | `size` | `"sm" | "md" | "lg"` ([`CNativeSelectSize`](#native-select-interface-native-select-size)) | Uses the server input. | Controls visual padding and text size. |
    ### Slots - ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CNativeSelect CSS variables Apply these variables to `CNativeSelect` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-native-select-background` | `color` | Native closed-control background. | `Canvas, variant adjusted` | | `--cui-native-select-foreground` | `color` | Selected text and indicator. | `CanvasText` | | `--cui-native-select-border-color` | `color` | Resting border. | `Subtle CanvasText mix, variant adjusted` | | `--cui-native-select-hover-border-color` | `color` | Hover border. | `Stronger CanvasText mix.` | | `--cui-native-select-focus-color` | `color` | Focus outline and border. | `Highlight` | | `--cui-native-select-invalid-border-color` | `color` | Invalid border. | `Scheme-aware negative color.` | | `--cui-native-select-disabled-background` | `color` | Disabled background. | `Subtle CanvasText/Canvas mix.` | | `--cui-native-select-placeholder-color` | `color` | Empty placeholder text and indicator. | `Muted CanvasText mix.` | | `--cui-native-select-radius` | `length` | Corner radius. | `0.5rem; 0 for plain` | | `--cui-native-select-inline-padding` | `length` | Logical inline padding. | `Size-derived length.` | | `--cui-native-select-block-padding` | `length` | Logical block padding. | `Size-derived length.` | | `--cui-native-select-font-size` | `length` | Closed-control text size. | `Size-derived length.` | | `--cui-native-select-indicator-size` | `length` | Each indicator triangle and its reserved inline space. | `0.4rem` | | `--cui-native-select-indicator-gap` | `length` | Logical gap between the indicator and root edge. | `0.75rem` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CNativeSelect attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-required` | Native Select | `present | absent` | Mirrors effective required state. | | `data-disabled` | Native Select | `present | absent` | Mirrors effective disabled state. | | `data-invalid` | Native Select | `present | absent` | Mirrors combined application and native invalid state. | | `data-empty` | Native Select | `present | absent` | Marks a selected placeholder or semantic no-selection. | | `data-variant` | Native Select | `"outline" | "filled" | "plain"` | Mirrors effective presentation variant. | | `data-size` | Native Select | `"sm" | "md" | "lg"` | Mirrors effective visual size. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CNativeSelect selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="native-select"]` | Native Select | Stable root, styling hook, and `attrs` destination. |
    ### 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]` | | `CNativeSelectVariant` | `Literal["outline", "filled", "plain"]` | | `CNativeSelectSize` | `Literal["sm", "md", "lg"]` | | `CNativeSelectItem` | `CNativeSelectOption | CNativeSelectGroup` |
    #### `CNativeSelectOption`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Unique nonempty canonical submitted value; CR and CRLF normalize to LF, and U+0000 is invalid. | | `label` | `str` | - | Nonempty plain-text option label. | | `disabled` | `bool` | False | Disables native selection. | | `attrs` | `Mapping[str, object] | None` | None | Adds trusted unowned native option attributes. |
    #### `CNativeSelectGroup`
    | Field | Type | Default | Meaning | |---|---|---|---| | `label` | `str` | - | Nonempty plain-text group label. | | `options` | `Sequence[CNativeSelectOption]` | - | Ordered direct options; groups cannot nest. | | `disabled` | `bool` | False | Disables every option in the native group. | | `attrs` | `Mapping[str, object] | None` | None | Adds trusted unowned native optgroup attributes. |
    ### Translation keys - --- # NumberInput Source: https://citry.dev/ui-library/components/number-input/ # NumberInput Use `CNumberInput` for a quantity where incrementing and decrementing make sense: item counts, measurements, thresholds, or bounded settings. Its public value is an exact canonical decimal string, so `0.1` stays `0.1` instead of becoming a JavaScript binary-float approximation. Use `CPinInput` for one-time codes and identifiers. A credit-card number, postal code, account number, or phone number is text, not a quantity. ## Edit a quantity Compose NumberInput inside `CField` for a visible label, description, error, and shared state. ```citry-html Crates Choose from 1 through 20. ``` ### Edit and submit a quantity [Open the rendered preview](/ui-library/components/number-input/_previews/basic/) ````citry from decimal import Decimal from typing import Any import citry_ui from citry import Component, citry from citry_ui import CNumberInput citry.register_library(citry_ui) class BasicNumberInput(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "python_control": CNumberInput( name="threshold", value=Decimal("2.5"), min=Decimal(0), max=Decimal(10), step=Decimal("0.5"), input_attrs={"aria-label": "Python threshold"}, ) } template = """
    Crates Choose from 1 through 20.

    Python composition

    {{ python_control }}
    """ css = """ :where(.number-input-demo-grid) { display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;align-items:start } :where(.number-input-demo-grid article) { display:grid;gap:.75rem } :where(.number-input-demo-grid h3) { margin:0 } """ preview = BasicNumberInput() preview # noqa: B018 ```` Standalone use needs an accessible name in `input_attrs`. ## Keep decimals exact Server inputs accept `int`, `Decimal`, or a plain-decimal string. Floats, scientific notation, NaN, and infinity are rejected. Client `value` is a canonical string or `null`. ### Step exact fractional values [Open the rendered preview](/ui-library/components/number-input/_previews/exact-decimals/) ````citry from citry import Component class ExactDecimalNumberInput(Component): template = """
    Calibration offset Exact increments of 0.0001.

    The submitted enhanced value remains the exact string 0.1001.

    """ css = ":where(.number-input-example-stack){display:grid;gap:.75rem;max-inline-size:28rem}" preview = ExactDecimalNumberInput() preview # noqa: B018 ```` `step` sets an exact grid based on `min`, or zero when `min` is omitted. Arrow keys move one step, Page Up and Page Down move ten, and Home/End use a supplied minimum/maximum. The adjacent Buttons do not add Tab stops. ## Validate or clamp a committed draft The default `commit_behavior="validate"` leaves an out-of-range or off-grid draft visible and invalid. Set `commit_behavior="clamp"` to clamp a parse-valid out-of-range draft on blur or Enter. Clamp never guesses an incomplete or malformed value. ### Compare validation and clamping [Open the rendered preview](/ui-library/components/number-input/_previews/constraints/) ````citry from citry import Component class NumberInputConstraints(Component): template = """
    Validate the draft Enter a quarter step from 0 through 3. Clamp on commit A parse-valid outside value moves to the nearest bound.
    """ css = """ :where(.number-input-example-grid) { display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem } """ preview = NumberInputConstraints() preview # noqa: B018 ```` `invalid=True` combines application validation with required, parse, minimum, maximum, and step validity. Inside Field, set `required`, `disabled`, `readonly`, and `invalid` on Field rather than on NumberInput. ## Control the canonical value Pass client `value` and `onValueChange` through `$c-props`. A controlled interaction is a request: the displayed committed value and Form transport do not change until the owner supplies the requested exact string. ### Control exact value ownership [Open the rendered preview](/ui-library/components/number-input/_previews/controlled/) ````citry from citry import Component class ControlledNumberInput(Component): template = """
    Canonical value: 2
    """ css = ":where(.number-input-example-stack){display:grid;gap:.75rem;max-inline-size:28rem}" preview = ControlledNumberInput() preview # noqa: B018 ```` `onInputValueChange` reports the literal draft and its `empty`, `incomplete`, `invalid`, or `valid` parse status. It does not make the draft a second controlled axis. Native `@input` also remains available through `input_attrs`. ## Hide controls or enable wheel stepping Set `show_controls=False` for a text-only spinbutton. Keyboard stepping remains available. Mouse-wheel and trackpad stepping are disabled by default so page scrolling cannot accidentally change a value; opt in with `wheel=True`. ### Use a compact text-only spinbutton [Open the rendered preview](/ui-library/components/number-input/_previews/without-controls/) ````citry from citry import Component class NumberInputWithoutControls(Component): template = """ Keyboard stepper Use Arrow Up/Down; adjacent controls are hidden. """ preview = NumberInputWithoutControls() preview # noqa: B018 ```` ### Opt in to focused wheel stepping [Open the rendered preview](/ui-library/components/number-input/_previews/wheel/) ````citry from citry import Component class WheelNumberInput(Component): template = """
    Wheel remains page scrolling Focused wheel changes value Explicitly enabled for this control.
    """ css = """ :where(.number-input-example-grid) { display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem } """ preview = WheelNumberInput() preview # noqa: B018 ```` ## Use localized decimal editing With configured Citry i18n, the server formats the initial value through the `citry-ui-number-input` number profile. Under a client-enabled `` provider, the editor accepts that locale's digits, decimal separator, grouping, and signs and reformats an idle value after a live locale change. ### Inspect locale-aware NumberInput composition [Open the rendered preview](/ui-library/components/number-input/_previews/locales/) ````citry from citry import Component class LocalizedNumberInput(Component): template = """

    Place the same component under a client-enabled <c-i18n> provider to switch locale in place.

    The editor and its ARIA value text use the provider locale; the enhanced Form value stays 1234.5.

    """ css = ":where(.number-input-example-stack){display:grid;gap:.75rem;max-inline-size:32rem}" preview = LocalizedNumberInput() preview # noqa: B018 ```` Without i18n configuration, the exact source format is canonical ASCII. If a page uses server-only localized i18n, NumberInput keeps the localized SSR text until focus and then exposes the separately shipped canonical value; it never guesses which punctuation the server rendered. An application may override every library-authored label or validity message. An explicit override stays fixed during locale switches and creates no catalog binding. ## Preserve native Form behavior Without JavaScript, the visible text input owns `name` and submits its literal localized value for server parsing. After enhancement, an owned hidden input submits the canonical decimal while the visible editor owns native validity. ### Submit and reset canonical values [Open the rendered preview](/ui-library/components/number-input/_previews/forms/) ````citry from citry import Component class NumberInputForms(Component): template = """
    Amount
    Not submitted
    """ css = ":where(.number-input-example-stack){display:grid;gap:.75rem;max-inline-size:28rem}" preview = NumberInputForms() preview # noqa: B018 ```` Readonly values remain focusable and submit. Disabled values do not submit. An uncanceled reset restores the server value; controlled state receives a reset request. ## Choose a variant, size, and public style Outline, filled, and plain variants combine with sm, md, and lg sizes. Public `--cui-number-input-*` variables and `[data-citry-ui-part="..."]` selectors customize the stable root, control, editor, and step Buttons. ### Compare NumberInput states and styling [Open the rendered preview](/ui-library/components/number-input/_previews/states/) ````citry from citry import Component class NumberInputStates(Component): template = """
    """ css = """ :where(.number-input-state-grid) { display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1rem;align-items:start } """ preview = NumberInputStates() preview # noqa: B018 ```` Logical CSS supports RTL while plus and minus keep their mathematical meaning. Coarse pointers receive larger targets; forced colors preserve borders and focus; print hides the controls. ## API reference ### Inputs #### CNumberInput server inputs Server inputs are passed in a template through `` or in Python through `CNumberInput(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `CNumberInputExact | None` ([`CNumberInputExact`](#number-input-interface-exact)) | `None` | Sets the initial exact canonical decimal or empty value. | | `name` | `str | None` | `None` | Sets the progressive native Form field name. | | `form` | `str | None` | `None` | Associates the visible fallback and enhanced transport with an external Form ID. | | `id` | `str | None` | generated | Sets the public editor ID and bases the private transport ID. | | `min` | `CNumberInputExact | None` ([`CNumberInputExact`](#number-input-interface-exact)) | `None` | Sets the inclusive exact minimum and step-grid base. | | `max` | `CNumberInputExact | None` ([`CNumberInputExact`](#number-input-interface-exact)) | `None` | Sets the inclusive exact maximum. | | `step` | `CNumberInputExact` ([`CNumberInputExact`](#number-input-interface-exact)) | `1` | Sets a positive exact step. | | `required` | `bool | None` | `None` | Enables empty-value validity outside Field; Field owns it inside Field. | | `disabled` | `bool | None` | `None` | Blocks focus mutation and Form submission outside Field; Form disabledness also wins. | | `readonly` | `bool | None` | `None` | Keeps a focusable submitted value while blocking mutation. | | `invalid` | `bool | None` | `None` | Adds application invalid state to native component validity. | | `show_controls` | `bool` | `True` | Shows or hides adjacent decrement and increment Buttons. | | `wheel` | `bool` | `False` | Opts a focused editor into wheel and trackpad stepping. | | `commit_behavior` | `"validate" | "clamp"` ([`CNumberInputCommitBehavior`](#number-input-interface-commit-behavior)) | `"validate"` | Leaves an invalid committed draft visible or clamps a parse-valid out-of-range value. | | `placeholder` | `str | None` | `None` | Sets ordinary editor placeholder text. | | `autocomplete` | `str | None` | `None` | Sets the native autocomplete hint. | | `increment_label` | `str` | `"Increase value"` | Overrides the catalog-backed increment Button accessible name. | | `decrement_label` | `str` | `"Decrease value"` | Overrides the catalog-backed decrement Button accessible name. | | `required_message` | `str` | `"Enter a number."` | Overrides catalog-backed empty required validity. | | `invalid_message` | `str` | `"Enter a valid number."` | Overrides catalog-backed parse validity. | | `minimum_message` | `str containing '{min}'` | `"Enter a value of at least {min}."` | Overrides catalog-backed minimum validity. | | `maximum_message` | `str containing '{max}'` | `"Enter a value of at most {max}."` | Overrides catalog-backed maximum validity. | | `step_message` | `str containing '{step}'` | `"Enter a value in increments of {step}."` | Overrides catalog-backed step-grid validity. | | `variant` | `"outline" | "filled" | "plain"` ([`CNumberInputVariant`](#number-input-interface-variant)) | `"outline"` | Selects visual treatment. | | `size` | `"sm" | "md" | "lg"` ([`CNumberInputSize`](#number-input-interface-size)) | `"md"` | Selects coordinated editor and control sizing. | | `class_` | `CClassValue | None` ([`CClassValue`](#number-input-interface-class-value)) | `None` | Adds classes to the documented root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#number-input-interface-style-value)) | `None` | Adds styles to the documented root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing owned state or runtime identity. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed editor attributes including accessible naming and native event observers. |
    #### CNumberInput client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `canonical string | null` | Releases control to the last uncontrolled committed value. | Controls the exact canonical value while supplied. | | `min` | `canonical string | null` | Uses the server minimum. | Replaces or removes the inclusive minimum. | | `max` | `canonical string | null` | Uses the server maximum. | Replaces or removes the inclusive maximum. | | `step` | `positive canonical string` | Uses the server step. | Replaces the exact step grid. | | `required` | `boolean` | Uses server or Field state. | Controls standalone required validity. | | `disabled` | `boolean` | Uses server or owner state. | Controls mutation and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable state. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `showControls` | `boolean` | Uses the server input. | Controls adjacent Button visibility. | | `wheel` | `boolean` | Uses the server input. | Controls focused wheel stepping. | | `commitBehavior` | `"validate" | "clamp"` ([`CNumberInputCommitBehavior`](#number-input-interface-commit-behavior)) | Uses the server input. | Controls out-of-range commit policy. | | `placeholder` | `string | null` | Uses the server input. | Controls visible placeholder text. | | `autocomplete` | `string | null` | Uses the server input. | Controls the autocomplete hint. | | `variant` | `"outline" | "filled" | "plain"` ([`CNumberInputVariant`](#number-input-interface-variant)) | Uses the server input. | Controls visual treatment. | | `size` | `"sm" | "md" | "lg"` ([`CNumberInputSize`](#number-input-interface-size)) | Uses the server input. | Controls coordinated sizing. | | `onValueChange` | `function` | No semantic value callback. | Receives successful commit and reset requests. | | `onInputValueChange` | `function` | No semantic draft callback. | Receives literal draft edits and parse status. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CNumberInput events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string | null, detail: CNumberInputValueChangeDetail) => void` ([`CNumberInputValueChangeDetail`](#number-input-interface-cnumber-input-value-change-detail)) | A valid blur, Enter, step, bound jump, wheel step, or reset requests a changed canonical value. | `{value, previousValue, inputValue, controlled, source, sourceEvent}` ([`CNumberInputValueChangeDetail`](#number-input-interface-cnumber-input-value-change-detail)) | Uncontrolled state and canonical Form transport commit before notification; controlled state is request-only. | | `onInputValueChange` | `(inputValue: string, detail: CNumberInputInputValueChangeDetail) => void` ([`CNumberInputInputValueChangeDetail`](#number-input-interface-cnumber-input-input-value-change-detail)) | A native input or completed IME composition changes the literal editor draft. | `{inputValue, previousInputValue, status, controlled, composing, sourceEvent}` ([`CNumberInputInputValueChangeDetail`](#number-input-interface-cnumber-input-input-value-change-detail)) | Reports the draft without committing or reformatting it; native input remains observable through input_attrs. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CNumberInput CSS variables Apply these variables to `CNumberInput` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-number-input-background` | `color` | Control background. | `Canvas` | | `--cui-number-input-foreground` | `color` | Editor and icon foreground. | `CanvasText` | | `--cui-number-input-border-color` | `color` | Control and step-divider border. | `Mixed CanvasText.` | | `--cui-number-input-focus-color` | `color` | Focus border and ring. | `Highlight` | | `--cui-number-input-invalid-border-color` | `color` | Invalid border. | `Theme error.` | | `--cui-number-input-radius` | `length` | Control corner radius. | `0.5rem` | | `--cui-number-input-height` | `length` | Editor and Button height. | `2.5rem` | | `--cui-number-input-inline-padding` | `length` | Editor inline inset. | `0.75rem` | | `--cui-number-input-control-size` | `length` | Step Button inline size. | `2.5rem` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CNumberInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-empty` | Root div | `present | absent` | Mirrors an empty canonical value. | | `data-required` | Root div | `present | absent` | Mirrors effective requiredness. | | `data-disabled` | Root div | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Root div | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Root div | `present | absent` | Mirrors application or revealed component invalidity. | | `data-variant` | Root div | `CNumberInputVariant` ([`CNumberInputVariant`](#number-input-interface-variant)) | Mirrors visual treatment. | | `data-size` | Root div | `CNumberInputSize` ([`CNumberInputSize`](#number-input-interface-size)) | Mirrors coordinated sizing. |
    #### CNumberInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Editor input | `"spinbutton"` | Exposes numeric stepping semantics while preserving text editing. | | `inputmode` | Editor input | `"decimal"` | Requests a decimal-capable virtual keyboard. | | `aria-valuenow` | Editor input | `canonical decimal | absent` | Exposes a valid committed canonical value. | | `aria-valuetext` | Editor input | `localized string | absent` | Exposes the locale-formatted committed value. | | `aria-valuemin` | Editor input | `canonical decimal | absent` | Exposes the inclusive minimum. | | `aria-valuemax` | Editor input | `canonical decimal | absent` | Exposes the inclusive maximum. | | `aria-invalid` | Editor input | `"true" | absent` | Mirrors application or revealed native validity. |
    #### CNumberInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `type` | Step Buttons | `"button"` | Prevents accidental Form submission. | | `tabindex` | Step Buttons | `"-1"` | Keeps the editor as the sole sequential Tab stop. | | `aria-label` | Step Buttons | `localized string` | Names increment or decrement. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CNumberInput selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="number-input"]` | Root div | State reflections and class_, style, and attrs destination. | | `[data-citry-ui-part="control"]` | Control div | Contains the editor and optional step Buttons. | | `[data-citry-ui-part="input"]` | Text input | Public focus target and input_attrs destination. | | `[data-citry-ui-part="decrement"]` | Button | Requests one exact decrement. | | `[data-citry-ui-part="increment"]` | Button | Requests one exact increment. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CNumberInputExact` | `int | Decimal | str` | | `CNumberInputCommitBehavior` | `Literal["validate", "clamp"]` | | `CNumberInputVariant` | `Literal["outline", "filled", "plain"]` | | `CNumberInputSize` | `Literal["sm", "md", "lg"]` | | `CNumberInputParseStatus` | `Literal["empty", "incomplete", "invalid", "valid"]` | | `CNumberInputChangeSource` | `Literal["blur", "enter", "increment", "decrement", "page", "home", "end", "wheel", "reset"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CNumberInputValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string | null` | - | Requested exact canonical value. | | `previousValue` | `string | null` | - | Effective canonical value before the request. | | `inputValue` | `string` | - | Visible formatted text associated with the request. | | `controlled` | `boolean` | - | Whether client value owns canonical state. | | `source` | `CNumberInputChangeSource` ([`CNumberInputChangeSource`](#number-input-interface-change-source)) | - | Interaction or reset cause. | | `sourceEvent` | `object | null` | - | Native interaction event when one exists. |
    #### `CNumberInputInputValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `inputValue` | `string` | - | Current literal draft. | | `previousInputValue` | `string` | - | Literal draft before this native input. | | `status` | `CNumberInputParseStatus` ([`CNumberInputParseStatus`](#number-input-interface-parse-status)) | - | Locale-aware parse state. | | `controlled` | `boolean` | - | Whether client value owns canonical state. | | `composing` | `boolean` | - | Whether an input method composition remains active. | | `sourceEvent` | `object | null` | - | Native input or composition event. |
    ### 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. #### CNumberInput translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-number-input-decrement` | Names the decrement Button. | `None` | `decrement_label` | $c-tr updates the stable aria-label. | | `citry-ui-number-input-increment` | Names the increment Button. | `None` | `increment_label` | $c-tr updates the stable aria-label. | | `citry-ui-number-input-required` | Supplies empty required validity. | `None` | `required_message` | Active `i18n.bind()` custom-validity destination. | | `citry-ui-number-input-invalid` | Supplies malformed or incomplete draft validity. | `None` | `invalid_message` | Active `i18n.bind()` custom-validity destination. | | `citry-ui-number-input-minimum` | Supplies inclusive-minimum validity. | `min: str` | `minimum_message` | `i18n.bind()` with locale-formatted min. | | `citry-ui-number-input-maximum` | Supplies inclusive-maximum validity. | `max: str` | `maximum_message` | `i18n.bind()` with locale-formatted max. | | `citry-ui-number-input-step` | Supplies exact step-grid validity. | `step: str` | `step_message` | `i18n.bind()` with locale-formatted step. |
    --- # PinInput Source: https://citry.dev/ui-library/components/pin-input/ # Pin input Use `CPinInput` for one-time codes, PINs, and short recovery tokens. Its value is always a string, so a leading zero is preserved. ## Enter a verification code Give a standalone PinInput an accessible `label`, or place it in `CField` for a visible label, help, error, and shared state. ```citry-html Verification code Enter the six digits from your message. ``` ### Enter a verification code [Open the rendered preview](/ui-library/components/pin-input/_previews/basic/) ````citry from citry import Component class BasicPinInput(Component): class Kwargs: pass class Slots: pass template = """ Verification code Enter the six digits from your message. """ preview = BasicPinInput() preview # noqa: B018 ```` One native text input owns focus, selection, paste, autofill, validation, and submission. The separate cells are visual only and create neither extra Tab stops nor separate Form values. Without JavaScript the native input remains a normal usable text box. ## Accept recovery-code letters The default `type="numeric"` accepts ASCII digits. Use `alphabetic` or `alphanumeric` for protocol tokens containing ASCII letters. These values are opaque identifiers, not localized numbers. ### Enter an alphanumeric recovery code [Open the rendered preview](/ui-library/components/pin-input/_previews/alphanumeric/) ````citry from citry import Component class AlphanumericPinInput(Component): class Kwargs: pass class Slots: pass template = """ Recovery code Use the eight letters and digits printed with your account. """ preview = AlphanumericPinInput() preview # noqa: B018 ```` Invalid characters are discarded and reported through `onValueInvalid`. `length` is structural and supports 1 through 32 characters. ## Control the value Client `value` controls the exact string. An edit is a request: the displayed cells and Form value remain owner-controlled until the Alpine expression returns the requested value. ### Control a PinInput [Open the rendered preview](/ui-library/components/pin-input/_previews/controlled/) ````citry from citry import Component # ruff: noqa: E501 - Alpine expression stays readable in public source class ControlledPinInput(Component): class Kwargs: pass class Slots: pass template = """
    No request yet Clear
    """ css = ":where(.pin-input-demo-stack){display:grid;justify-items:start;gap:.75rem}" preview = ControlledPinInput() preview # noqa: B018 ```` `onValueChange` reports accepted edits. `onComplete` reports a transition to a full value and never submits the Form automatically. Paste and autofill remain available. ## Preserve native Form behavior `required` combines with an exact-length native pattern, so an empty or partial required code blocks submission. Readonly values remain focusable and submit; disabled values do not submit. ### Submit and reset codes [Open the rendered preview](/ui-library/components/pin-input/_previews/forms/) ````citry from citry import Component # ruff: noqa: E501 - template expression stays readable in public source class PinInputForms(Component): class Kwargs: pass class Slots: pass template = """
    One-time code SubmitReset Submit or reset the Form """ css = ":where(.pin-input-demo-stack){display:grid;justify-items:start;gap:1rem}" preview = PinInputForms() preview # noqa: B018 ```` `one_time_code=True` emits `autocomplete="one-time-code"`. Set an explicit `input_attrs={"autocomplete": "..."}` when another autocomplete policy is required. Citry never invokes WebOTP or reads SMS messages. ## Mask or group the visual cells `mask=True` replaces filled visual cells with bullets without changing the submitted string. It reduces shoulder surfing but is not encryption and does not hide the accessible text-field value from assistive software. ### Mask a code [Open the rendered preview](/ui-library/components/pin-input/_previews/masked/) ````citry from citry import Component class MaskedPinInput(Component): class Kwargs: pass class Slots: pass template = """

    Masking changes the visual cells only. Treat the submitted token as sensitive data.

    """ css = ":where(.pin-input-demo-stack){display:grid;justify-items:start;gap:.75rem}" preview = MaskedPinInput() preview # noqa: B018 ```` Use `attached=True` to join cells. For a 3–3 presentation, provide `separator_after=(2,)` and the `separator` slot. Separator output is visual; put instructions in Field description text. ### Group code cells [Open the rendered preview](/ui-library/components/pin-input/_previews/separator/) ````citry from citry import Component class SeparatedPinInput(Component): class Kwargs: pass class Slots: pass template = """
    -
    """ css = ":where(.pin-input-demo-stack){display:grid;justify-items:start;gap:1rem}" preview = SeparatedPinInput() preview # noqa: B018 ```` ## Keep code direction and locale ownership clear PinInput renders protocol tokens left-to-right by default, including inside an RTL page. ASCII digits are not localized. Labels, Field text, placeholders, and separators belong to the application and stay in its locale. ### Use PinInput in RTL content [Open the rendered preview](/ui-library/components/pin-input/_previews/locales/) ````citry from citry import Component class PinInputLocales(Component): class Kwargs: pass class Slots: pass template = """
    رمز التحقق يبقى رمز البروتوكول من اليسار إلى اليمين.
    """ css = ":where(.pin-input-demo-stack){display:grid;justify-items:start;gap:.75rem}" preview = PinInputLocales() preview # noqa: B018 ```` ## Choose states and public styles Outline and subtle variants combine with sm, md, and lg sizes. Public `--cui-pin-input-*` variables and documented part selectors customize cells, focus, separators, and state treatment. ### Compare PinInput states [Open the rendered preview](/ui-library/components/pin-input/_previews/states/) ````citry from citry import Component class PinInputStates(Component): class Kwargs: pass class Slots: pass template = """
    """ css = """ :where(.pin-input-state-grid){display:grid;grid-template-columns:repeat(auto-fit,minmax(18rem,1fr));gap:1.5rem;align-items:start} :where(.pin-input-brand){--cui-pin-input-focus-color:#7c3aed;--cui-pin-input-radius:.75rem} """ preview = PinInputStates() preview # noqa: B018 ```` Tab enters the component once. Native text editing and clipboard shortcuts continue to work; Home, End, and pointer selection move the active visual cell. ## API reference ### Inputs #### CPinInput server inputs Server inputs are passed in a template through `` or in Python through `CPinInput(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | `""` | Sets the initial exact string and preserves leading zeroes. | | `name` | `str | None` | `None` | Sets the native Form field name. | | `form` | `str | None` | `None` | Associates the native input with an external Form ID. | | `id` | `str | None` | generated | Sets the native input ID and bases the root ID. | | `length` | `int` | `6` | Sets one through thirty-two characters cells maxlength and exact validity. | | `type` | `CPinInputType` ([`CPinInputType`](#pin-input-interface-type)) | `"numeric"` | Chooses the ASCII numeric alphabetic or alphanumeric token alphabet. | | `required` | `bool | None` | `None` | Enables exact-length native required validity outside Field. | | `disabled` | `bool | None` | `None` | Blocks focus edits and Form submission outside Field. | | `readonly` | `bool | None` | `None` | Preserves focus selection and submission while blocking edits outside Field. | | `invalid` | `bool | None` | `None` | Reflects application invalid state outside Field. | | `mask` | `bool` | `False` | Replaces filled visual cells with bullets without changing the value. | | `one_time_code` | `bool` | `True` | Emits one-time-code autocomplete unless input_attrs supplies another token. | | `placeholder` | `one-code-point str | None` | `"○"` | Supplies the caller-authored empty-cell marker. | | `attached` | `bool` | `False` | Joins adjacent visual cells. | | `separator_after` | `Sequence[int] | None` | `None` | Selects zero-based boundaries after which the separator slot renders. | | `label` | `str | None` | `None` | Names a standalone native input; use the Field label slot inside Field. | | `size` | `CPinInputSize` ([`CPinInputSize`](#pin-input-interface-size)) | `"md"` | Selects coordinated cell sizing. | | `variant` | `CPinInputVariant` ([`CPinInputVariant`](#pin-input-interface-variant)) | `"outline"` | Selects cell surface treatment. | | `class_` | `CClassValue | None` ([`CClassValue`](#pin-input-interface-class-value)) | `None` | Adds classes to the root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#pin-input-interface-style-value)) | `None` | Adds styles to the root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing owned state or identity. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied native attributes including accessible naming descriptions autocomplete and dir without replacing owned behavior. |
    #### CPinInput client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string` | Releases control to the last uncontrolled value. | Controls the exact accepted token string. | | `required` | `boolean` | Uses server or Field state. | Controls standalone exact-length validity. | | `disabled` | `boolean` | Uses server or owner state. | Controls editing focus and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable submission. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `mask` | `boolean` | Uses the server value. | Controls visual masking. | | `variant` | `CPinInputVariant` ([`CPinInputVariant`](#pin-input-interface-variant)) | Uses the server value. | Controls surface treatment. | | `size` | `CPinInputSize` ([`CPinInputSize`](#pin-input-interface-size)) | Uses the server value. | Controls coordinated sizing. | | `onValueChange` | `function` | No value callback. | Receives each accepted user edit or reset request. | | `onComplete` | `function` | No completion callback. | Receives transitions to a complete accepted token. | | `onValueInvalid` | `function` | No rejection callback. | Receives discarded characters and their input source. | | `onFocusChange` | `function` | No focus callback. | Receives native focus entry and exit. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CPinInput slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `separator` | no | `{index: int}` ([`CPinInputSeparatorSlotData`](#pin-input-interface-cpin-input-separator-slot-data)) | No visual content at each separator_after boundary. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CPinInput events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string, detail: CPinInputValueChangeDetail) => void` ([`CPinInputValueChangeDetail`](#pin-input-interface-cpin-input-value-change-detail)) | An accepted edit or Form reset requests a different value. | `{value, previousValue, controlled, source, sourceEvent}` ([`CPinInputValueChangeDetail`](#pin-input-interface-cpin-input-value-change-detail)) | Uncontrolled state commits first; controlled state is request-only. | | `onComplete` | `(value: string, detail: CPinInputCompleteDetail) => void` ([`CPinInputCompleteDetail`](#pin-input-interface-cpin-input-complete-detail)) | Accepted input transitions to the exact configured length. | `{value, controlled, source, sourceEvent}` ([`CPinInputCompleteDetail`](#pin-input-interface-cpin-input-complete-detail)) | Reports completion without automatically submitting. | | `onValueInvalid` | `(detail: CPinInputInvalidDetail) => void` ([`CPinInputInvalidDetail`](#pin-input-interface-cpin-input-invalid-detail)) | One edit contains disallowed or overflow characters. | `{value, rejected, source, sourceEvent}` ([`CPinInputInvalidDetail`](#pin-input-interface-cpin-input-invalid-detail)) | Reports plain rejected text after filtering it from the value. | | `onFocusChange` | `(focused: boolean, detail: CPinInputFocusChangeDetail) => void` ([`CPinInputFocusChangeDetail`](#pin-input-interface-cpin-input-focus-change-detail)) | The native text input focuses or blurs. | `{focused, sourceEvent}` ([`CPinInputFocusChangeDetail`](#pin-input-interface-cpin-input-focus-change-detail)) | Runs after focus reflection changes. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CPinInput CSS variables Apply these variables to `CPinInput` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-pin-input-cell-size` | `length` | Visual cell inline and block size. | `Size-dependent 2.75rem.` | | `--cui-pin-input-gap` | `length` | Space between separate cells. | `0.5rem` | | `--cui-pin-input-separator-gap` | `length` | Extra space for a separator boundary. | `0.4rem` | | `--cui-pin-input-border-color` | `color` | Outline cell border. | `Mixed CanvasText.` | | `--cui-pin-input-focus-color` | `color` | Active-cell focus ring. | `Highlight` | | `--cui-pin-input-invalid-color` | `color` | Invalid border treatment. | `Theme danger color.` | | `--cui-pin-input-background` | `color` | Cell surface. | `Canvas` | | `--cui-pin-input-color` | `color` | Entered character color. | `CanvasText` | | `--cui-pin-input-placeholder-color` | `color` | Empty-cell marker color. | `Muted CanvasText.` | | `--cui-pin-input-radius` | `length` | Cell corner radius. | `0.5rem` | | `--cui-pin-input-disabled-opacity` | `number` | Disabled treatment opacity. | `0.58` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CPinInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-required` | Root div | `present | absent` | Mirrors effective requiredness. | | `data-disabled` | Root div | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Root div | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Root div | `present | absent` | Mirrors effective application or native invalidity. | | `data-focused` | Root div | `present | absent` | Marks native input focus. | | `data-filled` | Root div | `present | absent` | Marks any accepted character. | | `data-complete` | Root div | `present | absent` | Marks an accepted exact-length value. | | `data-attached` | Root div | `present | absent` | Marks joined cell styling. | | `data-variant` | Root div | `CPinInputVariant` ([`CPinInputVariant`](#pin-input-interface-variant)) | Mirrors surface treatment. | | `data-size` | Root div | `CPinInputSize` ([`CPinInputSize`](#pin-input-interface-size)) | Mirrors coordinated sizing. |
    #### CPinInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-active` | Visual cell | `present | absent` | Marks the logical native selection or insertion cell. | | `data-filled` | Visual cell | `present | absent` | Marks an accepted character. | | `data-masked` | Visual cell | `present | absent` | Marks a character currently displayed as a bullet. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CPinInput selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="pin-input"]` | Root div | State reflections and root customization destination. | | `[data-citry-ui-part="input"]` | Native text input | Owns semantics focus editing validation Form value and input_attrs. | | `[data-citry-ui-part="cells"]` | Aria-hidden presentation span | Contains the segmented visual display. | | `[data-citry-ui-part="cell"]` | Visual cell span | Displays one accepted position and receives pointer selection. | | `[data-citry-ui-part="character"]` | Character span | Displays entered masked or placeholder content. | | `[data-citry-ui-part="caret"]` | Decorative caret span | Marks an active empty insertion cell. | | `[data-citry-ui-part="separator"]` | Visual separator span | Hosts the caller separator slot at selected boundaries. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CPinInputType` | `Literal["numeric", "alphabetic", "alphanumeric"]` | | `CPinInputSize` | `Literal["sm", "md", "lg"]` | | `CPinInputVariant` | `Literal["outline", "subtle"]` | | `CPinInputChangeSource` | `Literal["input", "paste", "autofill", "composition", "reset"]` | | `CPinInputInvalidSource` | `Literal["input", "paste", "autofill", "composition"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CPinInputSeparatorSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `index` | `int` | - | Zero-based cell index after which this separator renders. |
    #### `CPinInputValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string` | - | Requested accepted token. | | `previousValue` | `string` | - | Effective token before the request. | | `controlled` | `boolean` | - | Whether client value owns committed state. | | `source` | `CPinInputChangeSource` ([`CPinInputChangeSource`](#pin-input-interface-change-source)) | - | Edit or reset source. | | `sourceEvent` | `object | null` | - | Native event when one exists. |
    #### `CPinInputCompleteDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string` | - | Complete accepted token. | | `controlled` | `boolean` | - | Whether client value owns committed state. | | `source` | `CPinInputChangeSource` ([`CPinInputChangeSource`](#pin-input-interface-change-source)) | - | Completion source. | | `sourceEvent` | `object | null` | - | Native event. |
    #### `CPinInputInvalidDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string` | - | Accepted filtered token. | | `rejected` | `string` | - | Discarded plain characters. | | `source` | `CPinInputInvalidSource` ([`CPinInputInvalidSource`](#pin-input-interface-invalid-source)) | - | Rejected edit source. | | `sourceEvent` | `object | null` | - | Native input event. |
    #### `CPinInputFocusChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `focused` | `boolean` | - | Current native focus state. | | `sourceEvent` | `object` | - | Native focus or blur event. |
    ### Translation keys - --- # Radio Source: https://citry.dev/ui-library/components/radio/ # Radio Use `CRadioGroup` and `CRadio` when people should see every option and select exactly one. Native fieldset, legend, labels, keyboard behavior, validity, reset, and FormData stay browser-owned. ## Radio at a glance ### Radio at a glance [Open the rendered preview](/ui-library/components/radio/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RadioAtAGlance(Component): template = """

    Plan the garden path

    Choose the habitat the path should pass through.

    Habitat Woodland Wildflower meadow Wetland edge
    """ css = """ :where(.radio-glance) { display: grid; gap: 0.85rem; max-inline-size: 42rem; padding: 1.25rem; border: 1px solid light-dark(#a6b99b, #51664a); border-radius: 0.9rem; background: light-dark(#f4f8ef, #182219); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.radio-glance h2, .radio-glance p) { margin: 0; } :where(.radio-glance > p) { color: light-dark(#53634c, #b8c9b0); font-size: 0.82rem; } """ preview = RadioAtAGlance() preview # noqa: B018 ```` ## Compose a group Give Group one shared `name`, a visible `label` slot, and Radios with unique values. `CRadio` cannot be used outside Group. ### Compose a Radio Group [Open the rendered preview](/ui-library/components/radio/_previews/basic/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BasicRadioGroup(Component): template = """ Watering time Early morning Late evening """ preview = BasicRadioGroup() preview # noqa: B018 ```` ```citry-html Habitat Woodland Wetland ``` ## Add descriptions and disabled choices Descriptions connect to their native Radio. Disable one unavailable option without disabling its siblings. ### Describe and disable Radio options [Open the rendered preview](/ui-library/components/radio/_previews/descriptions/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DescribedRadios(Component): template = """ Soil blend Woodland loam Balanced drainage for ferns and woodland flowers. Alpine grit Fast drainage for rock-garden plants. Bog peat Unavailable while the bog bed recovers. """ css = """ :where(.radio-described) { max-inline-size: 34rem; } """ preview = DescribedRadios() preview # noqa: B018 ```` ## Control selection in the browser Pass `value` through `$c-props="{...}"`. A known string controls one option; `null` clears selection; omission releases control. Handle native `input` or `change` with `$event.target.value`. ### Control a Radio Group [Open the rendered preview](/ui-library/components/radio/_previews/controlled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledRadios(Component): template = """
    Ground cover Moss Creeping thyme Microclover
    """ css = """ :where(.radio-controlled) { display: grid; gap: 0.75rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.radio-controlled output) { color: light-dark(#3f6212, #bef264); font-size: 0.8rem; } """ preview = ControlledRadios() preview # noqa: B018 ```` ## Use native forms and validation The checked enabled Radio contributes one shared name/value entry. Required groups use native validation and reset. ### Submit and validate Radio values [Open the rendered preview](/ui-library/components/radio/_previews/forms/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RadioForm(Component): template = """
    Planting plot North wall Old orchard Pond margin Reserve plot
    """ css = """ :where(.radio-form) { display: grid; gap: 1rem; max-inline-size: 34rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = RadioForm() preview # noqa: B018 ```` ## Choose orientation Vertical is easiest to scan. Horizontal groups wrap and keep native keyboard behavior. ### Compare Radio orientations [Open the rendered preview](/ui-library/components/radio/_previews/orientation/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RadioOrientation(Component): template = """ Vertical Spring Autumn Horizontal Spring Autumn """ preview = RadioOrientation() preview # noqa: B018 ```` ## Choose presentation Compare solid and outline treatments, three sizes, and logical label placement. ### Compare Radio presentation [Open the rendered preview](/ui-library/components/radio/_previews/presentation/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RadioPresentation(Component): class Kwargs: pass class Slots: pass template = """ {{ variant }} OneTwo {{ size }}, labels first Leaf Flower """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"variants": ("solid", "outline"), "sizes": ("sm", "md", "lg")} preview = RadioPresentation() preview # noqa: B018 ```` ## Compose with Field Inside `CField`, Field owns label, description, error, required, disabled, and invalid state. Do not add the Group `label` slot there. ### Compose Radio with Field [Open the rendered preview](/ui-library/components/radio/_previews/field/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RadioField(Component): template = """ Preferred shade Full sun Partial shade Deep shade Choose the light available in this bed. Choose one shade level. """ preview = RadioField() preview # noqa: B018 ```` ## Customize Radio Override public group, control, color, focus, spacing, and disabled variables. Stable part selectors target the fieldset, legend, item, input, label, and description. ### Customize Radio with public CSS [Open the rendered preview](/ui-library/components/radio/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RadioCustomization(Component): template = """
    Plant collection Fern house Alpine house Orchid house
    """ css = """ :where(.radio-custom) { --cui-radio-active-color: light-dark(#7c3f00, #fbbf24); --cui-radio-border-color: light-dark(#a16207, #fde68a); --cui-radio-background: light-dark(#fffbeb, #2d2108); --cui-radio-control-size: 1.35rem; --cui-radio-group-gap: 1.25rem; padding: 1.25rem; border-radius: 0.8rem; background: light-dark(#f7f2df, #211d10); } """ preview = RadioCustomization() preview # noqa: B018 ```` ## Choose the right control Use Native Select when choices should collapse, Checkbox for independent choices, and Switch for an immediate Boolean setting. Radio Card and Segmented Control are separate interaction and anatomy families. ## API reference ### Inputs #### CRadioGroup server inputs Server inputs are passed in a template through `` or in Python through `CRadioGroup(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `name` | `str` | required | Sets the required shared native radio-group and FormData name. | | `value` | `str | None` | `None` | Sets initial checked value; it must match one Radio value; None leaves the group unselected. | | `form` | `str | None` | `None` | Associates every Radio with an external native Form ID. | | `required` | `bool | None` | `None` | Enables native same-name group validation; CField owns it when composed. | | `disabled` | `bool | None` | `None` | Disables the native fieldset; CField and CForm remain dominant. | | `invalid` | `bool | None` | `None` | Sets explicit invalid styling and ARIA; CField owns it when composed. | | `orientation` | `"vertical" | "horizontal"` ([`CRadioOrientation`](#radio-interface-orientation)) | `"vertical"` | Selects stacked or wrapping inline layout without replacing native keyboard behavior. | | `variant` | `"solid" | "outline"` ([`CRadioVariant`](#radio-interface-variant)) | `"solid"` | Selects checked-control treatment. | | `size` | `"sm" | "md" | "lg"` ([`CRadioSize`](#radio-interface-size)) | `"md"` | Sets control and text scale. | | `label_pos` | `"start" | "end"` ([`CRadioLabelPos`](#radio-interface-label-pos)) | `"end"` | Places item labels before or after controls. | | `id` | `str | None` | `None` | Sets the fieldset ID. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#radio-interface-class-value)) | `None` | Adds fieldset classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#radio-interface-style-value)) | `None` | Adds fieldset inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted nonconflicting metadata and targeted Alpine attributes to the fieldset. |
    #### CRadioGroup client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string | null` | Releases control to native selection. | Controls one known value or no selection; omission releases control. | | `required` | `boolean` | Uses the server or Field fallback. | Controls native required state outside Field. | | `disabled` | `boolean` | Uses the server or Field/Form fallback. | Controls local disabled state outside Field; Form disabled stays dominant. | | `invalid` | `boolean` | Uses the server or Field fallback. | Controls explicit invalid state outside Field. | | `orientation` | `"vertical" | "horizontal"` | Uses the server fallback. | Controls the public layout reflection. | | `variant` | `"solid" | "outline"` | Uses the server fallback. | Controls checked-control treatment. | | `size` | `"sm" | "md" | "lg"` | Uses the server fallback. | Controls public size. | | `label_pos` | `"start" | "end"` | Uses the server fallback. | Controls label placement. |
    #### CRadio server inputs Server inputs are passed in a template through `` or in Python through `CRadio(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | required | Sets unique canonical option and submitted value. | | `disabled` | `bool` | `False` | Disables this native Radio without disabling siblings. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#radio-interface-class-value)) | `None` | Adds item wrapper classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#radio-interface-style-value)) | `None` | Adds item wrapper inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted nonconflicting attributes to the item wrapper. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted nonconflicting native metadata and event listeners to the Radio input. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CRadioGroup slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `label` | no | `{}` ([`CRadioGroupLabelSlotData`](#radio-interface-group-label)) | Missing standalone label raises; the slot is forbidden under CField. | | `default` | yes | `{}` ([`CRadioGroupDefaultSlotData`](#radio-interface-group-default)) | Missing fill raises before rendering. |
    #### CRadio slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CRadioDefaultSlotData`](#radio-interface-radio-default)) | Missing visible label raises before rendering. | | `description` | no | `{}` ([`CRadioDescriptionSlotData`](#radio-interface-radio-description)) | Description wrapper and relationship are omitted. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CRadioGroup CSS variables Apply these variables to `CRadioGroup` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-radio-group-gap` | `length` | Spacing between items. | `0.75rem.` | | `--cui-radio-active-color` | `color` | Checked border/fill/dot. | `Scheme-aware primary.` | | `--cui-radio-border-color` | `color` | Unchecked border. | `Scheme-aware neutral.` | | `--cui-radio-background` | `color` | Native control background. | `Canvas.` | | `--cui-radio-foreground` | `color` | Labels and inherited text. | `CanvasText.` | | `--cui-radio-focus-color` | `color` | Keyboard focus ring. | `Highlight.` | | `--cui-radio-invalid-color` | `color` | Invalid control border. | `Scheme-aware danger.` | | `--cui-radio-control-size` | `length` | Radio control box. | `Size-derived length.` | | `--cui-radio-item-gap` | `length` | Control-to-body spacing. | `0.55rem.` | | `--cui-radio-label-gap` | `length` | Label-to-description spacing. | `0.2rem.` | | `--cui-radio-disabled-opacity` | `number` | Disabled item opacity. | `0.52.` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CRadioGroup attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `disabled` | Fieldset | `boolean present or absent` | Native group disabled state. | | `aria-invalid` | Fieldset | `"true" or absent` | Effective explicit or native invalid state. | | `data-value` | Fieldset | `canonical string or absent` | Current checked option value. | | `data-required` | Fieldset | `boolean present or absent` | Effective native-required request. | | `data-disabled` | Fieldset | `boolean present or absent` | Effective group disabled state. | | `data-invalid` | Fieldset | `boolean present or absent` | Effective invalid state. | | `data-orientation` | Fieldset | `"vertical" | "horizontal"` | Effective layout. | | `data-variant` | Fieldset | `"solid" | "outline"` | Effective selected-control treatment. | | `data-size` | Fieldset | `"sm" | "md" | "lg"` | Effective size. | | `data-label-pos` | Fieldset | `"start" | "end"` | Effective label placement. |
    #### CRadio attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `checked` | Native input | `boolean present or absent` | Server default checkedness; current checkedness is the native property. | | `disabled` | Native input | `boolean present or absent` | Item-local disabledness. | | `name` | Native input | `nonempty string` | Shared Group name. | | `value` | Native input | `canonical string` | Unique option/FormData value. | | `data-checked` | Item wrapper | `boolean present or absent` | Mirrors current native checkedness for styling. | | `data-disabled` | Item wrapper | `boolean present or absent` | Mirrors effective native disabledness. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CRadioGroup selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="radio-group"]` | Native fieldset | Group root and attrs destination. | | `[data-citry-ui-part="legend"]` | Native legend | Standalone group label. | | `[data-citry-ui-part="radio"]` | Item wrapper | Radio attrs destination. | | `[data-citry-ui-part="input"]` | Native radio input | Input attrs destination. | | `[data-citry-ui-part="body"]` | Item text wrapper | Label and description layout. | | `[data-citry-ui-part="label"]` | Native label | Visible option name and activation target. | | `[data-citry-ui-part="description"]` | Description span | Optional item guidance. |
    ### 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]` | | `CRadioOrientation` | `Literal["vertical", "horizontal"]` | | `CRadioVariant` | `Literal["solid", "outline"]` | | `CRadioSize` | `Literal["sm", "md", "lg"]` | | `CRadioLabelPos` | `Literal["start", "end"]` |
    #### `CRadioGroupDefaultSlotData` Empty dataclass: `{}`. #### `CRadioGroupLabelSlotData` Empty dataclass: `{}`. #### `CRadioDefaultSlotData` Empty dataclass: `{}`. #### `CRadioDescriptionSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Rating Source: https://citry.dev/ui-library/components/rating/ # Rating Use `CRating` for a short qualitative score such as a product review or conversation rating. Its public value is an exact canonical decimal string; `None` means unrated. ## Select a rating Supply a standalone accessible `label`, or compose Rating in `CField` for a visible label, description, error, and shared state. ```citry-html Product rating ``` ### Select a rating [Open the rendered preview](/ui-library/components/rating/_previews/basic/) ````citry from decimal import Decimal from typing import Any import citry_ui from citry import Component, citry from citry_ui import CRating citry.register_library(citry_ui) # ruff: noqa: E501 - template and CSS lines stay readable in public source examples class BasicRating(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return {"python_rating": CRating(label="Python-composed rating", value=Decimal("4.0"))} template = """
    Product rating Choose one through five stars.

    Python composition

    {{ python_rating }}
    """ css = """ :where(.rating-demo-grid){display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:1.25rem} :where(.rating-demo-grid article){display:grid;align-content:start;gap:.75rem}:where(.rating-demo-grid h3){margin:0} """ preview = BasicRating() preview # noqa: B018 ```` Without JavaScript, the component remains a same-name native radio group. It submits and validates `required` normally. The visual stars are decorative; each radio has a localized “value out of maximum” name. ## Choose fractional precision `precision` is an exact decimal that divides one. Half, quarter, fifth, and tenth ratings are supported as long as `max / precision` produces at most 200 choices. Floats and exponent notation are rejected. ### Use half and tenth ratings [Open the rendered preview](/ui-library/components/rating/_previews/precision/) ````citry from citry import Component class RatingPrecision(Component): class Kwargs: pass class Slots: pass template = """
    Half-star rating Tenth precision
    """ css = ":where(.rating-demo-stack){display:grid;gap:1.25rem}" preview = RatingPrecision() preview # noqa: B018 ```` `max` is an integer from 1 through 20. Use `CRadioGroup` if individual values need different text labels or meanings. ## Clear or control the value Set `allow_clear=True` to let a person click the committed value again and return to the unrated state. A required Rating then becomes natively invalid. ### Control and clear a rating [Open the rendered preview](/ui-library/components/rating/_previews/controlled/) ````citry from citry import Component # ruff: noqa: E501 - Alpine expression stays readable in the public source example class ControlledRating(Component): class Kwargs: pass class Slots: pass template = """
    No request yet
    """ css = ":where(.rating-demo-stack){display:grid;justify-items:start;gap:.75rem}" preview = ControlledRating() preview # noqa: B018 ```` Client `value` is a canonical string or `null`. A controlled interaction is a request: stars, checked radio, and FormData remain unchanged until the owner returns the requested value. `onHoverChange` reports preview only and never changes the submitted value. ## Preserve Form and reset behavior Editable Rating submits the checked native radio. Readonly Rating blocks mutation but submits its exact value through an owned hidden transport. Disabled Rating neither focuses nor submits. ### Submit and reset ratings [Open the rendered preview](/ui-library/components/rating/_previews/forms/) ````citry from citry import Component # ruff: noqa: E501 - template expressions stay readable in the public source example class RatingForms(Component): class Kwargs: pass class Slots: pass template = """
    Service rating SubmitReset Submit or reset the Form """ css = ":where(.rating-demo-stack){display:grid;justify-items:start;gap:1rem}" preview = RatingForms() preview # noqa: B018 ```` An uncanceled reset restores the server value. Controlled state receives a reset request and waits for its owner. `form` supports an external native Form; inside `CForm`, Rating cannot redirect ownership. ## Localize accessible value names `citry-ui-rating-value` names each exact choice and updates in place beneath a client-enabled `` provider. The number profile is `citry-ui-rating`. Zero-configuration source mode uses canonical digits and the component's English source message. ### Localize Rating choice names [Open the rendered preview](/ui-library/components/rating/_previews/locales/) ````citry from citry import Component class RatingLocales(Component): class Kwargs: pass class Slots: pass template = """

    The first Rating follows its nearest client-enabled i18n provider; the explicit pattern stays fixed.

    """ css = ":where(.rating-demo-stack){display:grid;justify-items:start;gap:1rem}" preview = RatingLocales() preview # noqa: B018 ```` Set `value_label="Score {value} / {max}"` for an application-owned fixed pattern. An explicit override creates no catalog binding. ## Choose states and public styles Solid and subtle variants combine with sm, md, and lg sizes. Public `--cui-rating-*` variables and `[data-citry-ui-part="..."]` selectors customize the documented anatomy. ### Compare Rating states and styling [Open the rendered preview](/ui-library/components/rating/_previews/states/) ````citry from citry import Component class RatingStates(Component): class Kwargs: pass class Slots: pass template = """
    """ css = """ :where(.rating-state-grid){display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1.5rem;align-items:start} :where(.rating-brand){--cui-rating-fill-color:#059669;--cui-rating-hover-color:#10b981;--cui-rating-gap:.4rem} """ preview = RatingStates() preview # noqa: B018 ```` RTL uses logical geometry. Coarse pointers retain large hit targets and forced colors preserve fill and focus. Custom symbol markup is intentionally not part of this contract; use Radio for differently named choices. ## API reference ### Inputs #### CRating server inputs Server inputs are passed in a template through `` or in Python through `CRating(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `CRatingExact | None` ([`CRatingExact`](#rating-interface-exact)) | `None` | Sets the initial exact score; zero and None mean unrated. | | `name` | `str | None` | `None` | Sets the progressive native radio Form field name. | | `form` | `str | None` | `None` | Associates every radio and readonly transport with an external Form ID. | | `id` | `str | None` | generated | Sets the first radio ID and bases later radio root and transport IDs. | | `max` | `int` | `5` | Sets one through twenty visual stars and the maximum score. | | `precision` | `CRatingExact` ([`CRatingExact`](#rating-interface-exact)) | `1` | Sets a positive exact selectable interval that divides one. | | `required` | `bool | None` | `None` | Enables native required radio-group validity outside Field. | | `disabled` | `bool | None` | `None` | Blocks focus mutation and Form submission outside Field. | | `readonly` | `bool | None` | `None` | Preserves focus and exact submission while blocking mutation outside Field. | | `invalid` | `bool | None` | `None` | Reflects application invalid state outside Field. | | `allow_clear` | `bool` | `False` | Lets a repeat click on the committed choice return to unrated. | | `label` | `str | None` | `None` | Names a standalone radiogroup; use the Field label slot inside Field. | | `value_label` | `str containing '{value}' and '{max}'` | `"{value} out of {max}"` | Overrides the catalog-backed accessible choice-name pattern. | | `variant` | `"solid" | "subtle"` ([`CRatingVariant`](#rating-interface-variant)) | `"solid"` | Selects active-star treatment. | | `size` | `"sm" | "md" | "lg"` ([`CRatingSize`](#rating-interface-size)) | `"md"` | Selects coordinated symbol sizing. | | `class_` | `CClassValue | None` ([`CClassValue`](#rating-interface-class-value)) | `None` | Adds classes to the documented root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#rating-interface-style-value)) | `None` | Adds styles to the documented root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed radiogroup attributes without replacing owned state or identity. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed attributes to every native radio. |
    #### CRating client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `canonical decimal string | null` | Releases control to the last uncontrolled value. | Controls the exact score or unrated state. | | `required` | `boolean` | Uses server or Field state. | Controls standalone required validity. | | `disabled` | `boolean` | Uses server or owner state. | Controls mutation and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable submission. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `allowClear` | `boolean` | Uses the server value. | Controls repeat-click clearing. | | `variant` | `CRatingVariant` ([`CRatingVariant`](#rating-interface-variant)) | Uses the server value. | Controls active-star treatment. | | `size` | `CRatingSize` ([`CRatingSize`](#rating-interface-size)) | Uses the server value. | Controls coordinated sizing. | | `onValueChange` | `function` | No semantic value callback. | Receives each user selection clear or reset request. | | `onHoverChange` | `function` | No hover-preview callback. | Receives pointer preview changes without committing. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CRating events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string | null, detail: CRatingValueChangeDetail) => void` ([`CRatingValueChangeDetail`](#rating-interface-crating-value-change-detail)) | A user selects or clears a value or resets the owning Form. | `{value, previousValue, controlled, source, sourceEvent}` ([`CRatingValueChangeDetail`](#rating-interface-crating-value-change-detail)) | Uncontrolled native and visual state commit before notification; controlled state is request-only. | | `onHoverChange` | `(value: string | null, detail: CRatingHoverChangeDetail) => void` ([`CRatingHoverChangeDetail`](#rating-interface-crating-hover-change-detail)) | Pointer preview enters a new exact choice or leaves the choices layer. | `{value, previousValue, sourceEvent}` ([`CRatingHoverChangeDetail`](#rating-interface-crating-hover-change-detail)) | Updates preview only and never changes FormData. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CRating CSS variables Apply these variables to `CRating` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-rating-empty-color` | `color` | Empty-star color. | `Mixed CanvasText.` | | `--cui-rating-fill-color` | `color` | Committed active-star color. | `Amber.` | | `--cui-rating-hover-color` | `color` | Pointer-preview color. | `Brighter amber.` | | `--cui-rating-focus-color` | `color` | Keyboard focus outline. | `Highlight` | | `--cui-rating-gap` | `length` | Space between stars. | `0.25rem` | | `--cui-rating-symbol-size` | `length` | Star size. | `1.5rem` | | `--cui-rating-control-size` | `length` | Minimum pointer-target block size. | `2.75rem` | | `--cui-rating-disabled-opacity` | `number` | Disabled treatment opacity. | `0.52` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CRating attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-hovering` | Root div | `present | absent` | Marks an active pointer preview. | | `data-required` | Root div | `present | absent` | Mirrors effective requiredness. | | `data-disabled` | Root div | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Root div | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Root div | `present | absent` | Mirrors application invalid state. | | `data-variant` | Root div | `CRatingVariant` ([`CRatingVariant`](#rating-interface-variant)) | Mirrors active-star treatment. | | `data-size` | Root div | `CRatingSize` ([`CRatingSize`](#rating-interface-size)) | Mirrors coordinated sizing. |
    #### CRating attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-checked` | Choice label | `present | absent` | Marks the committed exact choice. | | `data-highlighted` | Choice label | `present | absent` | Marks choices included in pointer preview. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CRating selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="rating"]` | Root div | State reflections and root customization destination. | | `[data-citry-ui-part="visual"]` | Decorative visual span | Contains empty and clipped active stars. | | `[data-citry-ui-part="empty"]` | Empty-star span | Displays the unfilled scale. | | `[data-citry-ui-part="fill"]` | Clipped active-star span | Displays preview or committed fill. | | `[data-citry-ui-part="symbol"]` | Decorative star span | Repeated fixed visual symbol. | | `[data-citry-ui-part="choices"]` | Choice layer span | Owns bounded exact hit targets and radios. | | `[data-citry-ui-part="choice"]` | Choice label | Exact pointer hit target and state hook. | | `[data-citry-ui-part="input"]` | Native radio input | Keyboard semantics Form value and input_attrs destination. | | `[data-citry-ui-part="choice-label"]` | Visually hidden span | Supplies the localized native radio accessible name. | | `[data-citry-ui-part="readonly-value"]` | Visually hidden span | Announces the readonly exact value. | | `[data-citry-ui-part="readonly-transport"]` | Hidden input | Submits a named readonly value. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CRatingExact` | `int | Decimal | str` | | `CRatingVariant` | `Literal["solid", "subtle"]` | | `CRatingSize` | `Literal["sm", "md", "lg"]` | | `CRatingChangeSource` | `Literal["pointer", "keyboard", "reset"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CRatingValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string | null` | - | Requested exact canonical score or unrated state. | | `previousValue` | `string | null` | - | Effective score before the request. | | `controlled` | `boolean` | - | Whether client value owns committed state. | | `source` | `CRatingChangeSource` ([`CRatingChangeSource`](#rating-interface-change-source)) | - | Pointer keyboard or reset cause. | | `sourceEvent` | `object | null` | - | Native interaction event when one exists. |
    #### `CRatingHoverChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string | null` | - | Current exact preview or null after leaving. | | `previousValue` | `string | null` | - | Preview before the pointer transition. | | `sourceEvent` | `object | null` | - | Native pointer event. |
    ### 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. #### CRating translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-rating-value` | Names every exact radio choice and the readonly current value. | `value: str; max: str` | `value_label` | `i18n.bind()` formats the values and updates the native label text. |
    --- # Select Source: https://citry.dev/ui-library/components/select/ # Select Use `CSelect` when people choose one value and the collection should remain compact until opened. The component progressively enhances a native Select, so form submission and reset retain native behavior. ## Select at a glance ### Select at a glance [Open the rendered preview](/ui-library/components/select/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class SelectAtAGlance(Component): template = """ Workspace Choose where new observations belong. """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CSelectOption("atlas", "Atlas research", "12 collaborators"), CSelectOption("aurora", "Aurora field notes", "7 collaborators"), CSelectOption("archive", "Archived studies", disabled=True), ] } preview = SelectAtAGlance() preview # noqa: B018 ```` ## Submit a value ### Submit a Select [Open the rendered preview](/ui-library/components/select/_previews/forms/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class SelectForm(Component): template = """
    Review status Save Reset
    """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CSelectOption("draft", "Draft"), CSelectOption("review", "Ready for review")]} preview = SelectForm() preview # noqa: B018 ```` ## Group related options ### Group options [Open the rendered preview](/ui-library/components/select/_previews/groups/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class GroupedSelect(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CSelectOption("oslo", "Oslo", group="Europe"), CSelectOption("prague", "Prague", group="Europe"), CSelectOption("kyoto", "Kyoto", group="Asia"), CSelectOption("seoul", "Seoul", group="Asia"), ] } preview = GroupedSelect() preview # noqa: B018 ```` ## Control selection ### Control selection [Open the rendered preview](/ui-library/components/select/_previews/controlled/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class ControlledSelect(Component): template = """

    Current:

    """ js = "Alpine.store('selectExample', {value:'draft'});" def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CSelectOption("draft", "Draft"), CSelectOption("published", "Published")]} preview = ControlledSelect() preview # noqa: B018 ```` ## Read-only and disabled states ### Select states [Open the rendered preview](/ui-library/components/select/_previews/states/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class SelectStates(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CSelectOption("active", "Active"), CSelectOption("paused", "Paused")]} preview = SelectStates() preview # noqa: B018 ```` ## Variants and sizes ### Select variants and sizes [Open the rendered preview](/ui-library/components/select/_previews/variants/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class SelectVariants(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CSelectOption("one", "One"), CSelectOption("two", "Two")]} preview = SelectVariants() preview # noqa: B018 ```` ## Keyboard behavior Enter, Space, Down, or Up opens the Listbox. Down and Up move the highlight; Home and End jump to its edges; printable text performs buffered typeahead; Enter or Space commits; Escape closes unchanged; and Tab closes while ordinary page navigation continues. ### Navigate Select [Open the rendered preview](/ui-library/components/select/_previews/keyboard/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class KeyboardSelect(Component): template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "options": [ CSelectOption("earth", "Earth"), CSelectOption("mars", "Mars"), CSelectOption("jupiter", "Jupiter"), ] } preview = KeyboardSelect() preview # noqa: B018 ```` ## Customize Select ### Customize Select [Open the rendered preview](/ui-library/components/select/_previews/customization/) ````citry import citry_ui from citry import Component, citry from citry_ui import CSelectOption citry.register_library(citry_ui) class CustomizedSelect(Component): css = """ .brand-select { --cui-select-radius: 1rem; --cui-select-selected-background: #53389e; --cui-select-selected-foreground: white; --cui-select-focus-color: #7f56d9; inline-size: min(100%, 22rem); } """ template = """ """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return {"options": [CSelectOption("botany", "Botany"), CSelectOption("astronomy", "Astronomy")]} preview = CustomizedSelect() preview # noqa: B018 ```` ## Accessibility and forms The visible Button uses the select-only combobox pattern and keeps DOM focus while `aria-activedescendant` identifies the highlighted Option. A native Select remains the form value, validity, and reset truth. Before client initialization, that native control is the visible fallback. Use `CListbox` for a persistent collection, `CMultiSelect` for several compact values, and `CCombobox` when users need text filtering or custom input. ## API reference ### Inputs #### CSelect server inputs Server inputs are passed in a template through `` or in Python through `CSelect(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `options` | `Sequence[CSelectOption]` | required | Supplies the nonempty ordered stable collection. | | `placeholder` | `str` | required | Supplies author-localized empty-value text. | | `name` | `str | None` | `None` | Sets the native form field name. | | `form` | `str | None` | `None` | Associates the native value proxy with a Form ID. | | `id` | `str | None` | `None` | Sets native proxy identity and generated relationships. | | `value` | `str | None` | `None` | Sets the initial selected stable value. | | `open` | `bool` | `False` | Sets initial popup visibility when eligible. | | `required` | `bool | None` | `None` | Enables native required validity outside Field. | | `disabled` | `bool | None` | `None` | Disables selection and form contribution. | | `readonly` | `bool | None` | `None` | Preserves submission while preventing changes. | | `invalid` | `bool | None` | `None` | Adds owner-supplied invalid presentation. | | `loop` | `bool` | `False` | Wraps open Listbox arrow navigation. | | `placement` | `"bottom-start" | "bottom-end" | "top-start" | "top-end"` ([`CSelectPlacement`](#select-interface-placement)) | `"bottom-start"` | Sets preferred logical popup placement. | | `match_width` | `bool` | `True` | Matches the popup inline size to the control within viewport limits. | | `variant` | `"outline" | "filled" | "plain"` ([`CSelectVariant`](#select-interface-variant)) | `"outline"` | Selects control treatment. | | `size` | `"sm" | "md" | "lg"` ([`CSelectSize`](#select-interface-size)) | `"md"` | Selects control and Option geometry. | | `class_` | `CClassValue | None` ([`CClassValue`](#select-interface-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#select-interface-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted nonconflicting root attributes. | | `trigger_attrs` | `Mapping[str, object] | None` | `None` | Adds trusted relationships events and accessible naming to the combobox Button. | | `listbox_attrs` | `Mapping[str, object] | None` | `None` | Adds trusted nonconflicting Listbox attributes. |
    #### CSelect client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string | null` | Releases control to committed selection. | Controls selected value while supplied. | | `open` | `boolean | null` | Releases control to committed visibility. | Controls popup visibility while supplied. | | `required` | `bool` | Uses the server or Field fallback. | Reactively changes required validity. | | `disabled` | `bool` | Uses the server or Field fallback. | Reactively disables selection. | | `readonly` | `bool` | Uses the server or Field fallback. | Reactively prevents changes while preserving submission. | | `invalid` | `bool` | Uses the server or Field fallback. | Reactively changes invalid presentation. | | `loop` | `bool` | Uses the server value. | Reactively changes arrow wrapping. | | `placement` | `CSelectPlacement` | Uses the server value. | Reactively changes preferred placement. | | `matchWidth` | `bool` | Uses the server value. | Reactively changes popup sizing. | | `variant` | `CSelectVariant` | Uses the server value. | Reactively changes treatment. | | `size` | `CSelectSize` | Uses the server value. | Reactively changes geometry. | | `onValueChange` | `((value: string | null, detail: CSelectValueChangeDetail) => void) | undefined` | No component callback runs. | Receives selection reset and structural requests. | | `onOpenChange` | `((open: boolean, detail: CSelectOpenChangeDetail) => void) | undefined` | No component callback runs. | Receives visibility requests and forced-close notices. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CSelect events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string | null, detail: CSelectValueChangeDetail) => void` ([`CSelectValueChangeDetail`](#select-interface-cselect-value-change-detail)) | Enabled selection reset or structural recovery. | `{value, previousValue, option, controlled, source, sourceEvent}` ([`CSelectValueChangeDetail`](#select-interface-cselect-value-change-detail)) | Commits immediately when uncontrolled and waits for owner acceptance when controlled. | | `onOpenChange` | `(open: boolean, detail: CSelectOpenChangeDetail) => void` ([`CSelectOpenChangeDetail`](#select-interface-cselect-open-change-detail)) | Visibility request or nonrejectable safety close. | `{open, reason, controlled, forced, source}` ([`CSelectOpenChangeDetail`](#select-interface-cselect-open-change-detail)) | Controlled requests notify without changing visibility; forced safety closes always hide. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CSelect CSS variables Apply these variables to `CSelect` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-select-background` | `color` | Control and popup surface. | `Canvas` | | `--cui-select-foreground` | `color` | Primary foreground. | `CanvasText` | | `--cui-select-placeholder-color` | `color` | Empty-value foreground. | `scheme-aware muted` | | `--cui-select-muted-color` | `color` | Description and disabled foreground. | `scheme-aware muted` | | `--cui-select-border-color` | `color` | Outline border. | `scheme-aware subtle border` | | `--cui-select-hover-background` | `color` | Highlighted Option surface. | `CanvasText mix` | | `--cui-select-selected-background` | `color` | Selected Option surface. | `scheme-aware blue` | | `--cui-select-selected-foreground` | `color` | Selected Option foreground. | `scheme-aware blue text` | | `--cui-select-focus-color` | `color` | Focus outline. | `Highlight` | | `--cui-select-radius` | `length` | Control and popup corners. | `0.625rem` | | `--cui-select-control-padding` | `length` | Control padding. | `size-derived` | | `--cui-select-option-padding` | `length` | Option padding. | `size-derived` | | `--cui-select-max-block-size` | `length` | Popup scroll boundary. | `18rem` | | `--cui-select-offset` | `length` | Anchor gap. | `0.25rem` | | `--cui-select-shadow` | `shadow` | Popup elevation. | `scheme-aware shadow` | | `--cui-select-duration` | `time` | Popup and indicator motion. | `120ms` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CSelect attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Control Button | `combobox` | Declares the select-only popup control. | | `role` | Listbox div | `listbox` | Declares the popup collection. | | `role` | Option div | `option` | Declares each value. | | `aria-expanded` | Control Button | `true | false` | Reflects popup visibility. | | `aria-controls` | Control Button | `IDREF` | Targets the Listbox. | | `aria-activedescendant` | Control Button | `IDREF or absent` | Identifies the highlighted open Option. | | `aria-required` | Control Button | `true or absent` | Mirrors effective required state. | | `aria-disabled` | Control Button | `true or absent` | Mirrors effective unavailability. | | `aria-readonly` | Control Button | `true or absent` | Mirrors read-only interaction. | | `aria-invalid` | Control Button | `true or absent` | Mirrors effective invalid presentation. | | `aria-selected` | Option div | `true | false` | Reflects effective selection. | | `data-open` | Root div | `present-or-absent` | Mirrors effective visibility. | | `data-empty` | Root div | `present-or-absent` | Mirrors no selected value. | | `data-required` | Root div | `present-or-absent` | Mirrors effective required state. | | `data-readonly` | Root div | `present-or-absent` | Mirrors read-only interaction. | | `data-invalid` | Root div | `present-or-absent` | Mirrors effective invalid presentation. | | `data-match-width` | Root div | `present-or-absent` | Mirrors popup width matching. | | `data-variant` | Root div | `outline | filled | plain` | Mirrors effective treatment. | | `data-size` | Root div | `sm | md | lg` | Mirrors effective geometry. | | `data-value` | Option div | `string` | Exposes stable identity. | | `data-selected` | Option div | `present-or-absent` | Mirrors selection. | | `data-highlighted` | Option div | `present-or-absent` | Mirrors active descendant. | | `data-disabled` | Root or Option div | `present-or-absent` | Mirrors effective unavailability. | | `data-placement` | Popup div | `CSelectPlacement` | Reflects preferred logical placement. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CSelect selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="root"]` | Root div | Stable root attrs and state surface. | | `[data-citry-ui-part="control"]` | Combobox Button | Visible control and focus owner. | | `[data-citry-ui-part="value"]` | Value span | Selected label or placeholder. | | `[data-citry-ui-part="indicator"]` | Indicator span | Decorative popup-state mark. | | `[data-citry-ui-part="popup"]` | Manual popover div | Top-layer scrolling surface. | | `[data-citry-ui-part="listbox"]` | Listbox div | Semantic collection. | | `[data-citry-ui-part="group"]` | Group div | Related Options. | | `[data-citry-ui-part="group-label"]` | Group label span | Visible group name. | | `[data-citry-ui-part="option"]` | Option div | Value semantics and state. | | `[data-citry-ui-part="option-label"]` | Option label span | Accessible Option name. | | `[data-citry-ui-part="option-description"]` | Option description span | Supporting description. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CSelectPlacement` | `Literal["bottom-start", "bottom-end", "top-start", "top-end"]` | | `CSelectVariant` | `Literal["outline", "filled", "plain"]` | | `CSelectSize` | `Literal["sm", "md", "lg"]` | | `CSelectChangeSource` | `Literal["pointer", "keyboard", "reset", "structure"]` | | `CSelectOpenReason` | `Literal["trigger", "keyboard", "selection", "escape", "tab", "outside", "focus-outside", "reset", "native", "ancestor"]` |
    #### `CSelectOption`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Stable unique form value. | | `label` | `str` | - | Visible accessible Option name. | | `description` | `str | None` | - | Optional separately described supporting text. | | `disabled` | `bool` | - | Prevents user selection. | | `group` | `str | None` | - | Optional contiguous visible group label. |
    #### `CSelectValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str | None` | - | Requested value. | | `previousValue` | `str | None` | - | Previous effective value. | | `option` | `HTMLElement | None` | - | Activated Option or None for reset and structure. | | `controlled` | `bool` | - | Whether client value owns selection. | | `source` | `CSelectChangeSource` | - | Request source. | | `sourceEvent` | `Event | None` | - | Native source event when present. |
    #### `CSelectOpenChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `open` | `bool` | - | Requested or forced visibility. | | `reason` | `CSelectOpenReason` | - | Visibility reason. | | `controlled` | `bool` | - | Whether client open owns visibility. | | `forced` | `bool` | - | Whether safety made the close nonrejectable. | | `source` | `EventTarget | None` | - | Native source or safety owner. |
    ### Translation keys - --- # Sliders Source: https://citry.dev/ui-library/components/slider/ # Slider and RangeSlider Use `CSlider` to choose one value from a bounded exact-decimal scale. Use `CRangeSlider` when the user chooses an ordered lower and upper value. ```citry Volume Price range ``` ### Choose one value [Open the rendered preview](/ui-library/components/slider/_previews/basic/) ````citry from decimal import Decimal from typing import Any import citry_ui from citry import Component, citry from citry_ui import CSlider citry.register_library(citry_ui) class BasicSlider(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "python_slider": CSlider( value=Decimal("0.5"), min=Decimal(0), max=Decimal(1), step=Decimal("0.1"), input_attrs={"aria-label": "Python opacity"}, ) } template = """
    Volume Use arrow keys for one-percent steps.

    Python composition

    {{ python_slider }}
    """ css = """ :where(.slider-example-grid){display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));gap:1.5rem} :where(.slider-example-grid article){display:grid;gap:.75rem}:where(.slider-example-grid h3){margin:0} """ preview = BasicSlider() preview # noqa: B018 ```` ### Choose a value range [Open the rendered preview](/ui-library/components/slider/_previews/range/) ````citry from citry import Component class RangeSliderExample(Component): template = """ Price range Lower and upper values stay at least 10 apart. """ preview = RangeSliderExample() preview # noqa: B018 ```` ## Choose exact values Server inputs accept `int`, `Decimal`, or a canonical plain-decimal string. Floats and exponent notation are rejected. The difference between `min` and `max` must contain a whole number of `step` intervals, capped at one million. Form submission and callbacks use canonical ASCII strings, so values such as `Decimal("0.300")` submit as `0.3` without binary-float drift. `large_step` controls Page Up and Page Down. It defaults to ten steps. Marks label selected grid positions; they do not add selectable values or alter the step grid. ### Use an exact decimal scale [Open the rendered preview](/ui-library/components/slider/_previews/exact-decimals/) ````citry from decimal import Decimal from typing import Any from citry import Component class ExactDecimalSlider(Component): def template_data(self, kwargs: Any, slots: Any) -> dict[str, Any]: # noqa: ARG002 return { "value": Decimal("0.30"), "marks": {Decimal("0.1"): "Low", Decimal("0.3"): "Target", Decimal("0.5"): "High"}, } template = """ Opacity Exact 0.05 steps avoid binary floating-point drift. """ preview = ExactDecimalSlider() preview # noqa: B018 ```` ### Label selected values [Open the rendered preview](/ui-library/components/slider/_previews/marks/) ````citry from typing import Any from citry import Component class SliderMarks(Component): def template_data(self, kwargs: Any, slots: Any) -> dict[str, Any]: # noqa: ARG002 return {"marks": {0: "Silent", 25: "Quiet", 50: "Medium", 75: "Loud", 100: "Maximum"}} template = """ Playback volume """ preview = SliderMarks() preview # noqa: B018 ```` ## Pick one value or an interval `CSlider` contributes one form entry. `CRangeSlider name="price"` contributes two ordered entries with the same name. Use `lower_name` and `upper_name` together when the server expects distinct field names. Range thumbs keep their lower and upper identities, remain in the same Tab order, and do not cross, swap, or push each other. `min_steps_between_thumbs` sets a grid-step gap between them. ### Submit Slider values [Open the rendered preview](/ui-library/components/slider/_previews/forms/) ````citry from citry import Component class SliderForm(Component): template = """
    Budget
    Submit to inspect values
    """ css = ":where(.slider-example-stack){display:grid;gap:1rem;max-inline-size:32rem}" preview = SliderForm() preview # noqa: B018 ```` ## Keyboard and pointer behavior Arrow Right and Arrow Up add one step; Arrow Left and Arrow Down subtract one. Page Up and Page Down use `large_step`; Home and End move to the current thumb's allowed bounds. For a range, Tab visits lower then upper. Horizontal pointer geometry mirrors in RTL while keyboard value direction stays stable. The no-JavaScript fallback is one native range input for `CSlider` and two clearly labeled native range inputs for `CRangeSlider`. Once enhanced, the styled thumbs take over interaction while the native controls continue to own form submission and reset. ### Use vertical Sliders [Open the rendered preview](/ui-library/components/slider/_previews/vertical/) ````citry from citry import Component class VerticalSliders(Component): template = """
    """ css = ":where(.vertical-slider-row){display:flex;gap:3rem;min-block-size:14rem;align-items:center}" preview = VerticalSliders() preview # noqa: B018 ```` ## Controlled values and callbacks Omitting client `value` leaves the component uncontrolled. Supplying it through `$c-props` makes every interaction a request: the thumb moves only after the owner returns the requested value. `onValueChange` fires during each accepted pointer or keyboard step. `onValueChangeEnd` fires once at the end of a pointer gesture and once after a keyboard request. ```citry
    ``` ### Control Slider values [Open the rendered preview](/ui-library/components/slider/_previews/controlled/) ````citry from citry import Component class ControlledRangeSlider(Component): template = """
    Selected 20 through 80
    """ css = ":where(.slider-example-stack){display:grid;gap:1rem;max-inline-size:32rem}" preview = ControlledRangeSlider() preview # noqa: B018 ```` ## Labels, fields, and localization Wrap either component in `CField` for its visible label, description, error, disabled, readonly, and invalid state. A standalone `CSlider` needs an accessible name through `input_attrs`. `CRangeSlider` combines the Field label with localized “Lower value” and “Upper value” labels; override those strings with `lower_label` and `upper_label` when the application needs domain-specific names. Displayed values and `aria-valuetext` use the `number.citry-ui-slider` profile. Under a client-enabled `c-i18n` provider, thumb labels and formatted values update after a browser-side locale switch. Canonical form values never change. ### Format localized Slider values [Open the rendered preview](/ui-library/components/slider/_previews/locales/) ````citry from citry import Component class LocalizedSlider(Component): template = """

    Inside a client-enabled <c-i18n>, labels and formatted values switch locale in place.

    Canonical Form values remain 1234.5 and 5678.5.

    """ css = ":where(.slider-example-stack){display:grid;gap:1rem;max-inline-size:36rem}" preview = LocalizedSlider() preview # noqa: B018 ```` ## State and customization `readonly` preserves a submitted value and focusable slider semantics while blocking mutation. `disabled` removes interaction and form participation. Choose `solid` or `subtle`, three sizes, horizontal or vertical orientation, and `never`, `interaction`, or `always` value bubbles. Use the documented CSS variables and part selectors for styling; `attrs` and input-attribute mappings cannot replace state, form, identity, or accessibility attributes owned by the component. ### Compare Slider states [Open the rendered preview](/ui-library/components/slider/_previews/states/) ````citry from citry import Component class SliderStates(Component): template = """
    """ css = ":where(.slider-state-grid){display:grid;gap:1.5rem;max-inline-size:36rem}" preview = SliderStates() preview # noqa: B018 ```` ## API reference ### Inputs #### CSlider server inputs Server inputs are passed in a template through `` or in Python through `CSlider(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `CSliderExact | None` ([`CSliderExact`](#slider-interface-exact)) | `None` | Sets the initial exact value; None uses min. | | `name` | `str | None` | `None` | Names the progressive native Form entry. | | `form` | `str | None` | `None` | Associates the Form entry with an external Form ID. | | `id` | `str | None` | generated | Sets the public native input ID and enhanced label target. | | `min` | `CSliderExact` ([`CSliderExact`](#slider-interface-exact)) | `0` | Sets the inclusive exact minimum and step-grid origin. | | `max` | `CSliderExact` ([`CSliderExact`](#slider-interface-exact)) | `100` | Sets the inclusive exact maximum. | | `step` | `CSliderExact` ([`CSliderExact`](#slider-interface-exact)) | `1` | Sets the positive exact grid interval. | | `large_step` | `CSliderExact | None` ([`CSliderExact`](#slider-interface-exact)) | Ten steps. | Sets the positive whole-step Page Up and Page Down interval. | | `disabled` | `bool | None` | `None` | Blocks focus mutation and Form participation outside Field. | | `readonly` | `bool | None` | `None` | Preserves focus and submission while blocking mutation outside Field. | | `invalid` | `bool | None` | `None` | Reflects application invalid state outside Field. | | `orientation` | `"horizontal" | "vertical"` ([`CSliderOrientation`](#slider-interface-orientation)) | `"horizontal"` | Selects track orientation. | | `variant` | `"solid" | "subtle"` ([`CSliderVariant`](#slider-interface-variant)) | `"solid"` | Selects visual treatment. | | `size` | `"sm" | "md" | "lg"` ([`CSliderSize`](#slider-interface-size)) | `"md"` | Selects track and thumb sizing. | | `show_value` | `"never" | "interaction" | "always"` ([`CSliderShowValue`](#slider-interface-show-value)) | `"interaction"` | Controls localized value bubbles. | | `show_marks` | `bool | None` | True when marks exist. | Shows or hides mark dots and labels. | | `marks` | `Mapping[CSliderExact, str] | Sequence[CSliderExact] | None` | `None` | Adds up to 101 bounded step-grid marks. | | `format` | `str` | `"citry-ui-slider"` | Selects the named i18n number format profile for visible and accessible values. | | `class_` | `CClassValue | None` ([`CClassValue`](#slider-interface-class-value)) | `None` | Adds classes to the documented root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#slider-interface-style-value)) | `None` | Adds styles to the documented root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing owned state or identity. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed native-input attributes including standalone accessible naming. |
    #### CRangeSlider server inputs Server inputs are passed in a template through `` or in Python through `CRangeSlider(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `tuple[CSliderExact, CSliderExact] | None` ([`CSliderExact`](#slider-interface-exact)) | (min, max) | Sets the initial ordered lower and upper exact values. | | `name` | `str | None` | `None` | Names both ordered Form entries when separate names are omitted. | | `lower_name` | `str | None` | `None` | Names the lower Form entry when supplied together with upper_name. | | `upper_name` | `str | None` | `None` | Names the upper Form entry when supplied together with lower_name. | | `form` | `str | None` | `None` | Associates both Form entries with an external Form ID. | | `id` | `str | None` | generated | Sets the lower native input ID and bases the generated upper and root IDs. | | `min` | `CSliderExact` ([`CSliderExact`](#slider-interface-exact)) | `0` | Sets the inclusive exact minimum and step-grid origin. | | `max` | `CSliderExact` ([`CSliderExact`](#slider-interface-exact)) | `100` | Sets the inclusive exact maximum. | | `step` | `CSliderExact` ([`CSliderExact`](#slider-interface-exact)) | `1` | Sets the positive exact grid interval. | | `large_step` | `CSliderExact | None` ([`CSliderExact`](#slider-interface-exact)) | Ten steps. | Sets the positive whole-step Page Up and Page Down interval. | | `min_steps_between_thumbs` | `int` | `0` | Keeps this many grid intervals between fixed lower and upper thumbs. | | `disabled` | `bool | None` | `None` | Blocks focus mutation and Form participation outside Field. | | `readonly` | `bool | None` | `None` | Preserves focus and ordered submission while blocking mutation outside Field. | | `invalid` | `bool | None` | `None` | Reflects application invalid state outside Field. | | `orientation` | `"horizontal" | "vertical"` ([`CSliderOrientation`](#slider-interface-orientation)) | `"horizontal"` | Selects track orientation. | | `variant` | `"solid" | "subtle"` ([`CSliderVariant`](#slider-interface-variant)) | `"solid"` | Selects visual treatment. | | `size` | `"sm" | "md" | "lg"` ([`CSliderSize`](#slider-interface-size)) | `"md"` | Selects track and thumb sizing. | | `show_value` | `"never" | "interaction" | "always"` ([`CSliderShowValue`](#slider-interface-show-value)) | `"interaction"` | Controls both localized value bubbles. | | `show_marks` | `bool | None` | True when marks exist. | Shows or hides mark dots and labels. | | `marks` | `Mapping[CSliderExact, str] | Sequence[CSliderExact] | None` | `None` | Adds up to 101 bounded step-grid marks. | | `format` | `str` | `"citry-ui-slider"` | Selects the named i18n number format profile for both values. | | `lower_label` | `str` | `"Lower value"` | Overrides the catalog-backed lower-thumb accessible name. | | `upper_label` | `str` | `"Upper value"` | Overrides the catalog-backed upper-thumb accessible name. | | `class_` | `CClassValue | None` ([`CClassValue`](#slider-interface-class-value)) | `None` | Adds classes to the documented root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#slider-interface-style-value)) | `None` | Adds styles to the documented root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing owned state or identity. | | `lower_input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed attributes to the lower native input. | | `upper_input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed attributes to the upper native input. |
    #### CSlider client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `canonical decimal string` | Releases control to the last uncontrolled value. | Controls the exact value while supplied. | | `min` | `canonical decimal string` | Uses the server value. | Replaces the minimum when the resulting grid is valid. | | `max` | `canonical decimal string` | Uses the server value. | Replaces the maximum when the resulting grid is valid. | | `step` | `positive canonical decimal string` | Uses the server value. | Replaces the grid interval when min and max contain whole steps. | | `largeStep` | `positive canonical decimal string` | Uses the server value. | Replaces the Page Up and Page Down interval. | | `disabled` | `boolean` | Uses server or owner state. | Controls mutation and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable state. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `orientation` | `CSliderOrientation` ([`CSliderOrientation`](#slider-interface-orientation)) | Uses the server value. | Controls track orientation. | | `variant` | `CSliderVariant` ([`CSliderVariant`](#slider-interface-variant)) | Uses the server value. | Controls visual treatment. | | `size` | `CSliderSize` ([`CSliderSize`](#slider-interface-size)) | Uses the server value. | Controls coordinated sizing. | | `showValue` | `CSliderShowValue` ([`CSliderShowValue`](#slider-interface-show-value)) | Uses the server value. | Controls value-bubble visibility. | | `format` | `string` | Uses the server profile. | Controls locale-aware visible and accessible value formatting. | | `onValueChange` | `function` | No live value callback. | Receives each user value request. | | `onValueChangeEnd` | `function` | No completed-interaction callback. | Receives each keyboard request and completed pointer gesture. |
    #### CRangeSlider client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `[canonical decimal string, canonical decimal string]` | Releases control to the last uncontrolled pair. | Controls the ordered exact pair while supplied. | | `min` | `canonical decimal string` | Uses the server value. | Replaces the minimum when the resulting grid is valid. | | `max` | `canonical decimal string` | Uses the server value. | Replaces the maximum when the resulting grid is valid. | | `step` | `positive canonical decimal string` | Uses the server value. | Replaces the grid interval when min and max contain whole steps. | | `largeStep` | `positive canonical decimal string` | Uses the server value. | Replaces the Page Up and Page Down interval. | | `minStepsBetweenThumbs` | `nonnegative integer` | Uses the server value. | Controls the minimum lower-to-upper grid gap. | | `disabled` | `boolean` | Uses server or owner state. | Controls mutation and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable state. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `orientation` | `CSliderOrientation` ([`CSliderOrientation`](#slider-interface-orientation)) | Uses the server value. | Controls track orientation. | | `variant` | `CSliderVariant` ([`CSliderVariant`](#slider-interface-variant)) | Uses the server value. | Controls visual treatment. | | `size` | `CSliderSize` ([`CSliderSize`](#slider-interface-size)) | Uses the server value. | Controls coordinated sizing. | | `showValue` | `CSliderShowValue` ([`CSliderShowValue`](#slider-interface-show-value)) | Uses the server value. | Controls both value bubbles. | | `format` | `string` | Uses the server profile. | Controls locale-aware visible and accessible value formatting. | | `onValueChange` | `function` | No live value callback. | Receives each ordered-pair request. | | `onValueChangeEnd` | `function` | No completed-interaction callback. | Receives each keyboard request and completed pointer gesture. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CSlider events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string, detail: CSliderValueChangeDetail) => void` ([`CSliderValueChangeDetail`](#slider-interface-cslider-value-change-detail)) | Each pointer-drag or keyboard value request. | `{value, previousValue, controlled, source, sourceEvent, phase}` ([`CSliderValueChangeDetail`](#slider-interface-cslider-value-change-detail)) | Uncontrolled state and native Form value update before notification; controlled state is request-only. | | `onValueChangeEnd` | `(value: string, detail: CSliderValueChangeDetail) => void` ([`CSliderValueChangeDetail`](#slider-interface-cslider-value-change-detail)) | A keyboard request or completed changed pointer gesture. | `{value, previousValue, controlled, source, sourceEvent, phase}` ([`CSliderValueChangeDetail`](#slider-interface-cslider-value-change-detail)) | Reports the final requested value once per completed interaction. |
    #### CRangeSlider events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: tuple[str, str], detail: CRangeSliderValueChangeDetail) => void` ([`CRangeSliderValueChangeDetail`](#slider-interface-crange-slider-value-change-detail)) | Each lower or upper pointer-drag or keyboard pair request. | `{value, previousValue, controlled, source, sourceEvent, phase, activeThumb}` ([`CRangeSliderValueChangeDetail`](#slider-interface-crange-slider-value-change-detail)) | Preserves ordered stable thumb identity; controlled state is request-only. | | `onValueChangeEnd` | `(value: tuple[str, str], detail: CRangeSliderValueChangeDetail) => void` ([`CRangeSliderValueChangeDetail`](#slider-interface-crange-slider-value-change-detail)) | A keyboard request or completed changed pointer gesture. | `{value, previousValue, controlled, source, sourceEvent, phase, activeThumb}` ([`CRangeSliderValueChangeDetail`](#slider-interface-crange-slider-value-change-detail)) | Reports the final requested ordered pair once per completed interaction. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CSlider CSS variables Apply these variables to `CSlider` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-slider-track-color` | `color` | Unfilled rail color. | `Mixed CanvasText.` | | `--cui-slider-fill-color` | `color` | Selected rail color. | `AccentColor` | | `--cui-slider-thumb-color` | `color` | Thumb fill. | `Canvas` | | `--cui-slider-thumb-border-color` | `color` | Thumb outline. | `AccentColor` | | `--cui-slider-focus-color` | `color` | Keyboard focus ring. | `Highlight` | | `--cui-slider-mark-color` | `color` | Mark dots. | `CanvasText` | | `--cui-slider-value-background` | `color` | Value-bubble background. | `High-contrast ink.` | | `--cui-slider-value-foreground` | `color` | Value-bubble text. | `High-contrast surface.` | | `--cui-slider-track-size` | `length` | Rail thickness. | `0.375rem` | | `--cui-slider-thumb-size` | `length` | Thumb diameter. | `1.25rem` | | `--cui-slider-control-size` | `length` | Minimum interaction block size. | `2.75rem` | | `--cui-slider-radius` | `length` | Rail and thumb rounding. | `999px` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CSlider attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-disabled` | Root div | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Root div | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Root div | `present | absent` | Mirrors effective invalid state. | | `data-dragging` | Root div | `present | absent` | Marks an active pointer gesture. | | `data-orientation` | Root div | `CSliderOrientation` ([`CSliderOrientation`](#slider-interface-orientation)) | Mirrors track orientation. | | `data-variant` | Root div | `CSliderVariant` ([`CSliderVariant`](#slider-interface-variant)) | Mirrors visual treatment. | | `data-size` | Root div | `CSliderSize` ([`CSliderSize`](#slider-interface-size)) | Mirrors coordinated sizing. | | `data-show-value` | Root div | `CSliderShowValue` ([`CSliderShowValue`](#slider-interface-show-value)) | Mirrors value-bubble policy. |
    #### CSlider attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Enhanced thumb Button | `"slider"` | Exposes slider interaction semantics. | | `aria-valuenow` | Enhanced thumb Button | `canonical decimal` | Exposes the exact current value. | | `aria-valuetext` | Enhanced thumb Button | `localized string` | Exposes the locale-formatted current value. | | `aria-valuemin` | Enhanced thumb Button | `canonical decimal` | Exposes the current inclusive lower bound. | | `aria-valuemax` | Enhanced thumb Button | `canonical decimal` | Exposes the current inclusive upper bound. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CSlider selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="slider"]` | CSlider root div | State reflections and root customization destination. | | `[data-citry-ui-part="range-slider"]` | CRangeSlider root div | State reflections and root customization destination. | | `[data-citry-ui-part="native-input"]` | Native range input | No-JavaScript fallback and enhanced Form transport. | | `[data-citry-ui-part="control"]` | Enhanced control div | Pointer interaction surface. | | `[data-citry-ui-part="track"]` | Track div | Positions fill marks and thumbs. | | `[data-citry-ui-part="fill"]` | Fill span | Shows the selected value or interval. | | `[data-citry-ui-part="mark"]` | Mark span | Shows a configured grid position. | | `[data-citry-ui-part="mark-label"]` | Mark label span | Shows application-owned mark text. | | `[data-citry-ui-part="thumb"]` | Enhanced slider Button | Keyboard focus target and draggable value owner. | | `[data-citry-ui-part="value"]` | Value span | Shows the locale-formatted current value. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CSliderExact` | `int | Decimal | str` | | `CSliderOrientation` | `Literal["horizontal", "vertical"]` | | `CSliderVariant` | `Literal["solid", "subtle"]` | | `CSliderSize` | `Literal["sm", "md", "lg"]` | | `CSliderShowValue` | `Literal["never", "interaction", "always"]` | | `CSliderChangeSource` | `Literal["pointer", "keyboard", "reset"]` | | `CSliderChangePhase` | `Literal["change", "end"]` | | `CRangeSliderThumb` | `Literal["lower", "upper"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CSliderValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `canonical decimal string` | - | Requested exact value. | | `previousValue` | `canonical decimal string` | - | Effective exact value before the interaction. | | `controlled` | `boolean` | - | Whether client value owns state. | | `source` | `CSliderChangeSource` ([`CSliderChangeSource`](#slider-interface-change-source)) | - | Pointer keyboard or reset cause. | | `sourceEvent` | `object | null` | - | Native interaction event when one exists. | | `phase` | `CSliderChangePhase` ([`CSliderChangePhase`](#slider-interface-change-phase)) | - | Live change or completed interaction. |
    #### `CRangeSliderValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `[canonical decimal string, canonical decimal string]` | - | Requested ordered exact pair. | | `previousValue` | `[canonical decimal string, canonical decimal string]` | - | Effective ordered pair before the interaction. | | `controlled` | `boolean` | - | Whether client value owns state. | | `source` | `CSliderChangeSource` ([`CSliderChangeSource`](#slider-interface-change-source)) | - | Pointer keyboard or reset cause. | | `sourceEvent` | `object | null` | - | Native interaction event when one exists. | | `phase` | `CSliderChangePhase` ([`CSliderChangePhase`](#slider-interface-change-phase)) | - | Live change or completed interaction. | | `activeThumb` | `CRangeSliderThumb` ([`CRangeSliderThumb`](#slider-interface-range-thumb)) | - | Stable lower or upper thumb that requested the change. |
    ### 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. #### CRangeSlider translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-range-slider-lower` | Distinguishes the lower thumb and native fallback input. | `None` | `lower_label` | $c-tr updates the stable hidden label; both controls reference it. | | `citry-ui-range-slider-upper` | Distinguishes the upper thumb and native fallback input. | `None` | `upper_label` | $c-tr updates the stable hidden label; both controls reference it. |
    --- # Switch Source: https://citry.dev/ui-library/components/switch/ # Switch Use `CSwitch` for a setting that takes effect immediately. Use Checkbox for a selection or acknowledgement, and Button for an action. ## Switch at a glance ### Switch at a glance [Open the rendered preview](/ui-library/components/switch/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SwitchAtAGlance(Component): template = """

    Evening room

    Reading lamp Window shades Quiet ventilation Keep air moving below the bedroom.
    """ css = """ :where(.switch-room) { display: grid; gap: 0.9rem; max-inline-size: 28rem; padding: 1.25rem; border: 1px solid light-dark(#c8bda8, #665d50); border-radius: 0.9rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.switch-room h2) { margin: 0; } """ preview = SwitchAtAGlance() preview # noqa: B018 ```` ## Change an immediate setting The visible label describes the setting and stays the same when state changes. ### Change home settings [Open the rendered preview](/ui-library/components/switch/_previews/basic/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class HomeSettings(Component): template = """ Porch light Robot vacuum schedule Door chime """ preview = HomeSettings() preview # noqa: B018 ```` ## Add descriptions Description content is connected to the native Switch. Disabled switches stay visible but cannot change or submit. ### Describe Switch settings [Open the rendered preview](/ui-library/components/switch/_previews/descriptions/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DescribedSwitches(Component): template = """ Air purifier Runs quietly until the room reaches clean-air target. Fireplace fan Available while the fireplace is warm. """ preview = DescribedSwitches() preview # noqa: B018 ```` ## Control state in the browser Pass `checked` through `$c-props="{...}"`; handle native `input` with `$event.target.checked`. Omit the prop to release browser ownership. ### Control a Switch [Open the rendered preview](/ui-library/components/switch/_previews/controlled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledSwitch(Component): template = """
    Reading mode
    """ css = """ :where(.switch-controlled) { display: grid; gap: 0.7rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.switch-controlled output) { color: light-dark(#3f6212, #bef264); font-size: 0.82rem; } """ preview = ControlledSwitch() preview # noqa: B018 ```` ## Submit and validate A checked named Switch contributes its value to FormData. Required means the setting must be on. ### Submit Switch settings [Open the rendered preview](/ui-library/components/switch/_previews/forms/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SwitchForm(Component): template = """
    Quiet hours Save home settings
    """ css = """ :where(.switch-form) { display: grid; gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = SwitchForm() preview # noqa: B018 ```` ## Choose size and label position Use `sm`, `md`, or `lg`. `label_pos="start"` puts text before the control in logical reading order. ### Compare Switch presentation [Open the rendered preview](/ui-library/components/switch/_previews/presentation/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SwitchPresentation(Component): class Kwargs: pass class Slots: pass template = """ {{ size }} switch Label before track """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"sizes": ("sm", "md", "lg")} preview = SwitchPresentation() preview # noqa: B018 ```` ## Compose with Field Inside `CField`, Field owns label, description, error, required, disabled, and invalid state. Do not add Switch slots there. ### Compose Switch with Field [Open the rendered preview](/ui-library/components/switch/_previews/field/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SwitchField(Component): template = """ Away mode Lower heating and pause routine lighting. Enable away mode before leaving. """ preview = SwitchField() preview # noqa: B018 ```` ## Use Switch semantics deliberately Switches announce on/off. Keep their labels stable and use them only for immediate settings. ### Choose Switch or Checkbox [Open the rendered preview](/ui-library/components/switch/_previews/semantics/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ChoiceSemantics(Component): template = """ Automatic hallway lighting Takes effect immediately. Include spare keys in the move checklist A selection, not an immediate setting. """ preview = ChoiceSemantics() preview # noqa: B018 ```` ## Customize Switch Override public colors, geometry, motion, and part selectors. ### Customize Switch with public CSS [Open the rendered preview](/ui-library/components/switch/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CustomSwitch(Component): template = """
    Oak reading nook
    """ css = """ :where(.switch-oak) { --cui-switch-on-color: light-dark(#7c4a25, #d8a06f); --cui-switch-off-color: light-dark(#8f8376, #9f9385); --cui-switch-thumb-color: light-dark(#fffaf2, #2a2119); --cui-switch-width: 3.4rem; --cui-switch-height: 1.9rem; padding: 1rem; border: 1px solid light-dark(#c6ad91, #725b44); border-radius: 0.8rem; } """ preview = CustomSwitch() preview # noqa: B018 ```` ## API reference ### Inputs #### CSwitch server inputs Server inputs are passed in a template through `` or in Python through `CSwitch(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `name` | `str | None` | `None` | Sets the optional native/FormData name. | | `value` | `str` | `"on"` | Sets the submitted token while checked. | | `id` | `str | None` | `None` | Sets native input identity and the label relationship. | | `checked` | `bool` | `False` | Sets server default checkedness. | | `required` | `bool | None` | `None` | Requires the setting to be on; CField owns it when composed. | | `disabled` | `bool | None` | `None` | Disables activation and submission; Field/Form remain dominant. | | `invalid` | `bool | None` | `None` | Sets explicit invalid styling and relationships; CField owns it when composed. | | `size` | `"sm" | "md" | "lg"` ([`CSwitchSize`](#switch-interface-size)) | `"md"` | Sets control and text scale. | | `label_pos` | `"start" | "end"` ([`CSwitchLabelPos`](#switch-interface-label-pos)) | `"end"` | Places the visible label before or after the track. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#switch-interface-class-value)) | `None` | Adds root classes and merges them with attrs. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#switch-interface-style-value)) | `None` | Adds root inline styles and merges them with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted nonconflicting metadata and targeted Alpine attributes to the root. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted nonconflicting naming metadata and native listeners to the input. |
    #### CSwitch client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `checked` | `boolean` | Releases control to native checkedness. | Controls current checkedness; omission releases control. | | `value` | `string` | Uses the server fallback. | Controls the native submission token. | | `required` | `boolean` | Uses the server or Field fallback. | Controls native required state outside Field. | | `disabled` | `boolean` | Uses the server or Field/Form fallback. | Controls local disabled state outside Field. | | `invalid` | `boolean` | Uses the server or Field fallback. | Controls explicit invalid state outside Field. | | `size` | `"sm" | "md" | "lg"` | Uses the server fallback. | Controls public size. | | `label_pos` | `"start" | "end"` | Uses the server fallback. | Controls logical label placement. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CSwitch slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CSwitchDefaultSlotData`](#switch-interface-default)) | Label-free standalone Switch requires an ARIA name; the slot is forbidden under CField. | | `description` | no | `{}` ([`CSwitchDescriptionSlotData`](#switch-interface-description)) | Description and relationship are omitted; the slot is forbidden under CField. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CSwitch CSS variables Apply these variables to `CSwitch` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-switch-off-color` | `color` | Track color while off. | `Scheme-aware neutral.` | | `--cui-switch-on-color` | `color` | Track color while on. | `Scheme-aware primary.` | | `--cui-switch-thumb-color` | `color` | Thumb fill. | `Canvas.` | | `--cui-switch-foreground` | `color` | Label and description foreground. | `CanvasText.` | | `--cui-switch-focus-color` | `color` | Keyboard focus ring. | `Highlight.` | | `--cui-switch-invalid-color` | `color` | Invalid-state outline. | `Scheme-aware danger.` | | `--cui-switch-disabled-opacity` | `number` | Disabled root opacity. | `0.52.` | | `--cui-switch-width` | `length` | Track inline size. | `Size-derived length.` | | `--cui-switch-height` | `length` | Track block size. | `Size-derived length.` | | `--cui-switch-padding` | `length` | Track inset around the thumb. | `0.1875rem.` | | `--cui-switch-gap` | `length` | Track-to-label spacing. | `0.625rem.` | | `--cui-switch-duration` | `time` | Track and thumb transition duration. | `140ms.` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CSwitch attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Native input | `"switch"` | Exposes on/off semantics. | | `checked` | Native input | `boolean present or absent` | Server default checkedness; current checkedness is the native property. | | `required` | Native input | `boolean present or absent` | Native required state. | | `disabled` | Native input | `boolean present or absent` | Native disabled state. | | `data-checked` | Root | `boolean present or absent` | Mirrors current native checkedness. | | `data-required` | Root | `boolean present or absent` | Mirrors effective required state. | | `data-disabled` | Root | `boolean present or absent` | Mirrors effective disabled state. | | `data-invalid` | Root | `boolean present or absent` | Mirrors explicit or native invalid state. | | `data-size` | Root | `"sm" | "md" | "lg"` | Mirrors effective size. | | `data-label-pos` | Root | `"start" | "end"` | Mirrors logical label placement. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CSwitch selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="switch"]` | Root span | Root styling and attrs destination. | | `[data-citry-ui-part="input"]` | Native checkbox input | Focus form state and native events. | | `[data-citry-ui-part="surface"]` | Presentation span | Shared track and text layout surface. | | `[data-citry-ui-part="track"]` | Decorative span | Off/on visual track. | | `[data-citry-ui-part="thumb"]` | Decorative span | Moving state indicator. | | `[data-citry-ui-part="body"]` | Text wrapper span | Label and description layout. | | `[data-citry-ui-part="label"]` | Visible label span | Stable setting name. | | `[data-citry-ui-part="description"]` | Description span | Optional connected guidance. |
    ### 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]` | | `CSwitchSize` | `Literal["sm", "md", "lg"]` | | `CSwitchLabelPos` | `Literal["start", "end"]` |
    #### `CSwitchDefaultSlotData` Empty dataclass: `{}`. #### `CSwitchDescriptionSlotData` Empty dataclass: `{}`. ### Translation keys - --- # TagsInput Source: https://citry.dev/ui-library/components/tags-input/ # TagsInput Use `CTagsInput` when a person creates an ordered list of free-form strings, such as labels, aliases, search terms, or routing keys. Committed tags and the unfinished editor draft are separate values. Use [MultiSelect](/ui-library/components/multi-select/) when choices come from a fixed collection. Suggestions, remote filtering, and create-from-search belong to a future Combobox rather than this component. Use [Tag and TagGroup](/ui-library/components/tag/) to display tags without an editor or native Form value. ## Add and submit tags Press Enter or type a configured delimiter to add one tag. Each committed tag becomes one selected Option in a native multiple Select, so `FormData.getAll(name)` returns repeated values in tag order. ```citry-html ``` Standalone use requires a nonempty static `aria-label` in `input_attrs`. Compose the component inside `CField` when it needs a visible label, description, error, required marker, or shared disabled and readonly state. ### Template and Python TagsInput composition [Open the rendered preview](/ui-library/components/tags-input/_previews/basic-tags/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTagsInput citry.register_library(citry_ui) class BasicTagsInput(Component): class Kwargs: pass class Slots: pass def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, Any]: return { "python_tags": CTagsInput( name="reviewers", value=("ada@example.test", "grace@example.test"), variant="filled", input_attrs={"aria-label": "Reviewers"}, ) } template = """
    Routing labels Press Enter or comma to add a label.

    Direct Python composition

    {{ python_tags }}
    Nothing submitted yet
    """ css = """ :where(.tags-input-basic) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-basic form, .tags-input-basic article) { display: grid; gap: 0.75rem; align-content: start; margin: 0; padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 0.75rem; } :where(.tags-input-basic h3) { margin: 0; } :where(.tags-input-basic output) { grid-column: 1 / -1; } """ preview = BasicTagsInput() preview # noqa: B018 ```` ## Control committed tags and the draft separately Client `value` owns the ordered committed tags. Client `inputValue` owns the raw editor draft. Either axis can be controlled alone, both can be controlled, or both can remain uncontrolled. `onValueChange` receives a complete proposed collection. A controlled request does not update tags or native Form values until the owner supplies that exact collection. An uncontrolled draft clears only after the related value request is accepted, so refusing a controlled value does not erase the person's text. ### Control tags and draft ownership [Open the rendered preview](/ui-library/components/tags-input/_previews/controlled-axes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledTagsInputAxes(Component): template = """

    Uncontrolled tags and draft

    Uncontrolled

    Controlled draft

    Draft owned

    Controlled tags, uncontrolled draft

    Value request not sent

    Controlled tags and draft

    Both axes owned
    """ css = """ :where(.tags-input-controlled) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-controlled article) { display: grid; gap: 0.75rem; align-content: start; padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 0.75rem; } :where(.tags-input-controlled h3) { margin: 0; } """ preview = ControlledTagsInputAxes() preview # noqa: B018 ```` Passing `null` or removing a controlled axis releases it to its latest uncontrolled committed baseline. It does not adopt the last controlled value. ## Keep paste and IME input atomic Paste text containing a delimiter or newline to add several tags at once. The component replaces the current editor selection, validates every completed fragment, and commits the batch in order. The final unterminated fragment remains the draft. If any fragment is empty, duplicated, invalid, or over `max_tags`, the whole batch is rejected. Existing tags, draft text, and selection remain unchanged. The component never partially accepts a paste. ### Paste, delimiters, and composition [Open the rendered preview](/ui-library/components/tags-input/_previews/paste-and-ime/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputPasteAndIme(Component): template = """
    Survey regions Comma, semicolon, and a pasted newline separate regions. At most five tags are accepted.

    Try replacing selected draft text with:

    coast,forest;wetland
    harbor
    Paste or compose in the editor

    The input method editor owns Enter and delimiters.

    """ css = """ :where(.tags-input-paste) { display: grid; gap: 1rem; max-inline-size: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-paste__sample) { padding: 0.85rem; border-radius: 0.75rem; background: color-mix(in srgb, CanvasText 6%, Canvas); } :where(.tags-input-paste__sample p) { margin-block-start: 0; } :where(.tags-input-paste pre) { margin: 0; white-space: pre-wrap; } """ preview = TagsInputPasteAndIme() preview # noqa: B018 ```` Enter and delimiters do not commit while an input method editor is composing. The final non-composing input is reconciled once after composition ends. ## Preserve native Form behavior The visible text editor is unnamed. The hidden native [`select multiple`](https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element) owns `name`, `form`, native required validity, and repeated values. A nonempty editable draft sets native custom validity until the person commits or clears it, so submission cannot silently omit unfinished text. ### Required values, external Forms, and reset [Open the rendered preview](/ui-library/components/tags-input/_previews/forms-and-reset/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputFormsAndReset(Component): template = """

    Specimen routing Form

    No Form action yet
    """ css = """ :where(.tags-input-forms) { display: grid; gap: 1rem; max-inline-size: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-forms form, .tags-input-forms__transport) { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; } :where(.tags-input-forms h3) { flex-basis: 100%; margin: 0; } """ preview = TagsInputFormsAndReset() preview # noqa: B018 ```` An uncanceled reset reconstructs the server values and initial draft after the native reset action. A canceled reset changes nothing. Controlled axes receive reset requests and remain owner-supplied until accepted. Readonly keeps the editor focusable and submits committed values through repeated hidden controls. A draft that becomes dormant while readonly remains visible but does not block submission and is not submitted. Disabled state submits no entries. Without JavaScript, the native multiple Select is visible. It supports deselecting server values, required validity, repeated submission, external Form ownership, and reset, but it cannot create new free-form values. ## Let Field own shared state Inside `CField`, configure `required`, `disabled`, `readonly`, and `invalid` on the Field. The TagsInput registers its editor as the one visible control while the native Select retains Form validity. ### Field-owned TagsInput states [Open the rendered preview](/ui-library/components/tags-input/_previews/field-states/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputFieldStates(Component): template = """
    Publication topics Field owns required and readonly state for the TagsInput. Review labels Resolve the review label before publishing.
    Enabled ancestry
    Moved labels
    Disabled ancestry
    """ css = """ :where(.tags-input-fields) { display: grid; gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-fields__controls, .tags-input-fields__fieldsets) { display: flex; flex-wrap: wrap; gap: 0.75rem; } :where(.tags-input-fields fieldset) { flex: 1 1 16rem; min-inline-size: 0; } """ preview = TagsInputFieldStates() preview # noqa: B018 ```` The visible editor mirrors effective requiredness with `aria-required`. Native invalid focus moves to the editor when possible, then to a safe Dialog or document fallback if the editor is unavailable. ## Navigate tags without leaving the editor The editor is the sole sequential Tab stop. At the start of an empty draft, Backspace first highlights the last tag and a second Backspace removes it. Logical arrow movement visits tags while DOM focus remains in the editor. Delete removes the highlighted tag, Home and End jump to an edge, and Escape returns to ordinary editing. ### Keyboard, focus, and removal [Open the rendered preview](/ui-library/components/tags-input/_previews/keyboard-and-focus/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputKeyboardAndFocus(Component): template = """

    Left-to-right navigation

    At an empty start position, Backspace selects the last tag. Press it again to remove. Arrow keys, Home, End, Delete, and Escape operate while focus stays in the editor.

    Right-to-left navigation

    Physical arrows follow the visual row while value order stays stable.

    Focus an editor to begin
    """ css = """ :where(.tags-input-keyboard) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-keyboard article) { display: grid; gap: 0.75rem; align-content: start; padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 0.75rem; } :where(.tags-input-keyboard h3, .tags-input-keyboard p) { margin: 0; } :where(.tags-input-keyboard output) { grid-column: 1 / -1; } """ preview = TagsInputKeyboardAndFocus() preview # noqa: B018 ```` Remove controls are native Buttons named from the tag value. A persistent polite status announces accepted additions and removals, highlighted tags, and rejected transactions. TagsInput does not use listbox, grid, combobox, or toolbar roles. ## Choose a variant and size `outline`, `filled`, and `plain` variants combine with `sm`, `md`, and `lg` sizes. Long values wrap inside the control. `max_tags` blocks only later additions when the current collection is already at or above the maximum. The server input accepts a positive integer or ASCII decimal string from a component tag, dynamic expression, or Python composition and normalizes it to an integer. ### Variants, sizes, and boundary states [Open the rendered preview](/ui-library/components/tags-input/_previews/variants-and-sizes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputVariantsAndSizes(Component): def template_data(self, kwargs, slots) -> dict[str, object]: # noqa: ANN001, ARG002 return { "variants": ("outline", "filled", "plain"), "sizes": ("sm", "md", "lg"), } template = """
    {{ variant }} / {{ size }}

    Empty and required

    At maximum

    Dark and narrow

    """ css = """ :where(.tags-input-variants) { display: grid; gap: 1.25rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-variants__grid) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 0.75rem; } :where(.tags-input-variants article) { display: grid; gap: 0.5rem; min-inline-size: 0; } :where(.tags-input-variants__boundaries) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 0.75rem; } :where(.tags-input-variants__boundaries article) { inline-size: min(100%, 20rem); padding: 0.85rem; border-radius: 0.75rem; background: Canvas; color: CanvasText; } :where(.tags-input-variants h3) { margin: 0; } """ preview = TagsInputVariantsAndSizes() preview # noqa: B018 ```` ## Customize stable parts and variables Public `--cui-tags-input-*` variables tune color, spacing, sizing, and tag presentation. Stable part selectors target the root, control, tag list, tags, labels, remove Buttons, editor, and status node. ### Brand and environment customization [Open the rendered preview](/ui-library/components/tags-input/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputCustomization(Component): template = """

    Orchard field notes

    Harbor field notes

    """ css = """ :where(.tags-input-customization) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-brand) { display: grid; gap: 0.75rem; align-content: start; min-block-size: 12rem; padding: 1rem; border-radius: 1rem; } :where(.tags-input-brand h3) { margin: 0; } :where(.tags-input-brand--orchard) { background: #f5f0df; color: #203422; --cui-tags-input-background: #fffdf5; --cui-tags-input-border-color: #78916d; --cui-tags-input-focus-color: #315f37; --cui-tags-input-tag-background: #d9e9cf; --cui-tags-input-tag-border-color: #78916d; } :where(.tags-input-brand--harbor) { background: #102b38; color: #eefaff; --cui-tags-input-background: #173c4c; --cui-tags-input-foreground: #eefaff; --cui-tags-input-border-color: #72b5ce; --cui-tags-input-focus-color: #c6ecff; --cui-tags-input-tag-background: #29586b; --cui-tags-input-tag-foreground: #eefaff; } .tags-input-brand .brand-tags [data-citry-ui-part="remove"] { border-radius: 999px; outline-offset: 2px; } @media (forced-colors: active) { :where(.tags-input-brand) { border: 1px solid CanvasText; } } @media print { :where(.tags-input-brand) { min-block-size: auto; background: transparent; color: black; } } """ preview = TagsInputCustomization() preview # noqa: B018 ```` Unlayered application rules override the Citry UI theme layer whether loaded before or after the component stylesheet. A named application layer must be ordered after `citry-ui.theme`. ## Preserve state through server updates Correlated server morphs preserve uncontrolled committed tags, draft, selection, focus, and highlighted-tag identity when their server baselines are unchanged. A changed baseline replaces only the matching uncontrolled axis. ### Morph preservation and cleanup [Open the rendered preview](/ui-library/components/tags-input/_previews/morph-and-cleanup/) ````citry from __future__ import annotations import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagsInputMorphAndCleanup(Component): class Kwargs: step: int = 0 class Slots: pass class Events: def refresh(self) -> TagsInputMorphAndCleanup: return TagsInputMorphAndCleanup() def advance(self) -> TagsInputMorphAndCleanup: return TagsInputMorphAndCleanup(step=2) def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, object]: # noqa: ARG002 baseline = ("server-one", "server-two") if kwargs.step >= 2: baseline = ("new-server-baseline",) return {"baseline": baseline, "step": kwargs.step} template = """

    Server step: {{ step }}

    Unchanged server baselines preserve uncontrolled tags, draft, selection, and focus. Step two supplies a new baseline. An active composition keeps the exact editor node through either morph.

    """ css = """ :where(.tags-input-morph) { display: grid; gap: 1rem; max-inline-size: 40rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.tags-input-morph__controls) { display: flex; flex-wrap: wrap; gap: 0.75rem; } :where(.tags-input-morph p) { margin: 0; } """ preview = TagsInputMorphAndCleanup() preview # noqa: B018 ```` An active composition keeps the exact editor DOM node. Removing the component cancels pending reset, focus, status, and controlled-acceptance work. ## Distinguish callbacks from native events Use these semantic component callbacks through `$c-props`: - `onValueChange` for a valid add, removal, or controlled reset request; - `onInputValueChange` for draft edits and accepted draft transitions; and - `onValueInvalid` for a rejected empty, duplicate, maximum, delimiter, or invalid-value transaction. Native editor events remain ordinary Alpine listeners such as `@input`, `@paste`, `@focus`, and `@blur` in `input_attrs`. Native bubbling `input` and `change` events on the Select proxy report accepted uncontrolled value changes. Controlled value requests dispatch no native proxy change event. TagsInput dispatches no custom DOM event and exposes no public method. Use an ordinary ref when application code needs to focus or inspect the editor. ## Treat attributes and values as data `attrs` targets the root and `input_attrs` targets the editor. They accept ordinary nonconflicting attributes, styling, permitted accessibility hints, and Alpine `@event` or `x-on:event` observers. The component rejects values that can replace its identity, native Form ownership, state, Field relationships, structure, or Alpine lifecycle. Tag values, drafts, placeholders, and message substitutions are assigned as text or native values. They are never evaluated as HTML, URLs, selectors, or Alpine expressions. ## API reference ### Inputs #### CTagsInput server inputs Server inputs are passed in a template through `` or in Python through `CTagsInput(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `name` | `str | None` | `None` | Sets the repeated native Form field name; omission makes values nonparticipating. | | `form` | `str | None` | `None` | Associates the native proxy and readonly transports with a Form ID. | | `id` | `str | None` | generated | Sets the public control ID exchanged between the native fallback and initialized editor. | | `value` | `Sequence[str]` | () | Sets the initial ordered canonical unique tags and repeated Form values. | | `input_value` | `str` | `""` | Sets the initial raw unfinished editor draft. | | `required` | `bool | None` | `None` | Enables native required validity outside Field. | | `disabled` | `bool | None` | `None` | Disables interaction and removes all successful controls outside Field. | | `readonly` | `bool | None` | `None` | Blocks editing while repeated hidden controls preserve submission outside Field. | | `invalid` | `bool | None` | `None` | Adds owner-supplied invalid presentation outside Field. | | `placeholder` | `str | None` | `None` | Sets editor placeholder text. | | `delimiters` | `Sequence[str]` | (",",) | Sets unique server-only single-code-point token separators. | | `max_tags` | `positive int | ASCII decimal str | None` | `None` | Limits later additions to a positive maximum without removing existing tags. | | `autocomplete` | `str | None` | `None` | Sets the editor autocomplete hint. | | `inputmode` | `str | None` | `None` | Sets the editor virtual-keyboard hint. | | `variant` | `"outline" | "filled" | "plain"` ([`CTagsInputVariant`](#tags-input-interface-variant)) | `"outline"` | Selects the control treatment. | | `size` | `"sm" | "md" | "lg"` ([`CTagsInputSize`](#tags-input-interface-size)) | `"md"` | Selects editor, tag, and control geometry. | | `messages` | `CTagsInputMessages | None` ([`CTagsInputMessages`](#tags-input-interface-ctags-input-messages)) | `None` | Overrides catalog-backed removal, status, rejection, and unfinished-draft text per field. | | `class_` | `CClassValue | None` ([`CClassValue`](#tags-input-interface-class-value)) | `None` | Adds root classes and merges them with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#tags-input-interface-style-value)) | `None` | Adds root inline styles and merges them with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed attributes to the root. | | `input_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed naming, descriptive, hint, style, and native-listener attributes to the editor. |
    #### CTagsInput client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string[] | null` | Releases to the latest committed uncontrolled baseline; null has the same effect. | Controls ordered committed tags while supplied as a valid array. | | `inputValue` | `string | null` | Releases to the latest committed uncontrolled draft baseline; null has the same effect. | Controls the raw editor draft while supplied as a valid string. | | `placeholder` | `string | null` | Uses the server value. | Controls editor placeholder text; null releases and an empty string removes the attribute. | | `autocomplete` | `string | null` | Uses the server value. | Controls the editor autocomplete hint; null releases and an empty string removes the attribute. | | `inputmode` | `string | null` | Uses the server value. | Controls the editor inputmode hint; null releases and an empty string removes the attribute. | | `required` | `boolean` | Uses the server or Field fallback. | Controls native required validity and the editor accessibility mirror outside Field. | | `disabled` | `boolean` | Uses the server or Field fallback. | Controls interaction and Form participation outside Field. | | `readonly` | `boolean` | Uses the server or Field fallback. | Controls read-only interaction and repeated hidden transport outside Field. | | `invalid` | `boolean` | Uses the server or Field fallback. | Controls owner-supplied invalid presentation outside Field. | | `maxTags` | `positive integer | null` | Uses the server value. | Controls the addition limit; null removes the maximum. | | `variant` | `"outline" | "filled" | "plain"` ([`CTagsInputVariant`](#tags-input-interface-variant)) | Uses the server value. | Controls presentation treatment. | | `size` | `"sm" | "md" | "lg"` ([`CTagsInputSize`](#tags-input-interface-size)) | Uses the server value. | Controls editor, tag, and control geometry. | | `onValueChange` | `function` | Omission or null selects no value callback. | Receives valid add, removal, and controlled reset requests. | | `onInputValueChange` | `function` | Omission or null selects no draft callback. | Receives direct draft edits and acceptance-gated draft transitions. | | `onValueInvalid` | `function` | Omission or null selects no rejection callback. | Receives one structured notice for each rejected user transaction. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CTagsInput events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(nextValue: string[], detail: CTagsInputValueChangeDetail) => void` ([`CTagsInputValueChangeDetail`](#tags-input-interface-ctags-input-value-change-detail)) | A valid enabled add or removal is requested, or a controlled value axis receives an uncanceled reset request. | `{source, added, removed, candidates, previousValue, nextInputValue, controlled}` ([`CTagsInputValueChangeDetail`](#tags-input-interface-ctags-input-value-change-detail)) | Runs after full batch validation. Uncontrolled values commit first; controlled values remain unchanged until an exact later acceptance edge. | | `onInputValueChange` | `(nextDraft: string, detail: CTagsInputInputValueChangeDetail) => void` ([`CTagsInputInputValueChangeDetail`](#tags-input-interface-ctags-input-input-value-change-detail)) | A direct editor input or accepted commit changes the draft, or a controlled draft receives an uncanceled reset request. | `{source, previousValue, nextValue, controlled, composing}` ([`CTagsInputInputValueChangeDetail`](#tags-input-interface-ctags-input-input-value-change-detail)) | Direct input is synchronous. Commit-related clear or trailing draft waits for the related value acceptance and matching draft generation. | | `onValueInvalid` | `(reason: CTagsInputInvalidReason, detail: CTagsInputInvalidDetail) => void` ([`CTagsInputInvalidReason`](#tags-input-interface-invalid-reason), [`CTagsInputInvalidDetail`](#tags-input-interface-ctags-input-invalid-detail)) | An enabled editable Enter, delimiter, or paste transaction fails an empty, duplicate, maximum, delimiter, or invalid-value guard. | `{source, candidate, candidates, value, inputValue, maxTags, controlled}` ([`CTagsInputInvalidDetail`](#tags-input-interface-ctags-input-invalid-detail)) | Fires once for the atomic transaction without changing tags, proxy values, draft, or selection. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTagsInput CSS variables Apply these variables to `CTagsInput` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-tags-input-background` | `color` | Control background. | `Canvas` | | `--cui-tags-input-foreground` | `color` | Editor and tag text. | `CanvasText` | | `--cui-tags-input-border-color` | `color` | Resting control border. | `color-mix(in srgb, CanvasText 28%, transparent)` | | `--cui-tags-input-hover-border-color` | `color` | Enabled hover border. | `color-mix(in srgb, CanvasText 55%, transparent)` | | `--cui-tags-input-focus-color` | `color` | Focus-visible outline. | `Highlight` | | `--cui-tags-input-invalid-border-color` | `color` | Revealed or owner-supplied invalid border. | `light-dark(#b42318, #fda29b)` | | `--cui-tags-input-disabled-background` | `color` | Disabled control background. | `color-mix(in srgb, CanvasText 6%, Canvas)` | | `--cui-tags-input-tag-background` | `color` | Tag background. | `color-mix(in srgb, CanvasText 8%, Canvas)` | | `--cui-tags-input-tag-foreground` | `color` | Tag text and removal foreground. | `CanvasText` | | `--cui-tags-input-tag-border-color` | `color` | Tag boundary. | `color-mix(in srgb, CanvasText 18%, transparent)` | | `--cui-tags-input-tag-highlighted-background` | `color` | Keyboard-active tag background. | `light-dark(#dbeafe, #19376d)` | | `--cui-tags-input-tag-highlighted-border-color` | `color` | Keyboard-active tag border. | `Highlight` | | `--cui-tags-input-radius` | `length` | Control and tag rounding. | `0.5rem` | | `--cui-tags-input-min-height` | `length` | Minimum control height. | `2.5rem` | | `--cui-tags-input-padding` | `length` | Control internal inset. | `0.375rem 0.5rem` | | `--cui-tags-input-gap` | `length` | Space between tags and editor. | `0.375rem` | | `--cui-tags-input-tag-gap` | `length` | Space between each tag label and remove Button. | `0.25rem` | | `--cui-tags-input-font-size` | `length` | Editor and tag text size. | `1rem` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTagsInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-empty` | Root div | `present | absent` | Present when no effective tags exist. | | `data-required` | Root div | `present | absent` | Mirrors effective requiredness. | | `data-disabled` | Root div | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Root div | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Root div | `present | absent` | Mirrors owner invalidity or a revealed native-invalid episode. | | `data-focused` | Root div | `present | absent` | Present while the editor has focus-visible context. | | `data-at-max` | Root div | `present | absent` | Present when the effective count is at or above maxTags. | | `data-variant` | Root div | `"outline" | "filled" | "plain"` ([`CTagsInputVariant`](#tags-input-interface-variant)) | Mirrors effective treatment. | | `data-size` | Root div | `"sm" | "md" | "lg"` ([`CTagsInputSize`](#tags-input-interface-size)) | Mirrors effective geometry. |
    #### CTagsInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `id` | Native multiple Select | `public ID or derived native ID` | Owns the public ID in fallback mode and the derived ID after initialization. | | `multiple` | Native multiple Select | `present` | Produces one repeated Form entry per selected Option. | | `name` | Native multiple Select | `string | absent` | Supplies the repeated Form field name while editable. | | `form` | Native multiple Select | `Form ID | absent` | Associates an external Form owner. | | `required` | Native multiple Select | `present | absent` | Owns native required validity. | | `disabled` | Native multiple Select | `present | absent` | Bars validation and submission for readonly or disabled transport modes. | | `aria-hidden` | Native multiple Select | `"true" | absent` | Hides the proxy from accessibility APIs only after successful initialization. | | `tabindex` | Native multiple Select | `"-1" | absent` | Removes the initialized proxy from sequential focus. | | `aria-invalid` | Native multiple Select | `"true" | absent` | Mirrors effective visible invalidity. |
    #### CTagsInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `id` | Editor input | `public ID or derived editor ID` | Owns the public ID after initialization and the derived ID in fallback mode. | | `type` | Editor input | `"text"` | Provides ordinary text editing and IME behavior. | | `readonly` | Editor input | `present | absent` | Blocks edits while retaining focusability. | | `disabled` | Editor input | `present | absent` | Removes editor interaction and focus. | | `aria-label` | Editor and proxy | `non-whitespace string | absent` | Supplies the required standalone static accessible name. | | `aria-labelledby` | Editor and proxy | `Field label IDREF | absent` | Mirrors Field-owned generated naming. | | `aria-describedby` | Editor and proxy | `description and error IDREFs | absent` | Mirrors Field or allowed standalone descriptions. | | `aria-required` | Editor input | `"true" | absent` | Mirrors native proxy requiredness on the visible control. | | `aria-invalid` | Editor input | `"true" | absent` | Mirrors owner invalidity or a revealed native-invalid episode. |
    #### CTagsInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-highlighted` | Tag span | `present | absent` | Marks the visually active tag while DOM focus remains in the editor. | | `type` | Remove Button | `"button"` | Prevents accidental Form submission. | | `tabindex` | Remove Button | `"-1"` | Keeps the editor as the sole sequential Tab stop. | | `aria-label` | Remove Button | `localized string` | Names removal with the exact tag value. | | `disabled` | Remove Button | `present | absent` | Blocks removal while readonly or disabled. |
    #### CTagsInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Status span | `"status"` | Exposes nonurgent accepted, rejected, and navigation updates. | | `aria-live` | Status span | `"polite"` | Queues updates without interrupting current speech. | | `aria-atomic` | Status span | `"true"` | Announces each complete status sentence. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTagsInput selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="tags-input"]` | Root div | State reflections and class, style, and attrs destination. | | `[data-citry-ui-part="control"]` | Visible control div | Wraps committed tags and the editor. | | `[data-citry-ui-part="tag-list"]` | Tag-list span | Wraps zero or more component-owned tag visuals before the editor. | | `[data-citry-ui-part="tag"]` | Tag span | Displays one effective canonical value and highlighted state. | | `[data-citry-ui-part="tag-label"]` | Tag label span | Displays the exact effective string. | | `[data-citry-ui-part="remove"]` | Native Button | Removes its named tag by pointer, touch, or programmatic activation. | | `[data-citry-ui-part="input"]` | Native text input | Sole custom editor and initialized focus owner. | | `[data-citry-ui-part="status"]` | Visually hidden span | Persistent polite accepted, rejected, and navigation announcements. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CTagsInputVariant` | `Literal["outline", "filled", "plain"]` | | `CTagsInputSize` | `Literal["sm", "md", "lg"]` | | `CTagsInputChangeSource` | `Literal["input", "enter", "delimiter", "paste", "backspace", "delete", "remove", "reset"]` | | `CTagsInputInvalidReason` | `Literal["empty", "duplicate", "maximum", "delimiter", "invalid-value"]` |
    #### `CTagsInputMessages`
    | Field | Type | Default | Meaning | |---|---|---|---| | `remove_label` | `str | None` | None | Overrides the catalog-backed remove label and requires `{value}`. | | `added_message` | `str | None` | None | Overrides the accepted-addition announcement and requires `{value}`. | | `removed_message` | `str | None` | None | Overrides the accepted-removal announcement and requires `{value}`. | | `selected_message` | `str | None` | None | Overrides the active-tag announcement and requires `{value}`. | | `duplicate_message` | `str | None` | None | Overrides duplicate rejection and requires `{value}`. | | `maximum_message` | `str | None` | None | Overrides maximum rejection and requires `{max}`. | | `empty_message` | `str | None` | None | Overrides the empty-candidate announcement. | | `invalid_message` | `str | None` | None | Overrides the noncanonical-candidate announcement. | | `uncommitted_message` | `str | None` | None | Overrides native custom validity for an editable unfinished draft. |
    #### `CTagsInputValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `source` | `CTagsInputChangeSource` | - | Identifies the interaction or reset request. | | `added` | `string[]` | - | Contains accepted or requested additions in order. | | `removed` | `string[]` | - | Contains accepted or requested removals in order. | | `candidates` | `string[]` | - | Contains the complete atomic candidate batch. | | `previousValue` | `string[]` | - | Copies the effective collection before the request. | | `nextInputValue` | `string` | - | Supplies the draft requested only after exact value acceptance. | | `controlled` | `boolean` | - | Reports whether client value owns the collection. |
    #### `CTagsInputInputValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `source` | `CTagsInputChangeSource` | - | Identifies direct input, accepted tokenization, or reset. | | `previousValue` | `string` | - | Copies the effective draft before the request. | | `nextValue` | `string` | - | Copies the requested next draft. | | `controlled` | `boolean` | - | Reports whether client inputValue owns the draft. | | `composing` | `boolean` | - | Reports whether an input callback occurred during active composition. |
    #### `CTagsInputInvalidDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `source` | `CTagsInputChangeSource` | - | Identifies Enter, delimiter, or paste as the rejected source. | | `candidate` | `string | null` | - | Identifies the first offending candidate when one exists. | | `candidates` | `string[]` | - | Copies the complete attempted atomic batch. | | `value` | `string[]` | - | Copies the unchanged effective tags. | | `inputValue` | `string` | - | Copies the unchanged effective draft. | | `maxTags` | `number | null` | - | Reports the effective maximum. | | `controlled` | `boolean` | - | Reports whether client value owns the collection. |
    ### 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. #### CTagsInput translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-tags-input-remove` | Names each tag remove control. | `value: str` | `messages.remove_label` | $c-tr handles initial controls; `i18n.bind()` handles recreated controls, with the fixed server-translated pattern below a server-only provider. | | `citry-ui-tags-input-added` | Announces accepted additions. | `value: str` | `messages.added_message` | One-shot `i18n.tr()` when available; otherwise the fixed server-translated pattern receives the validated value. | | `citry-ui-tags-input-removed` | Announces accepted removals. | `value: str` | `messages.removed_message` | One-shot `i18n.tr()` when available; otherwise the fixed server-translated pattern receives the validated value. | | `citry-ui-tags-input-selected` | Announces keyboard-active tags. | `value: str` | `messages.selected_message` | One-shot `i18n.tr()` when available; otherwise the fixed server-translated pattern receives the validated value. | | `citry-ui-tags-input-duplicate` | Announces duplicate rejection. | `value: str` | `messages.duplicate_message` | One-shot `i18n.tr()` when available; otherwise the fixed server-translated pattern receives the validated value. | | `citry-ui-tags-input-maximum` | Announces the maximum-tag limit. | `max: str` | `messages.maximum_message` | One-shot `i18n.tr()` with locale-formatted `max`; otherwise the fixed server-translated pattern receives the validated source-mode number. | | `citry-ui-tags-input-required` | Announces an empty candidate. | `None` | `messages.empty_message` | One-shot `i18n.tr()` when the interaction occurs. | | `citry-ui-tags-input-invalid` | Announces a noncanonical candidate. | `None` | `messages.invalid_message` | One-shot `i18n.tr()` when the interaction occurs. | | `citry-ui-tags-input-unfinished` | Supplies native validity text for an unfinished draft. | `None` | `messages.uncommitted_message` | One-shot `i18n.tr()` when validity is evaluated. |
    --- # Textarea Source: https://citry.dev/ui-library/components/textarea/ # Textarea Use `CTextarea` for notes, descriptions, reports, and other multiline plain text. It renders one native multiline text control, so editing, selection, validation, submission, reset, spelling, and mobile keyboards keep their browser behavior. ## Textarea at a glance ### Textarea at a glance [Open the rendered preview](/ui-library/components/textarea/_previews/at-a-glance/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaAtAGlance(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "canopy_note": "Beech leaves moving in a light western wind.\nA woodpecker crossed the clearing twice.", } template = """
    Canopy observation Record light, weather, and visible wildlife.
    Nocturnal call Add enough detail to identify the call.
    """ css = """ :where(.forest-glance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 1rem; max-width: 54rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.forest-glance > *) { padding: 1rem; border: 1px solid light-dark(#a9c6ae, #43634a); border-radius: 0.875rem; background: light-dark(#f4faf4, #132319); } :where(.forest-glance__night) { --cui-textarea-background: #17251c; --cui-textarea-border-color: #5f8067; --cui-textarea-focus-color: #91d39d; } """ preview = TextareaAtAGlance() preview # noqa: B018 ```` ## Compose a labelled control Put Textarea inside `CField` when it needs a label, description, or error. Field owns those relationships and the composed required, disabled, read-only, and invalid states. ### Compose labelled and standalone Textareas [Open the rendered preview](/ui-library/components/textarea/_previews/compose-textarea/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ComposeTextarea(Component): template = """
    Trail condition Shared with the next ranger patrol.
    """ css = """ :where(.forest-compose) { display: grid; gap: 1.25rem; max-width: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.forest-compose > div) { display: grid; gap: 0.5rem; } :where(.forest-compose > div > label) { font-weight: 600; } """ preview = ComposeTextarea() preview # noqa: B018 ```` Outside `CField`, provide a native label or an accessible name yourself: ```citry-html ``` `CTextarea` has no slots or child content. Pass initial text with `value`. ## Choose rows and resizing `rows` sets the initial visible line count. The default `resize="vertical"` keeps the control within its container. `horizontal` and `both` deliberately allow the browser resize handle to exceed a narrow container. Server `rows` and `cols` accept positive integers or ASCII decimal strings from component tags, dynamic expressions, and Python composition. Citry normalizes decimal strings to integers before applying the positive range check. ### Choose rows and resize behavior [Open the rendered preview](/ui-library/components/textarea/_previews/rows-and-resize/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class RowsAndResize(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return {"survey_note": "Fern cover: dense\nSeedlings: abundant\nGround moisture: high"} template = """
    Understory survey Horizontal and both-direction resizing may exceed this bounded stage.
    """ css = """ :where(.forest-resize) { max-width: 34rem; overflow: auto; padding: 1rem; border: 1px dashed light-dark(#789f7f, #698d70); border-radius: 0.75rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview_controls = ( { "name": "rows", "label": "Visible rows", "type": "select", "default": "4", "options": (("2", "2"), ("4", "4"), ("7", "7")), }, { "name": "resize", "label": "Resize", "type": "select", "default": "vertical", "options": ( ("none", "None"), ("vertical", "Vertical"), ("horizontal", "Horizontal"), ("both", "Both"), ), }, ) preview = RowsAndResize() preview # noqa: B018 ```` ## Choose a variant `outline`, `filled`, and `plain` change visual emphasis without changing the native editing or form contract. ### Compare Textarea variants [Open the rendered preview](/ui-library/components/textarea/_previews/variants/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaVariants(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return {"variants": ("outline", "filled", "plain")} template = """
    {{ variant.title() }} field note
    """ css = """ :where(.forest-variants) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = TextareaVariants() preview # noqa: B018 ```` ## Choose a size `sm`, `md`, and `lg` adjust padding, font size, and line geometry. They do not change `rows` or truncate text. ### Compare Textarea sizes [Open the rendered preview](/ui-library/components/textarea/_previews/sizes/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaSizes(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return {"sizes": ("sm", "md", "lg")} template = """
    {{ size.upper() }} specimen note
    """ css = """ :where(.forest-sizes) { display: grid; gap: 1rem; max-width: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = TextareaSizes() preview # noqa: B018 ```` ## Use Field and Form states Required, disabled, read-only, and invalid controls retain their native differences. Read-only text remains focusable and submitted. Disabled text is not submitted. ### Compare Textarea states [Open the rendered preview](/ui-library/components/textarea/_previews/field-states/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaFieldStates(Component): template = """
    Required survey Closed plot Archived note Unclear location Name a trail marker or grid reference.
    """ css = """ :where(.forest-states) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = TextareaFieldStates() preview # noqa: B018 ```` ## Validate, submit, and reset Pass common native constraints such as `minlength`, `maxlength`, and `spellcheck` through `attrs`. Native length validity follows the browser's user-edit rules: initial or script-controlled text is not guaranteed to set `tooShort` or `tooLong`, and browsers usually enforce `maxlength` while typing. ### Validate and reset a habitat report [Open the rendered preview](/ui-library/components/textarea/_previews/validation-and-forms/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaValidation(Component): template = """
    Habitat report Use 12 to 180 characters. Add a fuller habitat description.
    Save report Reset
    """ css = """ :where(.forest-report) { display: grid; gap: 1rem; max-width: 40rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.forest-report__actions) { display: flex; flex-wrap: wrap; gap: 0.75rem; } :where(.forest-report output) { white-space: pre-wrap; } """ preview = TextareaValidation() preview # noqa: B018 ```` ## Control the browser value Supply client `value` through `$c-props` to control current text. Mirror the native `input` event to accept edits. Omit the prop to release control without rewriting the current value. ### Control and release a draft [Open the rendered preview](/ui-library/components/textarea/_previews/controlled-values/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledTextarea(Component): template = """
    Patrol draft
    Release Replace draft
    """ css = """ :where(.forest-controlled) { display: grid; gap: 1rem; max-width: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.forest-controlled__actions) { display: flex; flex-wrap: wrap; gap: 0.75rem; } """ preview = ControlledTextarea() preview # noqa: B018 ```` Citry compares before assigning, waits for composition and consumer updates, and preserves the caret when your handler mirrors the native value. Listen to native `@input`, `@change`, focus, invalid, and composition events directly; Textarea adds no competing value-change callback. ## Keep native text and wrapping Server and client values normalize line endings to LF. Leading and blank lines remain text, and strings that look like HTML cannot create elements. `wrap="hard"` requires `cols` and may add line breaks to submitted data; `soft` does not add wrapping breaks. ### Use native multiline text and wrapping [Open the rendered preview](/ui-library/components/textarea/_previews/native-text/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class NativeTextareaText(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "preserved_text": "\nFirst line after a deliberate blank.\n\nThird observation.", "transect_text": ("A long plain-text observation wraps visually without becoming markup: & moss."), } template = """
    Preserved blank lines Hard-wrapped transect log
    """ css = """ :where(.forest-native) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = NativeTextareaText() preview # noqa: B018 ```` ## Write in either direction Use native `dir` and `dirname` attributes for writing direction. Logical padding and width work in LTR and RTL; long content scrolls inside the control. ### Write long LTR and RTL notes [Open the rendered preview](/ui-library/components/textarea/_previews/direction-and-content/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaDirection(Component): class Kwargs: pass class Slots: pass def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "english_note": ( "A very-long-unbroken-specimen-code-FOREST-TRANSECT-NORTH-204 remained readable inside the control." ), "arabic_note": "كانت أوراق البلوط تتحرك مع الريح الخفيفة قرب الجدول.", } template = """
    English trail note
    ملاحظة الغابة
    """ css = """ :where(.forest-direction) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; max-width: 52rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.forest-direction > *) { min-width: 0; } """ preview = TextareaDirection() preview # noqa: B018 ```` ## Customize the theme Override public variables on an ancestor or one Textarea. Use the stable part selector for targeted rules. ### Theme two field journals [Open the rendered preview](/ui-library/components/textarea/_previews/theme-customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TextareaThemes(Component): template = """
    Fern journal
    Charcoal journal
    """ css = """ :where(.forest-themes) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.forest-themes > div) { padding: 1rem; border-radius: 1rem; } :where(.forest-themes__fern) { --cui-textarea-background: #f7fff7; --cui-textarea-foreground: #153d24; --cui-textarea-border-color: #739b7d; --cui-textarea-hover-border-color: #315f3c; --cui-textarea-focus-color: #16713a; --cui-textarea-invalid-border-color: #b42318; --cui-textarea-disabled-background: #e1eee3; --cui-textarea-placeholder-color: #58715e; --cui-textarea-radius: 1rem; --cui-textarea-inline-padding: 1rem; --cui-textarea-block-padding: 0.875rem; --cui-textarea-font-size: 1rem; --cui-textarea-line-height: 1.6; background: #e2f0e4; } :where(.forest-themes__charcoal) { --cui-textarea-background: #162019; --cui-textarea-foreground: #e6f2e9; --cui-textarea-border-color: #66806d; --cui-textarea-hover-border-color: #9abc9f; --cui-textarea-focus-color: #8de49e; --cui-textarea-invalid-border-color: #ff8a80; --cui-textarea-disabled-background: #242d26; --cui-textarea-placeholder-color: #a7b8ab; --cui-textarea-radius: 0.25rem; --cui-textarea-inline-padding: 0.875rem; --cui-textarea-block-padding: 0.75rem; --cui-textarea-font-size: 1.025rem; --cui-textarea-line-height: 1.55; background: #0c120e; } :where(.forest-themes [data-citry-ui-part="textarea"]:focus-visible) { outline-style: double; } """ preview = TextareaThemes() preview # noqa: B018 ```` `class_` and `style` target the native root. Unlayered consumer CSS overrides the low-specificity defaults; named layers follow the site-wide Citry UI layer ordering contract. ## Know the fixed-height boundary Textarea does not auto-grow, count characters, add adornments, or render rich text. Those jobs need measurement, announcement, or editor contracts beyond a native fixed-row control. Manual CSS resize remains observer-free and works without JavaScript. ## Accessibility and trust Keep a visible label even when placeholder text is present. Textarea adds no role, focus proxy, or keyboard handler. `value`, name, ID, placeholder, autocomplete, and inputmode are always rendered as plain text, including trusted-string subclasses. `attrs`, `class_`, and `style` remain trusted code surfaces for native, ARIA, data, and Alpine attributes. ## API reference ### Inputs #### CTextarea server inputs Server inputs are passed in a template through `` or in Python through `CTextarea(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `name` | `non-empty str | None` | `None` | Sets the native submitted name; an unnamed Textarea contributes no `FormData` entry. | | `id` | `str | None` | generated | Uses the Field control ID when composed, otherwise sets or generates native identity. | | `value` | `str | None` | `None` | Sets LF-normalized, escaped native child text as the initial value and reset default. | | `rows` | `positive int | ASCII decimal str` | `4` | Sets the initial visible line count. | | `cols` | `positive int | ASCII decimal str | None` | `None` | Sets the native preferred character width and is required by hard wrapping; CSS still owns rendered inline size. | | `wrap` | `"soft" | "hard"` ([`CTextareaWrap`](#textarea-interface-input-type-aliases-ctextarea-wrap)) | `"soft"` | Selects native submission wrapping; hard requires cols. | | `required` | `bool | None` | `None` | Sets native required state when standalone; omit it inside `CField`, which owns the state. | | `disabled` | `bool | None` | `None` | Sets local disabled state when standalone; disabled `CForm` always wins. | | `readonly` | `bool | None` | Inherits `CForm` when standalone. | Sets read-only state when standalone; omit it inside `CField`. | | `invalid` | `bool | None` | `None` | Sets application invalid state when standalone; omit it inside `CField`. | | `autocomplete` | `str | None` | `None` | Sets the native autofill hint. | | `inputmode` | `str | None` | `None` | Sets the native virtual-keyboard hint. | | `placeholder` | `str | None` | `None` | Sets short hint text; it does not replace a label. | | `variant` | `"outline" | "filled" | "plain"` ([`CTextareaVariant`](#textarea-interface-input-type-aliases-ctextarea-variant)) | `"outline"` | Selects presentation. | | `size` | `"sm" | "md" | "lg"` ([`CTextareaSize`](#textarea-interface-input-type-aliases-ctextarea-size)) | `"md"` | Selects padding, text size, and line geometry. | | `resize` | `"none" | "vertical" | "horizontal" | "both"` ([`CTextareaResize`](#textarea-interface-input-type-aliases-ctextarea-resize)) | `"vertical"` | Selects the native CSS resize policy; horizontal and both may overflow a narrow container. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#textarea-interface-input-type-aliases-class-value)) | `None` | Adds native-root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#textarea-interface-input-type-aliases-style-value)) | `None` | Adds native-root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native constraints, ARIA, data, and trusted Alpine attributes not owned by explicit inputs. |
    #### CTextarea client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string` | Releases control and preserves the current native value. | Controls the LF-normalized native current value while supplied; an invalid value retains the prior valid controlled value. | | `rows` | `positive integer` | Uses the server input. | Controls the native rows property; invalid values use the server fallback. | | `required` | `boolean` | Uses the server value. | Controls required state when standalone; `CField` owns it when composed. | | `disabled` | `boolean` | Uses the server value. | Controls local disabled state when standalone; disabled `CForm` always wins. | | `readonly` | `boolean` | Uses the server or reactive Form value. | Controls read-only state when standalone; `CField` owns it when composed. | | `invalid` | `boolean` | Uses the server value. | Controls application invalid state when standalone; native invalidity still combines with it. | | `variant` | `"outline" | "filled" | "plain"` ([`CTextareaVariant`](#textarea-interface-input-type-aliases-ctextarea-variant)) | Uses the server input. | Controls presentation. | | `size` | `"sm" | "md" | "lg"` ([`CTextareaSize`](#textarea-interface-input-type-aliases-ctextarea-size)) | Uses the server input. | Controls padding and text geometry. | | `resize` | `"none" | "vertical" | "horizontal" | "both"` ([`CTextareaResize`](#textarea-interface-input-type-aliases-ctextarea-resize)) | Uses the server input. | Controls the native CSS resize policy. |
    ### Slots - ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTextarea CSS variables Apply these variables to `CTextarea` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-textarea-background` | `color` | Native root background. | `Canvas, variant adjusted` | | `--cui-textarea-foreground` | `color` | Entered text. | `CanvasText` | | `--cui-textarea-border-color` | `color` | Resting border. | `Subtle CanvasText mix, variant adjusted` | | `--cui-textarea-hover-border-color` | `color` | Hover border. | `Stronger CanvasText mix.` | | `--cui-textarea-focus-color` | `color` | Focus outline and border. | `Highlight` | | `--cui-textarea-invalid-border-color` | `color` | Invalid border. | `Scheme-aware negative color.` | | `--cui-textarea-disabled-background` | `color` | Disabled background. | `Subtle CanvasText/Canvas mix.` | | `--cui-textarea-placeholder-color` | `color` | Placeholder text. | `Muted CanvasText mix.` | | `--cui-textarea-radius` | `length` | Corner radius. | `0.5rem; 0 for plain` | | `--cui-textarea-inline-padding` | `length` | Logical inline padding. | `Size-derived length.` | | `--cui-textarea-block-padding` | `length` | Logical block padding. | `Size-derived length.` | | `--cui-textarea-font-size` | `length` | Editing text size. | `Size-derived length.` | | `--cui-textarea-line-height` | `number | length` | Editing line height and row geometry. | `1.5` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTextarea attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-required` | Native Textarea | `present | absent` | Mirrors effective required state. | | `data-disabled` | Native Textarea | `present | absent` | Mirrors effective disabled state. | | `data-readonly` | Native Textarea | `present | absent` | Mirrors effective read-only state. | | `data-invalid` | Native Textarea | `present | absent` | Mirrors combined application and native invalid state. | | `data-variant` | Native Textarea | `"outline" | "filled" | "plain"` | Mirrors effective presentation variant. | | `data-size` | Native Textarea | `"sm" | "md" | "lg"` | Mirrors effective visual size. | | `data-resize` | Native Textarea | `"none" | "vertical" | "horizontal" | "both"` | Mirrors the effective native CSS resize policy. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTextarea selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="textarea"]` | Native Textarea | Stable root, styling hook, and `attrs` destination. |
    ### 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]` | | `CTextareaVariant` | `Literal["outline", "filled", "plain"]` | | `CTextareaSize` | `Literal["sm", "md", "lg"]` | | `CTextareaResize` | `Literal["none", "vertical", "horizontal", "both"]` | | `CTextareaWrap` | `Literal["soft", "hard"]` |
    ### Translation keys - --- # TimeInput Source: https://citry.dev/ui-library/components/time-input/ # TimeInput Use `CTimeInput` when a browser-native time editor is the shortest path. It preserves platform keyboard, touch picker, validation, reset, and Form behavior while keeping the application value locale-neutral. ## Collect one time Compose the control in `CField` for its visible label, description, error, and shared state. A standalone input needs an accessible name through `attrs` or an external native label. ```citry-html Start time ``` ### Collect one time [Open the rendered preview](/ui-library/components/time-input/_previews/basic/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BasicTimeInput(Component): template = """ Start time Choose when the session starts. """ preview = BasicTimeInput() preview # noqa: B018 ```` Python composition accepts an exact zone-free `datetime.time`. Localized text, offset-aware times, fractional seconds, and noncanonical strings are rejected. ## Constrain a periodic time range `min`, `max`, and positive integer `step` map to native time constraints. A minimum later than the maximum deliberately expresses a wrapped interval such as 23:00 through 02:00. ### Constrain a native time [Open the rendered preview](/ui-library/components/time-input/_previews/constraints/) ````citry import citry_ui from citry import Component, citry # ruff: noqa: E501 - embedded Citry templates remain readable citry.register_library(citry_ui) class TimeInputConstraints(Component): template = """
    """ preview = TimeInputConstraints() preview # noqa: B018 ```` The server must validate submitted values again; the component never silently clamps or rounds. ## Use Forms and client control `name` contributes exactly one canonical value. Disabled inputs are omitted; readonly inputs remain submitted. Client `value` accepts a canonical string or `null`, and omission releases control at the latest accepted value. ### Submit and reset a time [Open the rendered preview](/ui-library/components/time-input/_previews/form/) ````citry import citry_ui from citry import Component, citry # ruff: noqa: E501 - embedded Citry templates remain readable citry.register_library(citry_ui) class TimeInputForm(Component): template = """
    Delivery time Submit Reset Submit to inspect FormData
    """ preview = TimeInputForm() preview # noqa: B018 ```` ## Understand locale behavior The DOM value and FormData stay `HH:MM` or `HH:MM:SS`; the browser chooses the visible segment order, hour cycle, picker, and native validation prose. Use `CTimePicker` when Citry i18n must own the visible choice labels. ## Compare states and styles Outline, filled, and plain variants combine with sm, md, and lg sizes. Public variables style the native control without replacing its semantics. ### Compare TimeInput states [Open the rendered preview](/ui-library/components/time-input/_previews/states/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TimeInputStates(Component): template = """
    """ preview = TimeInputStates() preview # noqa: B018 ```` `CTimeInput` owns no translation keys. Labels and errors belong to the application; the platform owns the native editor and its prose. ## API reference ### Inputs #### CTimeInput server inputs Server inputs are passed in a template through `` or in Python through `CTimeInput(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `CTimeInputValue | None` ([`CTimeInputValue`](#time-input-interface-value)) | `None` | Sets the initial and reset canonical time or empty value. | | `name` | `str | None` | `None` | Sets the native Form field name. | | `form` | `str | None` | `None` | Associates the input with an external native Form ID. | | `id` | `str | None` | generated | Sets the public native input ID. | | `min` | `CTimeInputValue | None` ([`CTimeInputValue`](#time-input-interface-value)) | `None` | Sets the inclusive native minimum and may start a wrapped range. | | `max` | `CTimeInputValue | None` ([`CTimeInputValue`](#time-input-interface-value)) | `None` | Sets the inclusive native maximum and may end a wrapped range. | | `step` | `int` | `60` | Sets the exact positive native step in seconds. | | `required` | `bool | None` | `None` | Enables native empty-value validity outside Field; Field owns it inside Field. | | `disabled` | `bool | None` | `None` | Blocks interaction and Form participation outside Field; Form disabledness also wins. | | `readonly` | `bool | None` | `None` | Keeps a focusable submitted value while blocking native edits. | | `invalid` | `bool | None` | `None` | Adds application invalid state to revealed native validity. | | `autocomplete` | `str | None` | `None` | Sets a native autofill hint. | | `variant` | `"outline" | "filled" | "plain"` ([`CTimeInputVariant`](#time-input-interface-variant)) | `"outline"` | Selects outer native-control treatment. | | `size` | `"sm" | "md" | "lg"` ([`CTimeInputSize`](#time-input-interface-size)) | `"md"` | Selects coordinated sizing. | | `class_` | `CClassValue | None` ([`CClassValue`](#time-input-interface-class-value)) | `None` | Adds classes to the native root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#time-input-interface-style-value)) | `None` | Adds styles to the native root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed native attributes without replacing owned identity state constraints or runtime markers. |
    #### CTimeInput client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `canonical string | null` | Releases control at the latest accepted value. | Controls the exact native value while supplied. | | `min` | `canonical string | null` | Uses the server minimum. | Replaces or removes the inclusive minimum. | | `max` | `canonical string | null` | Uses the server maximum. | Replaces or removes the inclusive maximum. | | `step` | `positive integer` | Uses the server step. | Replaces the native seconds step. | | `required` | `boolean` | Uses server or Field state. | Controls standalone required validity. | | `disabled` | `boolean` | Uses server or owner state. | Controls interaction and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable state. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `variant` | `"outline" | "filled" | "plain"` ([`CTimeInputVariant`](#time-input-interface-variant)) | Uses the server input. | Controls presentation. | | `size` | `"sm" | "md" | "lg"` ([`CTimeInputSize`](#time-input-interface-size)) | Uses the server input. | Controls coordinated sizing. |
    ### Slots - ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTimeInput CSS variables Apply these variables to `CTimeInput` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-time-input-background` | `color` | Native control background. | `Canvas` | | `--cui-time-input-foreground` | `color` | Native time text and indicator foreground. | `CanvasText` | | `--cui-time-input-border-color` | `color` | Resting border. | `Mixed CanvasText.` | | `--cui-time-input-hover-border-color` | `color` | Hover border. | `Stronger mixed CanvasText.` | | `--cui-time-input-focus-color` | `color` | Focus border and outline. | `Highlight` | | `--cui-time-input-invalid-border-color` | `color` | Invalid border. | `Theme error.` | | `--cui-time-input-disabled-background` | `color` | Disabled background. | `Muted Canvas.` | | `--cui-time-input-radius` | `length` | Outer corner radius. | `0.5rem` | | `--cui-time-input-height` | `length` | Minimum block size. | `2.5rem` | | `--cui-time-input-inline-padding` | `length` | Logical inline inset. | `0.75rem` | | `--cui-time-input-block-padding` | `length` | Logical block inset. | `0.5rem` | | `--cui-time-input-font-size` | `length` | Time text size. | `1rem` | | `--cui-time-input-min-inline-size` | `length` | Preferred minimum width before container clamping. | `10rem` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTimeInput attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `type` | Native root input | `"time"` | Selects browser-owned wall-clock editing and picker behavior. | | `value` | Native root input | `canonical time | absent` | Carries the initial and reset time. | | `min` | Native root input | `canonical time | absent` | Sets inclusive native minimum validity. | | `max` | Native root input | `canonical time | absent` | Sets inclusive native maximum validity. | | `step` | Native root input | `positive integer` | Sets the seconds step grid. | | `aria-invalid` | Native root input | `"true" | absent` | Mirrors application or revealed native invalidity. | | `data-empty` | Native root input | `present | absent` | Mirrors an empty canonical value. | | `data-required` | Native root input | `present | absent` | Mirrors effective requiredness. | | `data-disabled` | Native root input | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Native root input | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Native root input | `present | absent` | Mirrors application or revealed native invalidity. | | `data-variant` | Native root input | `CTimeInputVariant` ([`CTimeInputVariant`](#time-input-interface-variant)) | Mirrors visual treatment. | | `data-size` | Native root input | `CTimeInputSize` ([`CTimeInputSize`](#time-input-interface-size)) | Mirrors coordinated sizing. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTimeInput selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="time-input"]` | Native root input | Stable styling state Form focus event and attrs destination. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CTimeInputValue` | `time | str` | | `CTimeInputVariant` | `Literal["outline", "filled", "plain"]` | | `CTimeInputSize` | `Literal["sm", "md", "lg"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    ### Translation keys - --- # TimePicker Source: https://citry.dev/ui-library/components/time-picker/ # TimePicker Use `CTimePicker` when people choose from a bounded schedule and the active Citry locale should format every visible time. It submits the same canonical `HH:MM` or `HH:MM:SS` string as a native time input. ## Pick from regular intervals The default fifteen-minute step produces a finite day list. Bounds limit the choices; a later minimum than maximum creates a wrapped overnight interval. ```citry-html Appointment time ``` ### Pick an appointment time [Open the rendered preview](/ui-library/components/time-picker/_previews/basic/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BasicTimePicker(Component): template = """ Appointment time Choose a fifteen-minute slot. """ preview = BasicTimePicker() preview # noqa: B018 ```` ## Supply exact choices Use `options` for irregular schedules or second precision. Options are checked, bounded, unique, and preserve server order. Structural option changes require a server rerender. ### Supply exact time choices [Open the rendered preview](/ui-library/components/time-picker/_previews/options/) ````citry from datetime import time from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTimePicker citry.register_library(citry_ui) class TimePickerOptions(Component): def template_data(self, kwargs, slots) -> dict[str, Any]: # noqa: ANN001, ARG002 return { "picker": CTimePicker( name="departure", value=time(23, 5, 9), options=(time(23, 5, 9), "00:00:10", "12:30:45") ) } template = """

    Irregular second-precision departures

    {{ picker }}
    """ preview = TimePickerOptions() preview # noqa: B018 ```` ## Submit and reset The hidden native time control remains the single Form transport and the no-JavaScript control. `CField` and `CForm` own shared state; the nested Listbox never becomes another form field. ### Submit and reset a time picker [Open the rendered preview](/ui-library/components/time-picker/_previews/form/) ````citry import citry_ui from citry import Component, citry # ruff: noqa: E501 - embedded Citry templates remain readable citry.register_library(citry_ui) class TimePickerForm(Component): template = """
    Start time Submit Reset Submit to inspect FormData
    """ preview = TimePickerForm() preview # noqa: B018 ```` ## Control value and visibility Client `value` and `open` are independently controlled while supplied. `onValueChange` and `onOpenChange` report requests; omission releases each channel at its latest committed value. ### Control time and popup state [Open the rendered preview](/ui-library/components/time-picker/_previews/controlled/) ````citry import citry_ui from citry import Component, citry # ruff: noqa: E501 - embedded Citry templates remain readable citry.register_library(citry_ui) class ControlledTimePicker(Component): template = """
    No request yet
    """ preview = ControlledTimePicker() preview # noqa: B018 ```` ## Switch locales in place The server renders source-locale text first. Under a client-enabled `c-i18n` provider, the trigger, popup name, clear label, validity message, and generated option labels update when the locale changes. Canonical Form values do not. ### Format time choices by locale [Open the rendered preview](/ui-library/components/time-picker/_previews/locales/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class LocalizedTimePicker(Component): template = """
    English Čeština
    """ preview = LocalizedTimePicker() preview # noqa: B018 ```` Literal `placeholder`, `picker_label`, `change_label`, `clear_label`, and `unavailable_message` overrides remain exactly application-owned and do not register catalog bindings. ## Compare states and styles Outline, filled, and plain variants combine with sm, md, and lg sizes. The picker inherits Popover collision handling, Listbox keyboard behavior, forced colors, logical direction, and reduced-motion handling. ### Compare TimePicker states [Open the rendered preview](/ui-library/components/time-picker/_previews/states/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TimePickerStates(Component): template = """
    """ preview = TimePickerStates() preview # noqa: B018 ```` ## API reference ### Inputs #### CTimePicker server inputs Server inputs are passed in a template through `` or in Python through `CTimePicker(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `CTimePickerTime | None` ([`CTimePickerTime`](#time-picker-interface-time)) | `None` | Sets the initial and reset canonical time or empty value and must be an available option. | | `name` | `str | None` | `None` | Sets the native transport Form field name. | | `form` | `str | None` | `None` | Associates the native transport with an external Form ID. | | `id` | `str | None` | generated | Sets the public no-JavaScript input or enhanced Button ID and owned ID prefix. | | `min` | `CTimePickerTime | None` ([`CTimePickerTime`](#time-picker-interface-time)) | `None` | Sets the inclusive first selectable time and may begin a wrapped interval. | | `max` | `CTimePickerTime | None` ([`CTimePickerTime`](#time-picker-interface-time)) | `None` | Sets the inclusive last selectable time and may end a wrapped interval. | | `step` | `int` | `900` | Generates choices at an exact interval of at least 300 seconds when options is absent. | | `options` | `Sequence[CTimePickerTime] | None` | `None` | Supplies one through 288 unique exact choices in server order instead of generated intervals. | | `required` | `bool | None` | `None` | Enables native empty-value validity outside Field; Field owns it inside Field. | | `disabled` | `bool | None` | `None` | Blocks opening selection clearing and Form participation outside Field; Form disabledness also wins. | | `readonly` | `bool | None` | `None` | Keeps the picker focusable and submitted but blocks value changes. | | `invalid` | `bool | None` | `None` | Adds application invalid state to revealed native validity. | | `clearable` | `bool` | `True` | Shows a clear action for an optional non-empty writable value. | | `dismissible` | `bool` | `True` | Permits Escape outside and focus-outside close requests. | | `placement` | `CPopoverPlacement` ([`CPopoverPlacement`](#time-picker-interface-popover-placement)) | `"bottom-start"` | Sets the preferred logical Popover placement. | | `match_width` | `bool` | `True` | Makes the Popover at least as wide as the visible control. | | `placeholder` | `str` | `"Choose a time"` | Supplies visible empty-state text when explicitly overridden. | | `picker_label` | `str` | `"Choose time"` | Names the popup Listbox and empty trigger when explicitly overridden. | | `change_label` | `str` | `"Change time, {time}"` | Formats a selected trigger name and must retain the time placeholder when explicitly overridden. | | `clear_label` | `str` | `"Clear time"` | Names the clear Button when explicitly overridden. | | `unavailable_message` | `str` | `"Choose an available time."` | Supplies native custom validity if a selected value is unavailable. | | `variant` | `CTimePickerVariant` ([`CTimePickerVariant`](#time-picker-interface-variant)) | `"outline"` | Selects outline filled or plain field treatment. | | `size` | `CTimePickerSize` ([`CTimePickerSize`](#time-picker-interface-size)) | `"md"` | Selects coordinated control and text sizing. | | `class_` | `CClassValue | None` ([`CClassValue`](#time-picker-interface-class-value)) | `None` | Adds classes to the root and merges with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#time-picker-interface-style-value)) | `None` | Adds styles to the root and merges with attrs. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing owned state identity or runtime markers. |
    #### CTimePicker client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `canonical string | null` | Releases control at the latest committed value. | Controls selected and submitted time while supplied. | | `open` | `boolean | null` | Releases control at the latest committed visibility. | Controls popup visibility while supplied. | | `required` | `boolean` | Uses server or Field state. | Controls standalone required validity. | | `disabled` | `boolean` | Uses server or owner state. | Controls interaction and Form participation. | | `readonly` | `boolean` | Uses server or owner state. | Controls focusable nonmutable state. | | `invalid` | `boolean` | Uses server or Field state. | Controls application invalid state. | | `clearable` | `boolean` | Uses the server input. | Controls the optional clear action. | | `dismissible` | `boolean` | Uses the server input. | Controls passive popup dismissal. | | `placement` | `CPopoverPlacement` ([`CPopoverPlacement`](#time-picker-interface-popover-placement)) | Uses the server input. | Controls preferred logical placement. | | `matchWidth` | `boolean` | Uses the server input. | Controls trigger-width matching. | | `variant` | `CTimePickerVariant` ([`CTimePickerVariant`](#time-picker-interface-variant)) | Uses the server input. | Controls field presentation. | | `size` | `CTimePickerSize` ([`CTimePickerSize`](#time-picker-interface-size)) | Uses the server input. | Controls coordinated sizing. | | `onValueChange` | `function` | No semantic value callback. | Receives option clear native and reset value requests. | | `onOpenChange` | `function` | No semantic visibility callback. | Receives trigger selection dismissal reset and forced-close requests. |
    ### Slots - ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CTimePicker events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string | null, detail: CTimePickerValueChangeDetail) => void` ([`CTimePickerValueChangeDetail`](#time-picker-interface-ctime-picker-value-change-detail)) | Listbox selection clear reset or native fallback editing requests another value. | `{value, previousValue, controlled, source, sourceEvent}` ([`CTimePickerValueChangeDetail`](#time-picker-interface-ctime-picker-value-change-detail)) | Uncontrolled user commits emit native input/change; controlled requests wait for the owner. | | `onOpenChange` | `(open: boolean, detail: CTimePickerOpenChangeDetail) => void` ([`CTimePickerOpenChangeDetail`](#time-picker-interface-ctime-picker-open-change-detail)) | Trigger selection clear reset Escape outside focus-outside native or forced layer changes request visibility. | `{reason, controlled, forced, source}` ([`CTimePickerOpenChangeDetail`](#time-picker-interface-ctime-picker-open-change-detail)) | Uncontrolled requests commit before notification; controlled requests wait except forced safety closure. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTimePicker CSS variables Apply these variables to `CTimePicker` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-time-picker-background` | `color` | Visible control and clear background. | `Canvas or variant-derived.` | | `--cui-time-picker-foreground` | `color` | Text and icon color. | `CanvasText` | | `--cui-time-picker-border-color` | `color` | Visible control and clear boundary. | `Mixed CanvasText.` | | `--cui-time-picker-invalid-border-color` | `color` | Revealed invalid control boundary. | `Theme error.` | | `--cui-time-picker-focus-color` | `color` | Control and clear focus outline. | `Highlight` | | `--cui-time-picker-radius` | `length` | Visible control and clear corner radius. | `0.625rem` | | `--cui-time-picker-min-block-size` | `length` | Minimum interactive control height. | `2.5rem` | | `--cui-time-picker-padding-inline` | `length` | Visible control inline inset. | `0.75rem` | | `--cui-time-picker-gap` | `length` | Visible value and icon gap. | `0.5rem` | | `--cui-time-picker-list-max-block-size` | `length` | Maximum scrollable option-list height. | `18rem` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTimePicker attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-empty` | Root | `present | absent` | Marks no committed canonical value. | | `data-open` | Root | `present | absent` | Mirrors effective popup visibility. | | `data-required` | Root | `present | absent` | Mirrors effective requiredness. | | `data-disabled` | Root | `present | absent` | Mirrors effective disabledness. | | `data-readonly` | Root | `present | absent` | Mirrors effective readonly state. | | `data-invalid` | Root | `present | absent` | Mirrors application unavailable or revealed native invalidity. | | `data-variant` | Root | `CTimePickerVariant` ([`CTimePickerVariant`](#time-picker-interface-variant)) | Mirrors visual treatment. | | `data-size` | Root | `CTimePickerSize` ([`CTimePickerSize`](#time-picker-interface-size)) | Mirrors coordinated sizing. | | `data-enhanced` | Root | `present | absent` | Marks completed custom control activation. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTimePicker selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="time-picker"]` | Root div | State reflection attrs and styling destination. | | `[data-citry-ui-part="fallback-input"]` | Native Time input | No-JavaScript control and enhanced Form reset validity transport. | | `[data-citry-ui-part="enhanced-control"]` | Layout div | Groups the Popover activator and optional clear action. | | `[data-citry-ui-part="control"]` | Native Button | Full-width popup activator and enhanced public focus target. | | `[data-citry-ui-part="value"]` | Span | Displays localized selected time or placeholder. | | `[data-citry-ui-part="clear"]` | Native Button | Requests an empty optional value. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CTimePickerTime` | `time | str` | | `CTimePickerVariant` | `Literal["outline", "filled", "plain"]` | | `CTimePickerSize` | `Literal["sm", "md", "lg"]` | | `CTimePickerValueChangeSource` | `Literal["option", "clear", "reset", "native"]` | | `CPopoverPlacement` | `Literal["top-start", "top", "top-end", "bottom-start", "bottom", "bottom-end"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CTimePickerValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `string | null` | - | Requested selected canonical time or empty state. | | `previousValue` | `string | null` | - | Effective time before the request. | | `controlled` | `boolean` | - | Whether client value owns the commit. | | `source` | `CTimePickerValueChangeSource` ([`CTimePickerValueChangeSource`](#time-picker-interface-value-source)) | - | Option clear reset or native cause. | | `sourceEvent` | `object | null` | - | Native interaction event when one exists. |
    #### `CTimePickerOpenChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `reason` | `trigger | selection | clear | reset | escape | outside | focus-outside | native | ancestor | modal` | - | Exact request or forced-close cause. | | `controlled` | `boolean` | - | Whether client open owns ordinary visibility commits. | | `forced` | `boolean` | - | Whether ancestor or modal safety required closure. | | `source` | `object | null` | - | Associated browser source when one exists. |
    ### 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. #### CTimePicker translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-time-picker-placeholder` | Displays the empty control value. | `none` | `placeholder` | Parent i18n subscription calls `tr()` because the same destination later displays formatted times. | | `citry-ui-time-picker-label` | Names the popup Listbox and empty trigger. | `none` | `picker_label` | `$c-tr` updates the stable title; parent subscription updates the dynamic trigger name. | | `citry-ui-time-picker-change` | Names a selected trigger. | `` `time: str` localized by a time display profile `` | `change_label` containing `{time}` | Parent i18n subscription recomputes the formatted value and calls `tr()`. | | `citry-ui-time-picker-clear` | Names the optional clear Button. | `none` | `clear_label` | `$c-tr` updates the stable aria-label destination. | | `citry-ui-time-picker-unavailable` | Supplies native custom validity when the value is unavailable. | `none` | `unavailable_message` | `i18n.bind()` updates the browser-owned validity message. |
    --- # Transfer List Source: https://citry.dev/ui-library/components/transfer-list/ # Transfer List Use `CTransferList` when people need to compare a finite set of available items with an ordered chosen set. `CTransferListItem` declares stable values, plain accessible labels, optional rich presentation, and disabled state. ## Move items between two lists The enhanced component uses two labeled multi-select listboxes and explicit buttons. Without JavaScript, the same values remain available through a native `select[multiple]` form control. ### Choose and order reviewers [Open the rendered preview](/ui-library/components/transfer-list/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TransferListAtAGlance(Component): template = """ """ preview = TransferListAtAGlance() preview # noqa: B018 ```` Selection inside a pane is separate from the chosen form value. Select one or more enabled options, then use Add or Remove. The Add all and Remove all buttons can be omitted with `show_move_all=False`. Chosen items retain the exact order in `value` and in submitted form entries. ## Render rich, noninteractive items The Item default slot can replace its visible label with server-rendered presentation. Keep `label` plain and descriptive because native fallback, typeahead, and assistive naming use it. ### Render rich Transfer List items [Open the rendered preview](/ui-library/components/transfer-list/_previews/rich-items/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TransferListRichItems(Component): template = """ Platform
    Runtime and release infrastructure
    Design systems
    Components, tokens, and accessibility
    Security
    Managed by policy
    """ preview = TransferListRichItems() preview # noqa: B018 ```` Do not place links, buttons, inputs, editable content, or other focus stops inside an Item. The family follows the listbox interaction model and rejects interactive descendants during enhancement. ## Control chosen values from Alpine Pass `value` and `onValueChange` through `$c-props` for controlled state. Transfer and reorder actions become requests: the visible order changes only after the owner accepts the proposed array. ### Control a Transfer List [Open the rendered preview](/ui-library/components/transfer-list/_previews/controlled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TransferListControlled(Component): template = """
    No request
    """ preview = TransferListControlled() preview # noqa: B018 ```` Omit client `value`, or set it to `null`, for uncontrolled behavior. In that mode an accepted action updates the native form owner, emits native `input` then `change`, and calls `onValueChange`. ## Submit and validate forms Set `name` to submit one entry per chosen item in chosen order. `form` can associate the control with a non-ancestor form. `required=True` requires at least one chosen value and moves focus to the chosen list when native validation fails. ### Submit a required ordered selection [Open the rendered preview](/ui-library/components/transfer-list/_previews/forms/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TransferListForm(Component): template = """

    Not submitted
    """ preview = TransferListForm() preview # noqa: B018 ```` Native reset restores the server-rendered value. A disabled Item cannot be moved or reordered. An initially chosen disabled Item remains submitted by the native fallback through an ordered hidden option proxy. ## Keyboard and accessibility Each pane has one tab stop and an active descendant. Arrow keys, Home, End, typeahead, Space, Enter, Shift+Arrow range selection, and Ctrl/Cmd+A are available. Explicit transfer and reorder buttons remain reachable in normal tab order, so drag and drop is never required. ### Use disabled items and accessible labels [Open the rendered preview](/ui-library/components/transfer-list/_previews/accessibility/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TransferListAccessibility(Component): template = """ Audit access
    Required by policy; cannot be removed
    """ preview = TransferListAccessibility() preview # noqa: B018 ```` The family announces accepted moves and reorders through a polite live region. Pane labels, counts, controls, empty states, announcements, and required validation use Citry UI catalog messages by default. Any explicit `*_label` input belongs to the caller and does not switch with the Citry client locale. ## Responsive layout and customization The three-column layout stacks automatically in a narrow container and uses logical CSS properties for RTL. Customize the root and Items with `class_`, `style`, and `attrs`, or use the documented public variables and part selectors. ### Customize Transfer List [Open the rendered preview](/ui-library/components/transfer-list/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TransferListCustomization(Component): template = """ """ css = """ .brand-transfer { --cui-transfer-list-selected: color-mix(in srgb, MediumPurple 25%, Canvas); --cui-transfer-list-focus: MediumPurple; --cui-transfer-list-radius: 1rem; } """ preview = TransferListCustomization() preview # noqa: B018 ```` `size` changes the default list height. Forced colors preserve selected-state outlines, reduced-motion environments disable component motion, and print hides action controls while retaining both supplied panes. ## Scope boundaries This first family owns a complete finite server-rendered collection. It does not fetch, filter, virtualize, group into a tree, expose read-only mode, or provide drag and drop. Use `CMultiSelect` for compact selection and compose application state with `CVirtualWindow` when the collection cannot be fully rendered. ## API reference ### Inputs #### CTransferList server inputs Server inputs are passed in a template through `` or in Python through `CTransferList(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `id` | `str | None` | generated | Sets the root ID and bases stable listbox and option IDs. | | `value` | `Sequence[str]` | `"()"` | Sets the ordered initial chosen values; every unique value must name one Item. | | `name` | `str | None` | `None` | Sets the repeated native form-entry name. | | `form` | `str | None` | `None` | Associates native and enhanced form values with an external Form ID. | | `required` | `bool` | `False` | Requires at least one chosen value. | | `disabled` | `bool` | `False` | Disables list selection controls and form contribution. | | `show_move_all` | `bool` | `True` | Shows Add all and Remove all controls. | | `show_reorder` | `bool` | `True` | Shows chosen-order controls. | | `size` | `CTransferListSize` ([`CTransferListSize`](#transfer-list-interface-size)) | `"md"` | Selects compact default or spacious list height. | | `available_label` | `str` | `"Available items"` | Overrides the localized available-pane title. | | `chosen_label` | `str` | `"Chosen items"` | Overrides the localized chosen-pane title and native fallback label. | | `available_empty_label` | `str` | `"No available items"` | Overrides the localized available empty state. | | `chosen_empty_label` | `str` | `"No chosen items"` | Overrides the localized chosen empty state. | | `count_label` | `str` | `"{selected} of {total} selected"` | Overrides pane counts and must retain both named placeholders. | | `transfer_controls_label` | `str` | `"Transfer controls"` | Overrides the transfer toolbar accessible name. | | `add_label` | `str` | `"Add selected"` | Overrides the Add selected action. | | `add_all_label` | `str` | `"Add all"` | Overrides the Add all action. | | `remove_label` | `str` | `"Remove selected"` | Overrides the Remove selected action. | | `remove_all_label` | `str` | `"Remove all"` | Overrides the Remove all action. | | `reorder_controls_label` | `str` | `"Chosen item order"` | Overrides the reorder toolbar accessible name. | | `move_top_label` | `str` | `"Move to top"` | Overrides the Move to top action. | | `move_up_label` | `str` | `"Move up"` | Overrides the Move up action. | | `move_down_label` | `str` | `"Move down"` | Overrides the Move down action. | | `move_bottom_label` | `str` | `"Move to bottom"` | Overrides the Move to bottom action. | | `added_label` | `str` | `"{count} items added"` | Overrides multi-item Add announcements and must retain count. | | `removed_label` | `str` | `"{count} items removed"` | Overrides multi-item Remove announcements and must retain count. | | `reordered_label` | `str` | `"{count} items reordered"` | Overrides multi-item reorder announcements and must retain count. | | `required_label` | `str` | `"Choose at least one item"` | Overrides the required-validation announcement. | | `class_` | `CClassValue | None` ([`CClassValue`](#transfer-list-interface-class-value)) | `None` | Adds classes to the root. | | `style` | `CStyleValue | None` ([`CStyleValue`](#transfer-list-interface-style-value)) | `None` | Adds root styles before owned theme variables. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing owned form semantics state or runtime markers. |
    #### CTransferList client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string[] | null` | Omission or null releases control to the committed value. | Controls the exact ordered chosen values while supplied. | | `required` | `boolean` | Uses the server value. | Reactively changes required validity. | | `disabled` | `boolean` | Uses the server and Fieldset state. | Reactively disables interaction and form contribution. | | `onValueChange` | `function` | No component callback runs. | Receives transfer reorder and reset requests. |
    #### CTransferListItem server inputs Server inputs are passed in a template through `` or in Python through `CTransferListItem(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | required | Supplies nonempty unique stable identity and submitted value. | | `label` | `str` | required | Supplies native fallback typeahead and accessible text. | | `disabled` | `bool` | `False` | Prevents selection transfer and reorder while preserving an initial chosen value. | | `class_` | `CClassValue | None` ([`CClassValue`](#transfer-list-interface-class-value)) | `None` | Adds classes to the enhanced Option. | | `style` | `CStyleValue | None` ([`CStyleValue`](#transfer-list-interface-style-value)) | `None` | Adds styles to the enhanced Option. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed Option attributes without replacing owned semantics identity or state. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CTransferList slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CTransferListDefaultSlotData`](#transfer-list-interface-ctransfer-list-default-slot-data)) | Empty collection; accepts only CTransferListItem declarations. |
    #### CTransferListItem slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{value, label, disabled, in_target, index}` ([`CTransferListItemDefaultSlotData`](#transfer-list-interface-ctransfer-list-item-default-slot-data)) | Plain label text. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CTransferList events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value: string[], detail: CTransferListChangeDetail) => void` ([`CTransferListChangeDetail`](#transfer-list-interface-ctransfer-list-change-detail)) | A transfer reorder reset or accepted client reconciliation requests another ordered value. | `{value, previousValue, moved, source, controlled, sourceEvent}` ([`CTransferListChangeDetail`](#transfer-list-interface-ctransfer-list-change-detail)) | Uncontrolled state commits and emits native input/change first; controlled state is request-only until accepted. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTransferList CSS variables Apply these variables to `CTransferList` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-transfer-list-pane-size` | `length` | Native fallback and pane inline-size preference. | `15rem` | | `--cui-transfer-list-list-size` | `length` | Enhanced listbox block size. | `sm 11rem; md 15rem; lg 20rem` | | `--cui-transfer-list-gap` | `length` | Pane and control spacing. | `0.75rem` | | `--cui-transfer-list-border` | `complete border value` | Pane control and Button borders. | `Adaptive 1px solid neutral` | | `--cui-transfer-list-radius` | `length` | Pane and native fallback corners. | `0.625rem` | | `--cui-transfer-list-surface` | `color` | Pane native fallback and Button surfaces. | `Canvas` | | `--cui-transfer-list-selected` | `color` | Selected Option background. | `Adaptive blue` | | `--cui-transfer-list-hover` | `color` | Hovered Option background. | `Adaptive neutral` | | `--cui-transfer-list-focus` | `color` | Listbox and Button focus outline. | `Highlight` | | `--cui-transfer-list-disabled-opacity` | `number` | Disabled root Option and Button opacity. | `0.55` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTransferList attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Available and chosen listbox divs | `listbox` | Exposes each enhanced pane as a selectable collection. | | `aria-multiselectable` | Both listboxes | `true` | Declares independent multi-selection. | | `aria-activedescendant` | Focused listbox | `IDREF | absent` | Identifies the active Option while DOM focus remains on the listbox. | | `role` | Enhanced Item div | `option` | Exposes one declared choice. | | `aria-selected` | Enhanced Item div | `boolean-string` | Reflects ephemeral pane selection rather than chosen membership. | | `aria-disabled` | Root listboxes and disabled Items | `boolean-string` | Reflects effective unavailability. | | `aria-invalid` | Root | `true | absent` | Marks a failed required validity check. | | `data-value` | Enhanced Item div | `string` | Exposes stable identity. | | `data-selected` | Enhanced Item div | `present | absent` | Reflects ephemeral pane selection. | | `data-disabled` | Root and disabled Items | `present | absent` | Reflects effective unavailability. | | `data-required` | Root | `present | absent` | Reflects required validity. | | `data-invalid` | Root | `present | absent` | Reflects a failed native validity check. | | `data-size` | Root | `CTransferListSize` ([`CTransferListSize`](#transfer-list-interface-size)) | Mirrors list-height profile. | | `data-available-empty` | Root | `present | absent` | Marks no available Items. | | `data-chosen-empty` | Root | `present | absent` | Marks no chosen Items. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTransferList selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="transfer-list"]` | Root div | State reflections attrs and theme destination. | | `[data-citry-ui-part="native"]` | Native select multiple | Progressive fallback validity reset and initial form owner. | | `[data-citry-ui-part="control"]` | Enhanced grid | Contains both panes and transfer controls. | | `[data-citry-ui-part="pane"]` | Available or chosen section | Pane surface. | | `[data-citry-ui-part="pane-header"]` | Pane header | Groups title and selection count. | | `[data-citry-ui-part="pane-title"]` | Pane h3 | Visible listbox label. | | `[data-citry-ui-part="count"]` | Count span | Localized selected and total summary. | | `[data-citry-ui-part="listbox"]` | Pane listbox div | Focus selection and Item-scroll owner. | | `[data-citry-ui-part="option"]` | Item div | Rich presentation selection and stable Item customization. | | `[data-citry-ui-part="empty"]` | Pane paragraph | Localized empty state. | | `[data-citry-ui-part="transfer-controls"]` | Transfer toolbar | Add and Remove actions. | | `[data-citry-ui-part="reorder-controls"]` | Reorder toolbar | Chosen-order actions. | | `[data-citry-ui-part="button"]` | Native action Button | Transfer and reorder actions. | | `[data-citry-ui-part="status"]` | Visually hidden polite live region | Accepted action and validation announcements. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CTransferListSize` | `Literal["sm", "md", "lg"]` | | `CTransferListChangeSource` | `Literal["add", "add-all", "remove", "remove-all", "move-top", "move-up", "move-down", "move-bottom", "reset", "client"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CTransferListDefaultSlotData` Empty dataclass: `{}`. #### `CTransferListItemDefaultSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Stable declared Item value. | | `label` | `str` | - | Plain accessible and typeahead label. | | `disabled` | `bool` | - | Declared disabled state. | | `in_target` | `bool` | - | Whether the Item is initially chosen. | | `index` | `int` | - | Initial zero-based index in its pane. |
    #### `CTransferListChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `list[str]` | - | Requested or committed ordered chosen values. | | `previousValue` | `list[str]` | - | Effective ordered chosen values before the request. | | `moved` | `list[str]` | - | Values directly affected by the action in their action order. | | `source` | `CTransferListChangeSource` ([`CTransferListChangeSource`](#transfer-list-interface-change-source)) | - | Transfer reorder reset or client cause. | | `controlled` | `bool` | - | Whether client value currently owns chosen state. | | `sourceEvent` | `object | None` | - | Native source Event or null for client reconciliation. |
    ### 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. #### CTransferList translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-transfer-list-available` | Titles the available pane. | `None.` | `available_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-chosen` | Titles the chosen pane and native fallback. | `None.` | `chosen_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-available-empty` | Describes an empty available pane. | `None.` | `available_empty_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-chosen-empty` | Describes an empty chosen pane. | `None.` | `chosen_empty_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-count` | Summarizes selected and total Items in one pane. | `selected: str; total: str` | `count_label` with `{selected}` and `{total}` | Two `i18n.bind()` registrations update when selection totals or locale change. | | `citry-ui-transfer-list-transfer-controls` | Names the transfer toolbar. | `None.` | `transfer_controls_label` | Stable `$c-tr` attribute follows client locale changes. | | `citry-ui-transfer-list-add` | Labels the Add selected action. | `None.` | `add_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-add-all` | Labels the Add all action. | `None.` | `add_all_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-remove` | Labels the Remove selected action. | `None.` | `remove_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-remove-all` | Labels the Remove all action. | `None.` | `remove_all_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-reorder-controls` | Names the chosen-order toolbar. | `None.` | `reorder_controls_label` | Stable `$c-tr` attribute follows client locale changes. | | `citry-ui-transfer-list-move-top` | Labels the Move to top action. | `None.` | `move_top_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-move-up` | Labels the Move up action. | `None.` | `move_up_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-move-down` | Labels the Move down action. | `None.` | `move_down_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-move-bottom` | Labels the Move to bottom action. | `None.` | `move_bottom_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-transfer-list-added-one` | Announces one accepted addition. | `None.` | `added_label` formats the fallback | One-shot `i18n.tr()` writes the live region. | | `citry-ui-transfer-list-added` | Announces multiple accepted additions. | `count: str` | `added_label` with `{count}` | One-shot `i18n.tr()` writes the live region. | | `citry-ui-transfer-list-removed-one` | Announces one accepted removal. | `None.` | `removed_label` formats the fallback | One-shot `i18n.tr()` writes the live region. | | `citry-ui-transfer-list-removed` | Announces multiple accepted removals. | `count: str` | `removed_label` with `{count}` | One-shot `i18n.tr()` writes the live region. | | `citry-ui-transfer-list-reordered-one` | Announces one accepted reorder. | `None.` | `reordered_label` formats the fallback | One-shot `i18n.tr()` writes the live region. | | `citry-ui-transfer-list-reordered` | Announces multiple accepted reorders. | `count: str` | `reordered_label` with `{count}` | One-shot `i18n.tr()` writes the live region. | | `citry-ui-transfer-list-required` | Announces failed required validity. | `None.` | `required_label` | One-shot `i18n.tr()` writes the live region. |
    --- # Col and Row Source: https://citry.dev/ui-library/components/col-row/ # Col and Row Use `CCol` for vertical flow and `CRow` for horizontal flow. Both keep your children unchanged, expose one native root, and render without JavaScript. ## Layout at a glance ### Compose Col and Row [Open the rendered preview](/ui-library/components/col-row/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FlowAtAGlance(Component): template = """

    Kiln room · shelf 4

    Moon jar firing notes

    Hold at 1,280°C until the glaze softens to a pale blue-white.

    Porcelain Reduction 12 hours Archive Save firing
    """ css = """ :where(.flow-glance) { max-inline-size: 34rem; padding: 1.25rem; border: 1px solid light-dark(#d7c8b4, #6f6357); border-radius: 0.85rem; background: light-dark(#fffaf2, #241f1a); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-glance h2, .flow-glance p) { margin: 0; } :where(.flow-glance h2) { font-size: 1.05rem; } :where(.flow-glance__eyebrow) { color: light-dark(#8a4b2b, #f0aa7d); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } :where(.flow-glance__tag) { padding: 0.25rem 0.55rem; border-radius: 999px; background: light-dark(#ead8bd, #4a3d31); font-size: 0.78rem; } """ preview = FlowAtAGlance() preview # noqa: B018 ```` ```citry-html

    Glaze tests

    Archive Publish
    ``` Compose the same layout in Python: ```python from citry_ui import CCol, CRow actions = CRow(slots={"default": ["Archive", "Publish"]}) panel = CCol(gap="lg", slots={"default": ["Glaze tests", actions]}) ``` ## Choose spacing Use the shared `0`, `xs`, `sm`, `md`, `lg`, and `xl` presets. Col defaults to `md`; Row defaults to the tighter `sm`. ### Compare Col spacing [Open the rendered preview](/ui-library/components/col-row/_previews/col-spacing/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class StackSpacing(Component): class Kwargs: pass class Slots: pass template = """
    {{ gap }} Clay body Glaze test
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"gaps": ("0", "xs", "sm", "md", "lg", "xl")} css = """ :where(.flow-spacing) { display: grid; grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); gap: 1rem; max-inline-size: 62rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-spacing__stack) { padding: 0.85rem; border: 1px solid light-dark(#d9c8b2, #62564b); border-radius: 0.65rem; background: light-dark(#fffaf2, #251f1a); } :where(.flow-spacing__stack span) { padding: 0.35rem; border-radius: 0.3rem; background: light-dark(#ead8bd, #493b30); font-size: 0.8rem; } """ preview = StackSpacing() preview # noqa: B018 ```` ## Align and distribute children `align` controls the cross axis. `justify` distributes children along the flow axis. The same vocabulary works across both components. ### Align and distribute Row children [Open the rendered preview](/ui-library/components/col-row/_previews/row-alignment/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GroupAlignment(Component): class Kwargs: pass class Slots: pass template = """ justify={{ justify }} TrimBisqueGlaze """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"justifies": ("start", "center", "end", "between", "around", "evenly")} css = """ :where(.flow-alignments) { max-inline-size: 44rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-alignments__group) { min-block-size: 3.5rem; padding: 0.65rem; border-radius: 0.55rem; background: light-dark(#f2e4cf, #362c24); } :where(.flow-alignments__group span) { padding: 0.35rem 0.5rem; border-radius: 0.35rem; background: light-dark(#b96540, #d7815b); color: #ffffff; font-size: 0.78rem; } """ preview = GroupAlignment() preview # noqa: B018 ```` ## Wrap horizontal content Row wraps by default, making action rows and short metadata collections safe at narrow widths. Set `wrap=False` only when horizontal overflow is deliberate. ### Compare wrapping behavior [Open the rendered preview](/ui-library/components/col-row/_previews/wrapping/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GroupWrapping(Component): class Kwargs: pass class Slots: pass template = """
    Wraps by default {{ label }} No wrap
    {{ label }}
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"labels": ("Wheel throwing", "Hand building", "Slip casting", "Raku firing")} css = """ :where(.flow-wrapping) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); gap: 1rem; max-inline-size: 46rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-wrapping__group) { inline-size: 100%; padding: 0.75rem; background: light-dark(#f1e0c7, #352b23); } :where(.flow-wrapping__group span) { padding: 0.35rem 0.55rem; border: 1px solid currentColor; border-radius: 999px; white-space: nowrap; } :where(.flow-wrapping__scroll) { overflow-x: auto; } """ preview = GroupWrapping() preview # noqa: B018 ```` ## Choose native semantics The default `div` makes no semantic claim. Use `section` for a named section, `nav` for navigation, or `ul`/`ol` when every direct child follows native list content rules. ### Choose semantic roots [Open the rendered preview](/ui-library/components/col-row/_previews/semantic-roots/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FlowSemanticRoots(Component): template = """ ClayGlazeKilns
  • Wedge the porcelain.
  • Center it on the wheel.
  • Pull the walls evenly.
  • """ css = """ :where(.flow-semantics) { max-inline-size: 36rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-semantics a) { color: light-dark(#8a3f24, #f0a47c); } :where(.flow-semantics__list) { margin: 0; padding-inline-start: 1.4rem; } """ preview = FlowSemanticRoots() preview # noqa: B018 ```` The components add no role, accessible name, heading, or list item. Supply the native structure required by your content. ## Nest layouts Col and Row can be nested without extra coordination or client state. ### Build a nested ceramics layout [Open the rendered preview](/ui-library/components/col-row/_previews/nested-layouts/) ````citry from dataclasses import dataclass import citry_ui from citry import Component, citry citry.register_library(citry_ui) @dataclass(frozen=True, slots=True) class FiringBatch: name: str clay: str cone: str class NestedFlowLayouts(Component): class Kwargs: pass class Slots: pass template = """ {{ batch.name }} {{ batch.clay }} {{ batch.cone }} Open log """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return { "batches": ( FiringBatch("Sea mist bowls", "Porcelain", "Cone 10"), FiringBatch("Cedar cups", "Speckled stoneware", "Cone 6"), FiringBatch("Ember vases", "Red earthenware", "Cone 04"), ) } css = """ :where(.flow-nested) { max-inline-size: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-nested__row) { padding: 0.8rem; border-block-end: 1px solid light-dark(#d6c4ad, #5f5247); } :where(.flow-nested__cone) { padding: 0.2rem 0.5rem; border-radius: 999px; background: light-dark(#ead7bd, #4b3b30); font-size: 0.75rem; } """ preview = NestedFlowLayouts() preview # noqa: B018 ```` ## Customize layout Override the public gap variables on an ancestor or one instance. Use stable part selectors, `class_`, or `style` for responsive rules beyond the preset API. ### Customize Flow with public CSS [Open the rendered preview](/ui-library/components/col-row/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FlowCustomization(Component): template = """
    Cobalt studioWide vertical rhythm
    Clay archiveCompact action spacing
    """ css = """ :where(.flow-custom) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; max-inline-size: 40rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-custom__brand) { padding: 1rem; border-radius: 0.75rem; } :where(.flow-custom__brand--cobalt) { --cui-col-gap: 1.35rem; background: light-dark(#dbe8f5, #172b40); } :where(.flow-custom__brand--clay) { --cui-row-gap: 0.25rem; background: light-dark(#f2dfd0, #3b2820); } :where(.flow-custom__brand [data-citry-ui-part="col"], .flow-custom__brand [data-citry-ui-part="row"]) { padding: 0.7rem; border: 1px solid currentColor; border-radius: 0.5rem; } """ preview = FlowCustomization() preview # noqa: B018 ```` ## Direction, visual order, and accessibility Logical alignment follows the document direction. `reverse=True` reverses only the visual flex flow: DOM, reading, and keyboard order do not change. Use it only when the original order remains understandable. ### Compare direction and visual order [Open the rendered preview](/ui-library/components/col-row/_previews/direction/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class FlowDirection(Component): template = """
    LTR kiln sequence LoadFireCool
    تسلسل الفرن تحميلحرقتبريد
    Long label celadon-test-series-with-a-deliberately-long-unbroken-identifier
    """ css = """ :where(.flow-direction) { display: grid; gap: 1.25rem; max-inline-size: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.flow-direction [data-citry-ui-part="row"]) { padding: 0.7rem; background: light-dark(#eee0c9, #372d24); } :where(.flow-direction__long span) { min-inline-size: 0; overflow-wrap: anywhere; } """ preview = FlowDirection() preview # noqa: B018 ```` Flow renders completely without JavaScript. Attribute maps accept native, ARIA, data, and trusted targeted Alpine attributes, but reserve layout reflections, part markers, structural directives, and Citry runtime ownership fields. ## API reference ### Inputs #### CCol server inputs Server inputs are passed in a template through `` or in Python through `CCol(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `tag` | `"div" | "section" | "nav" | "ul" | "ol"` ([`CFlowTag`](#flow-layout-interface-input-type-aliases-cflow-tag)) | `"div"` | Selects the native root without adding a role or accessible name. | | `gap` | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` ([`CFlowGap`](#flow-layout-interface-input-type-aliases-cflow-gap)) | `"md"` | Selects the vertical space between direct children. | | `align` | `"start" | "center" | "end" | "stretch" | "baseline"` ([`CFlowAlign`](#flow-layout-interface-input-type-aliases-cflow-align)) | `"stretch"` | Aligns direct children across the horizontal axis. | | `justify` | `"start" | "center" | "end" | "between" | "around" | "evenly"` ([`CFlowJustify`](#flow-layout-interface-input-type-aliases-cflow-justify)) | `"start"` | Distributes direct children along the vertical axis. | | `reverse` | `bool` | `False` | Reverses visual flow without changing DOM, reading, or Tab order. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#flow-layout-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#flow-layout-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted targeted Alpine attributes without replacing owned layout or Citry runtime fields. |
    #### CRow server inputs Server inputs are passed in a template through `` or in Python through `CRow(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `tag` | `"div" | "section" | "nav" | "ul" | "ol"` ([`CFlowTag`](#flow-layout-interface-input-type-aliases-cflow-tag)) | `"div"` | Selects the native root without adding a role or accessible name. | | `gap` | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` ([`CFlowGap`](#flow-layout-interface-input-type-aliases-cflow-gap)) | `"sm"` | Selects horizontal and wrapped-row spacing between direct children. | | `align` | `"start" | "center" | "end" | "stretch" | "baseline"` ([`CFlowAlign`](#flow-layout-interface-input-type-aliases-cflow-align)) | `"center"` | Aligns direct children across each row. | | `justify` | `"start" | "center" | "end" | "between" | "around" | "evenly"` ([`CFlowJustify`](#flow-layout-interface-input-type-aliases-cflow-justify)) | `"start"` | Distributes direct children along each row. | | `wrap` | `bool` | `True` | Allows direct children to continue on later rows when space runs out. | | `reverse` | `bool` | `False` | Reverses visual flow without changing DOM, reading, or Tab order. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#flow-layout-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#flow-layout-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted targeted Alpine attributes without replacing owned layout or Citry runtime fields. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CCol slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CColDefaultSlotData`](#flow-layout-interface-ccol-default-slot-data)) | Renders an empty layout root. |
    #### CRow slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CRowDefaultSlotData`](#flow-layout-interface-crow-default-slot-data)) | Renders an empty layout root. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CCol CSS variables Apply these variables to `CCol` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-col-gap` | `length` | Overrides the selected direct-child gap. | `Gap-preset length.` |
    #### CRow CSS variables Apply these variables to `CRow` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-row-gap` | `length` | Overrides horizontal and wrapped-row gaps. | `Gap-preset length.` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CCol attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-gap` | Root | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` | Reflects the selected spacing preset. | | `data-align` | Root | `"start" | "center" | "end" | "stretch" | "baseline"` | Reflects cross-axis alignment. | | `data-justify` | Root | `"start" | "center" | "end" | "between" | "around" | "evenly"` | Reflects main-axis distribution. | | `data-reverse` | Root | `Boolean presence` | Present while visual order is reversed. |
    #### CRow attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-gap` | Root | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` | Reflects the selected spacing preset. | | `data-align` | Root | `"start" | "center" | "end" | "stretch" | "baseline"` | Reflects cross-axis alignment. | | `data-justify` | Root | `"start" | "center" | "end" | "between" | "around" | "evenly"` | Reflects main-axis distribution. | | `data-wrap` | Root | `Boolean presence` | Present while wrapping is enabled. | | `data-reverse` | Root | `Boolean presence` | Present while visual order is reversed. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CCol selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="col"]` | Native root | Stable Col root and `attrs` destination. |
    #### CRow selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="row"]` | Native root | Stable Row root and `attrs` destination. |
    ### 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]` | | `CFlowTag` | `Literal["div", "section", "nav", "ul", "ol"]` | | `CFlowGap` | `Literal["0", "xs", "sm", "md", "lg", "xl"]` | | `CFlowAlign` | `Literal["start", "center", "end", "stretch", "baseline"]` | | `CFlowJustify` | `Literal["start", "center", "end", "between", "around", "evenly"]` |
    #### `CColDefaultSlotData` Empty dataclass: `{}`. #### `CRowDefaultSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Container and Grid Source: https://citry.dev/ui-library/components/container-grid/ # Container and Grid `CContainer` constrains page width. `CGrid` handles the common equal-column layout. Add `CGridItem` only when individual content needs an asymmetric span. All three render with native CSS and no JavaScript. ## Layout at a glance ### Browse a responsive mineral atlas [Open the rendered preview](/ui-library/components/container-grid/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridAtAGlance(Component): template = """

    Field atlas · volcanic collection

    Minerals born from fire

    Olivine

    Olive-green crystals found in basalt and mantle rock.

    Obsidian

    Volcanic glass cooled before crystals could form.

    Sulfur

    Bright deposits gathered around volcanic vents.

    Pumice

    Foamed lava light enough to float on water.

    """ css = """ :where(.mineral-atlas) { color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.mineral-atlas__header) { margin-block-end: 1.25rem; } :where(.mineral-atlas__header h2, .mineral-atlas__header p, .mineral-atlas__card h3, .mineral-atlas__card p) { margin: 0; } :where(.mineral-atlas__header p) { color: light-dark(#7c3f16, #f4ad74); font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } :where(.mineral-atlas__header h2) { margin-block-start: 0.25rem; font-size: 1.1rem; } :where(.mineral-atlas__card) { padding: 1rem; border: 1px solid light-dark(#d7d3c8, #55524b); border-radius: 0.8rem; background: light-dark(#fffefa, #22211f); } :where(.mineral-atlas__sample) { display: block; inline-size: 2.25rem; block-size: 2.25rem; margin-block-end: 0.8rem; border-radius: 0.65rem 1rem 0.5rem 0.9rem; background: var(--sample-color); box-shadow: inset -0.3rem -0.3rem 0.7rem rgb(0 0 0 / 20%); transform: rotate(-7deg); } :where(.mineral-atlas__card h3) { font-size: 0.9rem; } :where(.mineral-atlas__card p) { margin-block-start: 0.35rem; color: GrayText; font-size: 0.78rem; line-height: 1.45; } :where(.mineral-atlas__card--olivine) { --sample-color: #7c9d38; } :where(.mineral-atlas__card--obsidian) { --sample-color: #493e57; } :where(.mineral-atlas__card--sulfur) { --sample-color: #efc928; } :where(.mineral-atlas__card--pumice) { --sample-color: #caa68e; } """ preview = GridAtAGlance() preview # noqa: B018 ```` ```citry-html ... ``` The base layout has one column. `sm="2"` applies from `40rem`; `lg="4"` applies from `64rem`. Missing breakpoints keep the nearest earlier value. ## Choose responsive columns Put equal-column counts on Grid itself. This keeps the frequent card, tile, and gallery case short—no item wrapper required. ### Compare fixed and responsive columns [Open the rendered preview](/ui-library/components/container-grid/_previews/responsive-columns/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridResponsiveColumns(Component): class Kwargs: pass class Slots: pass template = """

    Crystal systems

    Resize the preview to watch one column become two, then four.

    {{ system }}

    Fixed three-column index

    {{ name }}
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return { "systems": ("Cubic", "Hexagonal", "Monoclinic", "Trigonal"), "fixed_names": ("Quartz", "Calcite", "Galena"), } css = """ :where(.grid-columns) { max-inline-size: 52rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.grid-columns h2, .grid-columns h3, .grid-columns p) { margin: 0; } :where(.grid-columns h2) { font-size: 1rem; } :where(.grid-columns h3) { margin-block-start: 1.25rem; margin-block-end: 0.5rem; font-size: 0.82rem; } :where(.grid-columns p) { margin-block: 0.25rem 0.8rem; color: GrayText; font-size: 0.78rem; } :where(.grid-columns__cell) { min-block-size: 3.25rem; padding: 0.7rem; border-inline-start: 0.3rem solid #4b77be; border-radius: 0.35rem; background: light-dark(#edf4ff, #1c2b40); font-size: 0.78rem; font-weight: 700; } :where(.grid-columns__cell--quiet) { border-inline-start-color: #a55f38; background: light-dark(#faf0e9, #35241c); } """ preview = GridResponsiveColumns() preview # noqa: B018 ```` Static template values use flat decimal attributes. Dynamic template values use the normal `c-` expression prefix: ```citry-html ... ``` Column counts and spans accept integers or ASCII decimal strings from static attributes, dynamic expressions, and Python composition. Citry normalizes a decimal string to an integer, then applies the same 1 through 12 range check. For example, Python can use `CGrid(sm=2, lg=desktop_cols)`. ## Build asymmetric layouts Use a 12-column Grid and span only the exceptional items. `CGridItem` remains a normal wrapper; it adds no region or landmark semantics. ### Compose field notes and a specimen index [Open the rendered preview](/ui-library/components/container-grid/_previews/asymmetric-layout/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridAsymmetricLayout(Component): template = """

    Expedition 14 · obsidian ridge

    Glass formed at the lava margin

    The largest fragments show conchoidal fractures, faint silver banding, and almost no visible crystal growth.

    Specimen index

    R-14A
    Black glass
    R-14B
    Snowflake
    R-14C
    Mahogany
    """ css = """ :where(.field-journal) { color: CanvasText; font-family: ui-serif, Georgia, serif; } :where(.field-journal__notes, .field-journal__index) { padding: 1.1rem; border: 1px solid light-dark(#cec8b8, #625d52); border-radius: 0.65rem; background: light-dark(#fffdf6, #25231f); } :where(.field-journal h2, .field-journal h3, .field-journal p, .field-journal dl) { margin: 0; } :where(.field-journal__eyebrow) { color: light-dark(#8d4727, #eab08d); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.68rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } :where(.field-journal h2) { margin-block: 0.35rem 0.65rem; font-size: 1.05rem; } :where(.field-journal__notes > p:last-child) { color: GrayText; font-size: 0.8rem; line-height: 1.55; } :where(.field-journal h3) { margin-block-end: 0.6rem; font-size: 0.85rem; } :where(.field-journal dl > div) { display: flex; justify-content: space-between; gap: 0.5rem; padding-block: 0.35rem; border-block-end: 1px dotted GrayText; font-size: 0.76rem; } :where(.field-journal dd) { margin: 0; color: GrayText; } """ preview = GridAsymmetricLayout() preview # noqa: B018 ```` ```citry-html ... ... ``` Keep DOM order meaningful. Responsive spans change visual width, not reading, keyboard, or form-submission order. ## Fit columns to available space `min_col` uses intrinsic auto-fit tracks. It is useful when card width matters more than named viewport steps. ### Fit mineral cards by minimum width [Open the rendered preview](/ui-library/components/container-grid/_previews/intrinsic-grid/) ````citry from dataclasses import dataclass import citry_ui from citry import Component, citry citry.register_library(citry_ui) @dataclass(frozen=True, slots=True) class Mineral: name: str hardness: str class GridIntrinsic(Component): class Kwargs: pass class Slots: pass template = """

    Mohs hardness field set

    {{ mineral.name }} {{ mineral.hardness }}
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return { "minerals": ( Mineral("Talc", "1 · very soft"), Mineral("Calcite", "3 · copper scratch"), Mineral("Apatite", "5 · knife edge"), Mineral("Quartz", "7 · scratches glass"), Mineral("Corundum", "9 · near diamond"), ) } css = """ :where(.intrinsic-minerals) { max-inline-size: 50rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.intrinsic-minerals h2) { margin: 0 0 0.75rem; font-size: 0.95rem; } :where(.intrinsic-minerals__card) { display: flex; justify-content: space-between; gap: 0.75rem; padding: 0.75rem; border-block-start: 0.2rem solid #6f63a8; background: light-dark(#f5f1ff, #28243a); font-size: 0.76rem; } :where(.intrinsic-minerals__card span) { color: GrayText; text-align: end; } """ preview = GridIntrinsic() preview # noqa: B018 ```` Intrinsic mode owns track sizing, so it cannot be combined with `cols` or breakpoint counts. For CSS functions such as `clamp()`, set `--cui-grid-min-column` instead. ## Constrain page content Container defaults to a centered `80rem` maximum with `1rem` inline gutters. Choose a smaller/larger size, or use `fluid` to retain gutters without a maximum width. ### Compare Container sizes and fluid width [Open the rendered preview](/ui-library/components/container-grid/_previews/container-sizes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridContainerSizes(Component): template = """

    Atlas page widths

    sm · 40rem maximum Focused specimen notes md · 48rem maximum Illustrated field article fluid · no maximum Full-width comparison plate
    """ css = """ :where(.container-sizes) { color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.container-sizes h2) { margin: 0 0 0.75rem; font-size: 0.95rem; } :where(.container-sizes__sample) { display: flex; justify-content: space-between; gap: 0.75rem; margin-block: 0.5rem; padding-block: 0.65rem; border: 1px solid light-dark(#cbc7bb, #5c5952); border-radius: 0.45rem; font-size: 0.74rem; } :where(.container-sizes__sample span) { color: GrayText; text-align: end; } :where(.container-sizes__sample--sm) { border-inline-start: 0.3rem solid #b56b3f; } :where(.container-sizes__sample--md) { border-inline-start: 0.3rem solid #4c7a6a; } :where(.container-sizes__sample--fluid) { border-inline-start: 0.3rem solid #596fb1; } """ preview = GridContainerSizes() preview # noqa: B018 ```` Container does not establish a CSS query container. Add `container-type` in consumer CSS only where that behavior is needed. ## Adjust spacing Grid `gap` controls both axes. Container `gutter` controls logical inline padding. Both use `0`, `xs`, `sm`, `md`, `lg`, and `xl`. ### Compare Grid gaps and Container gutters [Open the rendered preview](/ui-library/components/container-grid/_previews/spacing/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridSpacing(Component): class Kwargs: pass class Slots: pass template = """

    Spacing scale

    gap={{ gap }}
    Container gutter=xl keeps this note away from both inline edges.
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"gaps": ("0", "sm", "md", "xl")} css = """ :where(.grid-spacing) { max-inline-size: 46rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.grid-spacing h2) { margin: 0 0 0.75rem; font-size: 0.95rem; } :where(.grid-spacing__example) { padding: 0.7rem; border: 1px solid light-dark(#d4d0c5, #56534c); border-radius: 0.5rem; font-size: 0.7rem; } :where(.grid-spacing__example strong) { display: block; margin-block-end: 0.45rem; } :where(.grid-spacing__example span) { min-block-size: 1.8rem; border-radius: 0.25rem; background: light-dark(#d1e3dd, #285044); } :where(.grid-spacing__gutter) { margin-block-start: 1rem; padding-block: 0.65rem; border-block: 1px dashed light-dark(#8d7662, #b9a28d); background: light-dark(#f9f3ea, #30271f); font-size: 0.74rem; } """ preview = GridSpacing() preview # noqa: B018 ```` ## Choose semantics and nest layouts Select native elements that match the content. Grid can render `ul`/`ol`, and GridItem can render `li`; Citry does not fabricate list or landmark semantics. Nested grids keep their own breakpoint values. ### Build a semantic nested specimen catalog [Open the rendered preview](/ui-library/components/container-grid/_previews/semantics-and-nesting/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridSemanticsAndNesting(Component): template = """

    Mineral families

    Silicates QuartzFeldspar Carbonates CalciteDolomite
    """ css = """ :where(.mineral-catalog) { color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.mineral-catalog h2) { margin: 0 0 0.75rem; font-size: 0.95rem; } :where(.mineral-catalog__list) { margin: 0; padding: 0; list-style: none; } :where(.mineral-catalog__list > li) { padding: 0.85rem; border: 1px solid light-dark(#d7cfbe, #5e574c); border-radius: 0.55rem; background: light-dark(#fffaf0, #29251f); font-size: 0.78rem; } :where(.mineral-catalog__nested) { margin-block-start: 0.6rem; } :where(.mineral-catalog__nested span) { padding: 0.35rem; border-radius: 0.25rem; background: light-dark(#e5eee9, #263a32); text-align: center; } """ preview = GridSemanticsAndNesting() preview # noqa: B018 ```` ## Customize the layout Use public variables for local changes and stable part selectors or `class_` for bespoke responsive rules. Tailwind and similar utility frameworks can style these native roots through `class_`; Citry UI does not duplicate their utility vocabulary. ### Customize Grid variables and a container query [Open the rendered preview](/ui-library/components/container-grid/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class GridCustomization(Component): template = """

    Custom field trays

    GraniteGabbroRhyolite
    SlateSchistGneiss
    Logical gutters follow the reading direction without a separate RTL input.
    """ css = """ :where(.grid-custom) { max-inline-size: 48rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.grid-custom h2) { margin: 0 0 0.75rem; font-size: 0.95rem; } :where(.grid-custom__brand) { --cui-grid-columns: 3; --cui-grid-gap: 0.35rem; padding: 0.75rem; border-radius: 0.55rem; background: light-dark(#e9f0f7, #1d2e3e); } :where(.grid-custom__brand [data-citry-ui-part="grid"] > span) { padding: 0.55rem; border-radius: 0.3rem; background: light-dark(#ffffff, #2c4357); font-size: 0.74rem; text-align: center; } :where(.grid-custom__query-box) { container-type: inline-size; margin-block-start: 0.75rem; padding: 0.75rem; border: 1px solid light-dark(#b9af9d, #6c6254); border-radius: 0.55rem; } :where(.grid-custom__query-grid > span) { padding: 0.5rem; background: light-dark(#f4eadb, #3a2d22); font-size: 0.74rem; text-align: center; } @container (min-width: 28rem) { :where(.grid-custom__query-grid) { --cui-grid-columns: 3; } } :where(.grid-custom__rtl) { margin-block-start: 0.75rem; border-inline-start: 0.25rem solid #7f5baa; background: light-dark(#f6efff, #332541); font-size: 0.74rem; } """ preview = GridCustomization() preview # noqa: B018 ```` The built-in `sm`, `md`, `lg`, `xl`, and `xxl` thresholds are viewport-based and fixed. A custom class can use any media or container query without adding another component input. The family reserves its part/configuration attributes, Citry runtime fields, whole-object spreads, and structural Alpine directives. Ordinary native, ARIA, data, listener, and targeted unrelated binding attributes remain available through `attrs`. ## API reference ### Inputs #### CContainer server inputs Server inputs are passed in a template through `` or in Python through `CContainer(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `tag` | `"div" | "main" | "section" | "article" | "nav" | "aside"` ([`CContainerTag`](#grid-container-interface-input-type-aliases-container-tag)) | `"div"` | Selects the native root without adding a role or accessible name. | | `size` | `"sm" | "md" | "lg" | "xl" | "xxl"` ([`CContainerSize`](#grid-container-interface-input-type-aliases-container-size)) | `"xl"` | Selects the centered maximum inline size from `40rem` through `96rem`. | | `fluid` | `bool` | `False` | Removes the maximum width while retaining the selected inline gutter. | | `gutter` | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` ([`CLayoutGap`](#grid-container-interface-input-type-aliases-layout-gap)) | `"lg"` | Selects logical inline padding from `0` through `1.5rem`. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#grid-container-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#grid-container-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted targeted Alpine attributes without replacing owned layout or Citry runtime fields. |
    #### CGrid server inputs Server inputs are passed in a template through `` or in Python through `CGrid(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `tag` | `"div" | "main" | "section" | "article" | "ul" | "ol"` ([`CGridTag`](#grid-container-interface-input-type-aliases-grid-tag)) | `"div"` | Selects the native Grid root without adding semantics. | | `cols` | `int | ASCII decimal str` | `1` | Sets the equal base column count from 1 through 12. | | `sm` | `int | ASCII decimal str | None` | `None` | Overrides equal columns at `40rem` and wider. | | `md` | `int | ASCII decimal str | None` | `None` | Overrides equal columns at `48rem` and wider. | | `lg` | `int | ASCII decimal str | None` | `None` | Overrides equal columns at `64rem` and wider. | | `xl` | `int | ASCII decimal str | None` | `None` | Overrides equal columns at `80rem` and wider. | | `xxl` | `int | ASCII decimal str | None` | `None` | Overrides equal columns at `96rem` and wider. | | `min_col` | `str | None` | `None` | Uses intrinsic auto-fit columns with one positive `px`, `rem`, `em`, `ch`, viewport-width, or viewport-height length; cannot be combined with fixed/responsive counts. | | `gap` | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` ([`CLayoutGap`](#grid-container-interface-input-type-aliases-layout-gap)) | `"md"` | Selects row and column gap from `0` through `1.5rem`. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#grid-container-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#grid-container-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted targeted Alpine attributes without replacing owned layout or Citry runtime fields. |
    #### CGridItem server inputs Server inputs are passed in a template through `` or in Python through `CGridItem(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `tag` | `"div" | "main" | "section" | "article" | "aside" | "li"` ([`CGridItemTag`](#grid-container-interface-input-type-aliases-grid-item-tag)) | `"div"` | Selects the native Grid item root without adding semantics. | | `span` | `int | ASCII decimal str` | `1` | Sets the base column span from 1 through 12. | | `sm` | `int | ASCII decimal str | None` | `None` | Overrides the span at `40rem` and wider. | | `md` | `int | ASCII decimal str | None` | `None` | Overrides the span at `48rem` and wider. | | `lg` | `int | ASCII decimal str | None` | `None` | Overrides the span at `64rem` and wider. | | `xl` | `int | ASCII decimal str | None` | `None` | Overrides the span at `80rem` and wider. | | `xxl` | `int | ASCII decimal str | None` | `None` | Overrides the span at `96rem` and wider. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#grid-container-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#grid-container-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted targeted Alpine attributes without replacing owned layout or Citry runtime fields. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CContainer slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CContainerDefaultSlotData`](#grid-container-interface-ccontainer-default-slot-data)) | Renders an empty Container root. |
    #### CGrid slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CGridDefaultSlotData`](#grid-container-interface-cgrid-default-slot-data)) | Renders an empty Grid root. |
    #### CGridItem slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CGridItemDefaultSlotData`](#grid-container-interface-cgriditem-default-slot-data)) | Renders an empty Grid item root. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CContainer CSS variables Apply these variables to `CContainer` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-container-max-width` | `length` | Overrides the selected centered maximum inline size. | ``Selected size from `40rem` through `96rem`.`` | | `--cui-container-gutter` | `length` | Overrides logical inline padding. | `Selected gutter-preset length.` |
    #### CGrid CSS variables Apply these variables to `CGrid` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-grid-columns` | `integer` | Overrides the effective equal column count at every breakpoint. | ``Effective responsive `cols` value.`` | | `--cui-grid-gap` | `length` | Overrides row and column gap. | `Selected gap-preset length.` | | `--cui-grid-min-column` | `length` | Overrides the requested intrinsic minimum column size. | `` `min_col` value. `` |
    #### CGridItem CSS variables Apply these variables to `CGridItem` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-grid-item-span` | `integer` | Overrides the effective span at every breakpoint. | ``Effective responsive `span` value.`` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CContainer attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-size` | Root | `"sm" | "md" | "lg" | "xl" | "xxl"` | Reflects the selected maximum-width preset. | | `data-fluid` | Root | `Boolean presence` | Present while the maximum width is removed. | | `data-gutter` | Root | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` | Reflects the selected inline-gutter preset. |
    #### CGrid attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-cols` | Root | `Integer 1–12` | Reflects the base equal column count. | | `data-cols-sm` | Root | `Integer 1–12 when supplied` | Reflects the authored `sm` column override. | | `data-cols-md` | Root | `Integer 1–12 when supplied` | Reflects the authored `md` column override. | | `data-cols-lg` | Root | `Integer 1–12 when supplied` | Reflects the authored `lg` column override. | | `data-cols-xl` | Root | `Integer 1–12 when supplied` | Reflects the authored `xl` column override. | | `data-cols-xxl` | Root | `Integer 1–12 when supplied` | Reflects the authored `xxl` column override. | | `data-intrinsic` | Root | `Boolean presence` | Present in intrinsic auto-fit mode. | | `data-gap` | Root | `"0" | "xs" | "sm" | "md" | "lg" | "xl"` | Reflects the selected gap preset. |
    #### CGridItem attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-span` | Root | `Integer 1–12` | Reflects the base column span. | | `data-span-sm` | Root | `Integer 1–12 when supplied` | Reflects the authored `sm` span override. | | `data-span-md` | Root | `Integer 1–12 when supplied` | Reflects the authored `md` span override. | | `data-span-lg` | Root | `Integer 1–12 when supplied` | Reflects the authored `lg` span override. | | `data-span-xl` | Root | `Integer 1–12 when supplied` | Reflects the authored `xl` span override. | | `data-span-xxl` | Root | `Integer 1–12 when supplied` | Reflects the authored `xxl` span override. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CContainer selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="container"]` | Native root | Stable Container root and `attrs` destination. |
    #### CGrid selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="grid"]` | Native root | Stable Grid root and `attrs` destination. |
    #### CGridItem selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="grid-item"]` | Native root | Stable GridItem root and `attrs` destination. |
    ### 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]` | | `CContainerTag` | `Literal["div", "main", "section", "article", "nav", "aside"]` | | `CGridTag` | `Literal["div", "main", "section", "article", "ul", "ol"]` | | `CGridItemTag` | `Literal["div", "main", "section", "article", "aside", "li"]` | | `CContainerSize` | `Literal["sm", "md", "lg", "xl", "xxl"]` | | `CLayoutGap` | `Literal["0", "xs", "sm", "md", "lg", "xl"]` |
    #### `CContainerDefaultSlotData` Empty dataclass: `{}`. #### `CGridDefaultSlotData` Empty dataclass: `{}`. #### `CGridItemDefaultSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Divider Source: https://citry.dev/ui-library/components/divider/ # Divider Use `CDivider` for a thematic break between sections or a decorative line in dense layouts. It adds no external spacing and no JavaScript. ## Divider at a glance ### Divider at a glance [Open the rendered preview](/ui-library/components/divider/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DividerAtAGlance(Component): template = """

    Deep-sky field guide

    Northern summer

    Trace bright nebulae before the Milky Way reaches the western horizon.

    After midnight
    Cygnus Lyra Aquila
    """ css = """ :where(.divider-glance) { max-inline-size: 36rem; padding: 1.25rem; border: 1px solid light-dark(#b9c9e8, #41557c); border-radius: 0.9rem; background: light-dark(#f7f9ff, #141b30); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-glance h2, .divider-glance p) { margin: 0; } :where(.divider-glance h2) { margin-block: 0.2rem 0.5rem; font-size: 1.15rem; } :where(.divider-glance__eyebrow) { color: light-dark(#3d5c9a, #a9bfe8); font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } :where(.divider-glance [data-citry-ui-part="divider"][data-labeled]) { margin-block: 1rem; } :where(.divider-glance__row) { display: flex; min-block-size: 2.25rem; align-items: stretch; gap: 0.75rem; } """ preview = DividerAtAGlance() preview # noqa: B018 ```` ## Compose a Divider An unlabelled horizontal Divider is a native thematic break. ### Compose semantic Dividers [Open the rendered preview](/ui-library/components/divider/_previews/basic-dividers/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BasicDividers(Component): template = """

    Orion Nebula

    A luminous stellar nursery around 1,300 light-years away.

    {{ python_divider }}

    Lagoon Nebula

    Dark dust lanes cross a glowing cloud in Sagittarius.

    """ def template_data( self, kwargs: Any, # noqa: ARG002 slots: Any, # noqa: ARG002 ) -> dict[str, object]: return {"python_divider": citry_ui.CDivider(variant="dotted")} css = """ :where(.divider-basic) { display: grid; gap: 1rem; max-inline-size: 34rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-basic h2, .divider-basic p) { margin: 0; } :where(.divider-basic h2) { font-size: 1rem; } """ preview = BasicDividers() preview # noqa: B018 ```` ```citry-html ``` Compose the same result in Python: ```python from citry_ui import CDivider divider = CDivider() ``` ## Choose semantic or decorative output Keep the default when the break separates topics. Use `decorative=True` when the line is only visual and nearby structure already conveys the grouping. ### Compare semantic and decorative lines [Open the rendered preview](/ui-library/components/divider/_previews/semantic-and-decorative/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SemanticAndDecorativeDividers(Component): template = """

    Semantic break

    Observation notes end here.

    A new topic begins with the equipment log.

    Decorative line

    Exposure 180 s
    """ css = """ :where(.divider-meaning) { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 1rem; max-inline-size: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-meaning article) { display: grid; gap: 0.75rem; padding: 1rem; border: 1px solid light-dark(#cbd5e1, #475569); border-radius: 0.75rem; } :where(.divider-meaning h2, .divider-meaning p) { margin: 0; } :where(.divider-meaning h2) { font-size: 1rem; } :where(.divider-meaning__metric) { display: grid; gap: 0.5rem; } """ preview = SemanticAndDecorativeDividers() preview # noqa: B018 ```` ## Choose orientation Horizontal Dividers separate vertically stacked content. Vertical Dividers separate items across a flex or grid row and stretch with their container. ### Compare horizontal and vertical Dividers [Open the rendered preview](/ui-library/components/divider/_previews/orientations/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DividerOrientations(Component): template = """
    First quarter Full moon
    Rise 20:14 Transit 01:36 Set 06:51
    """ css = """ :where(.divider-orientations) { display: grid; gap: 1.25rem; max-inline-size: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-orientations__horizontal) { display: grid; gap: 0.75rem; } :where(.divider-orientations__vertical) { display: flex; min-block-size: 2.5rem; flex-wrap: wrap; align-items: stretch; gap: 0.75rem; padding: 0.75rem; border-radius: 0.6rem; background: light-dark(#eef2ff, #1e2744); } """ preview = DividerOrientations() preview # noqa: B018 ```` ## Add a visible label The optional default slot places ordinary visible content between two decorative lines. Use a real heading inside when the document needs heading semantics. Labels are horizontal only. ### Position visible Divider labels [Open the rendered preview](/ui-library/components/divider/_previews/labels/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DividerLabels(Component): template = """
    Inner planets Asteroid belt Outer planets
    """ css = """ :where(.divider-labels) { display: grid; gap: 1.5rem; max-inline-size: 40rem; padding: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = DividerLabels() preview # noqa: B018 ```` ## Choose line style and thickness Variants select solid, dashed, or dotted lines. Sizes provide concise 1, 2, and 4 pixel thickness presets. ### Compare Divider variants and sizes [Open the rendered preview](/ui-library/components/divider/_previews/variants-and-sizes/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DividerVariantsAndSizes(Component): template = """
    {{ variant }}
    {{ size }}
    """ def template_data( self, kwargs: Any, # noqa: ARG002 slots: Any, # noqa: ARG002 ) -> dict[str, object]: return { "variants": ("solid", "dashed", "dotted"), "sizes": ("sm", "md", "lg"), } css = """ :where(.divider-matrix) { display: grid; gap: 1rem; max-inline-size: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-matrix__row) { display: grid; grid-template-columns: 5rem repeat(3, minmax(4rem, 1fr)); align-items: center; gap: 0.75rem; } :where(.divider-matrix__row > div) { display: grid; gap: 0.35rem; } :where(.divider-matrix span) { color: light-dark(#475569, #cbd5e1); font-size: 0.72rem; text-align: center; } """ preview = DividerVariantsAndSizes() preview # noqa: B018 ```` ## Align with nested content Insets add logical spacing along the line axis. They follow text direction, so `start` and `end` remain meaningful in LTR and RTL layouts. ### Apply logical Divider insets [Open the rendered preview](/ui-library/components/divider/_previews/insets/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DividerInsets(Component): template = """
    {{ inset }}
    start in RTL
    """ def template_data( self, kwargs: Any, # noqa: ARG002 slots: Any, # noqa: ARG002 ) -> dict[str, object]: return {"insets": ("none", "start", "end", "both")} css = """ :where(.divider-insets) { display: grid; gap: 1rem; max-inline-size: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-insets > div) { display: grid; gap: 0.35rem; padding: 0.5rem; border-inline: 1px dashed light-dark(#94a3b8, #64748b); } :where(.divider-insets span) { font-size: 0.75rem; } """ preview = DividerInsets() preview # noqa: B018 ```` ## Customize Divider Override public variables on an ancestor or one Divider. Stable selectors let you style the root, label, or labelled line segments without private classes. ### Customize Divider with public CSS [Open the rendered preview](/ui-library/components/divider/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DividerCustomization(Component): template = """
    Polar observatory
    Eclipse watch
    """ css = """ :where(.divider-themes) { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 1rem; max-inline-size: 40rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.divider-themes > div) { padding: 1.25rem; border-radius: 0.75rem; } :where(.divider-themes__aurora) { --cui-divider-color: #138a7b; --cui-divider-label-color: #12584f; --cui-divider-thickness: 2px; background: #e7faf6; } :where(.divider-themes__eclipse) { color-scheme: dark; --cui-divider-color: #f2b84b; --cui-divider-label-color: #ffe2a6; --cui-divider-label-font-weight: 750; background: #171421; } :where(.divider-themes [data-citry-ui-part="label"]) { letter-spacing: 0.03em; } """ preview = DividerCustomization() preview # noqa: B018 ```` ## Accessibility and behavior The default horizontal form renders a native `hr`. The vertical form renders a nonfocusable ARIA separator. Decorative output is hidden from assistive technology. Labelled lines are decorative while the label remains ordinary document content. Divider never owns focus, keyboard input, resize behavior, or external margin. Use layout gaps for spacing and `CSplitter` for adjustable panes. ## API reference ### Inputs #### CDivider server inputs Server inputs are passed in a template through `` or in Python through `CDivider(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `orientation` | `"horizontal" | "vertical"` ([`CDividerOrientation`](#divider-interface-input-type-aliases-cdivider-orientation)) | `"horizontal"` | Selects native horizontal or ARIA vertical separator semantics. | | `variant` | `"solid" | "dashed" | "dotted"` ([`CDividerVariant`](#divider-interface-input-type-aliases-cdivider-variant)) | `"solid"` | Selects the line style. | | `size` | `"sm" | "md" | "lg"` ([`CDividerSize`](#divider-interface-input-type-aliases-cdivider-size)) | `"sm"` | Selects 1px, 2px, or 4px fallback thickness. | | `inset` | `"none" | "start" | "end" | "both"` ([`CDividerInset`](#divider-interface-input-type-aliases-cdivider-inset)) | `"none"` | Adds logical spacing along the line axis. | | `label_pos` | `"start" | "center" | "end"` ([`CDividerLabelPos`](#divider-interface-input-type-aliases-cdivider-label-pos)) | `"center"` | Positions a supplied visible label; non-default values require the default slot. | | `decorative` | `bool` | `False` | Removes an unlabelled Divider from the accessibility tree. Labelled line segments are always decorative. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#divider-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#divider-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted native, data, targeted Alpine, and event attributes without replacing Divider semantics, anatomy, or Citry runtime fields. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CDivider slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CDividerDefaultSlotData`](#divider-interface-cdivider-default-slot-data)) | Renders one unlabelled line. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CDivider CSS variables Apply these variables to `CDivider` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-divider-color` | `color` | Line color. | `Nested-scheme border color.` | | `--cui-divider-thickness` | `length` | Line thickness. | `Size-derived 1px, 2px, or 4px.` | | `--cui-divider-inset` | `length` | Logical start/end inset amount. | `1.5rem` | | `--cui-divider-label-gap` | `length` | Gap from a visible label to each line. | `0.75rem` | | `--cui-divider-label-color` | `color` | Visible label foreground. | `CanvasText` | | `--cui-divider-label-font-size` | `length` | Visible label text size. | `0.875rem` | | `--cui-divider-label-font-weight` | `font-weight` | Visible label emphasis. | `600` | | `--cui-divider-min-length` | `length` | Useful vertical minimum length. | `1em` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CDivider attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-orientation` | Root | `"horizontal" | "vertical"` | Reflects the line axis and semantic form. | | `data-variant` | Root | `"solid" | "dashed" | "dotted"` | Reflects the line style. | | `data-size` | Root | `"sm" | "md" | "lg"` | Reflects the thickness preset. | | `data-inset` | Root | `"none" | "start" | "end" | "both"` | Reflects logical inset geometry. | | `data-labeled` | Root | `present-or-absent` | Present when the default label slot renders. | | `data-label-pos` | Labelled root | `"start" | "center" | "end"` | Reflects labelled line balance. | | `data-decorative` | Root | `present-or-absent` | Present when the line exposes no separator semantics. | | `aria-orientation` | Vertical semantic root | `"vertical"` | Communicates the nondefault separator orientation. | | `aria-hidden` | Decorative root or labelled line | `boolean-presence` | Removes the decorative line from the accessibility tree. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CDivider selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="divider"]` | Root | Stable Divider root and `attrs` destination. | | `[data-citry-ui-part="line"]` | Labelled decorative line segment | Styles either of the two direct line children. | | `[data-citry-ui-part="label"]` | Labelled visible-content wrapper | Styles the authored section label. |
    ### 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]` | | `CDividerOrientation` | `Literal["horizontal", "vertical"]` | | `CDividerVariant` | `Literal["solid", "dashed", "dotted"]` | | `CDividerSize` | `Literal["sm", "md", "lg"]` | | `CDividerInset` | `Literal["none", "start", "end", "both"]` | | `CDividerLabelPos` | `Literal["start", "center", "end"]` |
    #### `CDividerDefaultSlotData` Empty dataclass: `{}`. ### Translation keys - --- # ScrollArea Source: https://citry.dev/ui-library/components/scroll-area/ # ScrollArea Use `CScrollArea` when bounded content needs a consistent focus stop, optional region name, logical-axis policy, normalized scroll callback, or retained-root lifecycle behavior. The component renders one native scrolling `div`. The browser still owns its scrollbar, wheel, touch, trackpad, and keyboard behavior. Use ordinary CSS when `overflow: auto` is enough. ScrollArea does not replace native scrollbars or add track, thumb, corner, edge-shadow, or scroll-button elements. ## Start with one native viewport The default slot is transparent. It adds no content wrapper and does not change the semantics, focus order, or layout of its children. ```citry-html
    1. Import completed
    2. Review requested
    3. Release approved
    ``` When `aria_label` or `aria_labelledby` is supplied, the viewport becomes a named region. Omit both for a generic focusable viewport. The two inputs are mutually exclusive. ### Block, inline, and two-axis native scrolling [Open the rendered preview](/ui-library/components/scroll-area/_previews/at-a-glance/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CScrollArea citry.register_library(citry_ui) class ScrollAreaAtAGlance(Component): class Kwargs: pass class Slots: pass def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, Any]: return { "python_activity": CScrollArea( style={"--cui-scroll-area-max-block-size": "7rem"}, slots={ "default": ( "Python composition keeps the same native viewport. ", "Its content remains ordinary escaped slot content. ", "The scrollbar belongs to the browser.", ), }, ), } template = """

    Recent activity

    1. Import completed
    2. Review requested
    3. Access approved
    4. Build started
    5. Checks completed
    6. Release published
    7. Audit archived

    Applied filters

    Region: Central Europe Status: Needs review Owner: Operations Window: Last 90 days

    Result matrix

    ServiceOwnerRegion AccountsIdentityPrague LedgerFinanceBerlin SearchDiscoveryVienna ArchiveRecordsWarsaw

    Python composition

    {{ python_activity }}
    """ css = """ :where(.scroll-area-glance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-glance article) { display: grid; gap: 0.75rem; align-content: start; min-inline-size: 0; } :where(.scroll-area-glance h3) { margin: 0; } :where(.scroll-area-glance__activity) { display: grid; gap: 0.5rem; margin: 0; padding: 1rem 1rem 1rem 2rem; } :where(.scroll-area-glance__rail) { display: flex; inline-size: max-content; gap: 0.75rem; padding: 1rem; } :where(.scroll-area-glance__rail span) { padding: 0.375rem 0.625rem; border-radius: 999px; background: color-mix(in srgb, Highlight 14%, Canvas); } :where(.scroll-area-glance__matrix) { display: grid; grid-template-columns: repeat(3, minmax(9rem, 1fr)); gap: 1px; inline-size: max-content; min-inline-size: 30rem; background: color-mix(in srgb, CanvasText 18%, transparent); } :where(.scroll-area-glance__matrix > *) { padding: 0.625rem; background: Canvas; } """ preview = ScrollAreaAtAGlance() preview # noqa: B018 ```` ## Enter the viewport with the keyboard The viewport always has `tabindex="0"` and a visible focus ring. Native Page, Home, End, Space, arrow, wheel, and touch behavior stays with the browser, so exact keys and pixel increments can differ by platform. Focusable children keep their ordinary Tab order. ScrollArea never traps or moves focus. ### Viewport and descendant focus [Open the rendered preview](/ui-library/components/scroll-area/_previews/activity-and-focus/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaActivityAndFocus(Component): template = """

    Tab enters the viewport before its descendants. Native scrolling keys keep focus on the viewport.

    1. 09:10 Build completed. View build details
    2. 09:18 Security review requested. Open review
    3. 09:26 Staging deployment completed. Read staging log
    4. 09:42 Production approval received. Publish release
    5. 09:51 Release notes archived. Open release notes
    Focus the viewport, a link, or an action
    """ css = """ :where(.scroll-area-focus) { display: grid; gap: 0.75rem; max-inline-size: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-focus p, .scroll-area-focus output) { margin: 0; } :where(.scroll-area-focus__timeline) { display: grid; gap: 1rem; margin: 0; padding: 1rem 1rem 1rem 2.5rem; } :where(.scroll-area-focus__timeline li) { display: grid; grid-template-columns: 4rem 1fr; gap: 0.375rem 0.75rem; align-items: center; } :where(.scroll-area-focus__timeline li > :not(strong)) { grid-column: 2; } """ preview = ScrollAreaActivityAndFocus() preview # noqa: B018 ```` Do not attach a root key handler to reproduce native scrolling. It can consume Home, End, or arrow keys intended for an input or another interactive child. ## Keep wide data semantic Use `axis="both"` for a table or other surface whose meaning requires two dimensions. The slotted Table keeps its own caption, headers, cells, and focus behavior. ScrollArea only supplies the bounded native viewport. ### A semantic table at narrow width [Open the rendered preview](/ui-library/components/scroll-area/_previews/wide-table/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaWideTable(Component): template = """

    Quarterly service results

    The Table keeps its caption and headers. ScrollArea only bounds the two-dimensional viewport.

    Latency and availability by quarter
    Service Q1 latency Q2 latency Q3 latency Q4 latency Availability
    Accounts 112 ms104 ms98 ms 91 ms99.99%
    Ledger 190 ms172 ms160 ms 151 ms99.97%
    Search 86 ms81 ms74 ms 69 ms99.95%
    Archive 244 ms231 ms218 ms 205 ms99.90%
    Reports 155 ms149 ms141 ms 134 ms99.96%

    This fixture supplies its own compact print table so the final column fits inside the physical page.

    """ css = """ :where(.scroll-area-wide-table) { display: grid; gap: 0.75rem; inline-size: min(100%, 42rem); min-inline-size: 0; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-wide-table h2, .scroll-area-wide-table p) { margin: 0; } :where(.scroll-area-wide-table > button) { justify-self: start; } :where(.scroll-area-wide-table__table) { inline-size: 52rem; border-collapse: collapse; } :where(.scroll-area-wide-table__table caption) { padding: 0.75rem; font-weight: 700; text-align: start; } :where(.scroll-area-wide-table__table th, .scroll-area-wide-table__table td) { min-inline-size: 7rem; padding: 0.625rem; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); text-align: start; } :where(.scroll-area-wide-table__table thead th) { background: color-mix(in srgb, Highlight 12%, Canvas); } @media print { :where(.scroll-area-wide-table) { inline-size: 100%; } :where(.scroll-area-wide-table__table) { inline-size: 100%; table-layout: fixed; font-size: 8pt; } :where(.scroll-area-wide-table__table th, .scroll-area-wide-table__table td) { min-inline-size: 0; padding: 0.2rem; overflow-wrap: anywhere; } } """ preview = ScrollAreaWideTable() preview # noqa: B018 ```` At 400 percent zoom, prefer block flow unless two-dimensional content is essential. In print, ScrollArea removes its own maximum size, border, and overflow clipping. An application must still reflow, scale, rotate, or replace content that is wider than the physical page. ## Change native overflow policy `axis` accepts logical `block`, `inline`, or `both`. `scrollbar_width` accepts `auto` or `thin`. `scrollbar_gutter` accepts `auto`, `stable`, or `stable-both-edges`. Native scrollbar thickness, overlay behavior, and gutter pixels remain browser and operating-system choices. `overscroll="contain"` limits native scroll chaining on enabled axes, while `none` also requests suppression of local boundary effects. These are CSS policies, not promises that every browser, device, or synthetic event delivers the same gesture behavior. The policies follow [CSS Overflow](https://drafts.csswg.org/css-overflow/), [CSS Scrollbars](https://drafts.csswg.org/css-scrollbars/), and [CSS Overscroll Behavior](https://drafts.csswg.org/css-overscroll-1/). ### Reactive axis and scrollbar policy [Open the rendered preview](/ui-library/components/scroll-area/_previews/configuration/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaConfiguration(Component): template = """
    Record 01Identity reviewApproved Record 02Ledger reviewPending Record 03Archive reviewApproved Record 04Search reviewPending Record 05Report reviewApproved Record 06Export reviewPending
    Requested: block, auto, auto, auto
    """ css = """ :where(.scroll-area-configuration) { display: grid; gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-configuration__controls) { display: flex; flex-wrap: wrap; gap: 0.75rem; } :where(.scroll-area-configuration__controls label) { display: grid; gap: 0.25rem; } :where(.scroll-area-configuration__content) { display: grid; grid-template-columns: repeat(3, minmax(9rem, 1fr)); gap: 1px; inline-size: 38rem; min-block-size: 18rem; background: color-mix(in srgb, CanvasText 18%, transparent); } :where(.scroll-area-configuration__content span) { padding: 0.75rem; background: Canvas; } :where(.scroll-area-configuration__actions) { display: flex; flex-wrap: wrap; gap: 0.75rem; } """ preview = ScrollAreaConfiguration() preview # noqa: B018 ```` Client `axis`, `scrollbarWidth`, `scrollbarGutter`, and `overscroll` values win field by field. `null` or omission releases one field to its latest server fallback. An invalid value keeps the last valid effective value and reports one diagnostic for that invalid episode. The root owns instantaneous `scroll-behavior: auto` for direction, disabled-axis, and morph repair. An application can still request a smooth native movement in an explicit `scrollTo()` call, but it cannot replace the root's computed CSS policy. ## Read logical RTL offsets `onScrollChange` receives logical distance from inline start and block distance from the top. RTL callers do not need to interpret a negative browser `scrollLeft`. The detail describes the callback instant only and does not claim persistent edge or progress state. Raw viewport geometry and native events follow [CSSOM View](https://drafts.csswg.org/cssom-view/). ### LTR and RTL logical offsets [Open the rendered preview](/ui-library/components/scroll-area/_previews/rtl-and-direction/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaRtlAndDirection(Component): template = """
    Third rail direction: ltr

    LTR

    PlanBuildReview ApprovePublishArchive
    Logical offset 0

    RTL

    تخطيطبناءمراجعة موافقةنشرأرشفة
    Logical offset 0

    Direction change

    NorthSouthEast WestCoastHarbor
    Logical offset 0
    """ css = """ :where(.scroll-area-direction) { display: grid; gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-direction__controls) { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; } :where(.scroll-area-direction__grid) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; } :where(.scroll-area-direction article) { display: grid; gap: 0.5rem; min-inline-size: 0; } :where(.scroll-area-direction h3) { margin: 0; } :where(.scroll-area-direction__rail) { display: flex; inline-size: max-content; gap: 0.75rem; padding: 1rem; } :where(.scroll-area-direction__rail span) { min-inline-size: 7rem; padding: 0.625rem; border-radius: 0.5rem; background: color-mix(in srgb, Highlight 12%, Canvas); text-align: center; } """ preview = ScrollAreaRtlAndDirection() preview # noqa: B018 ```` A direction change preserves the last cached logical distance when the same root remains connected. Stylesheet-only direction changes are reconciled at the next native scroll, configuration update, or Citry morph settlement. Vertical writing modes keep usable native overflow but suspend normalized callbacks and lifecycle repair. ## Nest independent scrolling regions Nested ScrollAreas remain ordinary nested native scroll containers. The browser decides which area receives a gesture. Give nested named regions distinct useful names, and leave incidental regions unnamed. ### Nested regions and overscroll policy [Open the rendered preview](/ui-library/components/scroll-area/_previews/nested-areas/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaNestedAreas(Component): template = """

    Operations document

    The outer document and inner inspector are separate native scroll containers. Tab order and gesture targeting stay with the browser.

    The deployment plan contains enough content to scroll before and after the nested inspector.

    Review the service boundary, owner, and current policy before continuing to the approval section.

    Service
    Ledger export
    Owner
    Finance platform
    Region
    Central Europe
    Status
    Needs approval
    Retention
    Seven years
    Encryption
    Customer managed
    Review
    Quarterly
    PlanBuildReview ApproveRelease

    Continue through the remaining deployment notes after leaving the inspector.

    The outer viewport does not register the inner viewport as a widget or arbitrate its gestures.

    Real wheel, precision trackpad, and touch behavior remains a platform acceptance check.

    """ css = """ :where(.scroll-area-nested) { display: grid; gap: 0.75rem; max-inline-size: 40rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-nested h2, .scroll-area-nested p) { margin: 0; } :where(.scroll-area-nested > button) { justify-self: start; } :where(.scroll-area-nested__document) { display: grid; gap: 1.5rem; padding: 1rem; } :where(.scroll-area-nested__inspector) { display: grid; grid-template-columns: max-content 1fr; gap: 0.625rem 1rem; margin: 0; padding: 1rem; } :where(.scroll-area-nested__inspector dt) { font-weight: 700; } :where(.scroll-area-nested__inspector dd) { margin: 0; } :where(.scroll-area-nested__rail) { display: flex; inline-size: max-content; gap: 0.75rem; padding: 1rem; } :where(.scroll-area-nested__rail span) { min-inline-size: 7rem; padding: 0.5rem; background: color-mix(in srgb, Highlight 12%, Canvas); } """ preview = ScrollAreaNestedAreas() preview # noqa: B018 ```` ## Distinguish the component callback from native events `onScrollChange` is a semantic component callback supplied through `$c-props`. It runs at most once per animation frame after one or more actual native `scroll` events. It receives the latest native event as `detail.source`. Content resize, image load, configuration changes, and component-owned repairs do not create this callback. ### Event-scoped logical scroll details [Open the rendered preview](/ui-library/components/scroll-area/_previews/native-callback/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaNativeCallback(Component): template = """
    Absolute marker

    Open ShadowRoot content changes native layout without creating a component callback.

    Native scroll events
    0
    Native scrollend events
    0
    Component callbacks
    0
    Logical inline offset
    0
    Block offset
    0
    """ css = """ :where(.scroll-area-callback) { display: grid; gap: 1rem; max-inline-size: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-callback__controls) { display: flex; flex-wrap: wrap; gap: 0.75rem; } :where(.scroll-area-callback__content) { position: relative; inline-size: 44rem; min-block-size: 26rem; padding: 1rem; } :where(.scroll-area-callback__content--expanded) { min-block-size: 34rem; } :where(.scroll-area-callback__content p) { margin: 0 0 1rem; } :where(.scroll-area-callback__sentinel) { position: absolute; inset-inline-start: 28rem; padding: 0.375rem 0.625rem; border-radius: 0.375rem; background: color-mix(in srgb, Highlight 18%, Canvas); } :where(.scroll-area-callback__image) { display: block; inline-size: 30rem; block-size: 6rem; margin-block: 1rem; background: color-mix(in srgb, Highlight 14%, Canvas); } :where(.scroll-area-callback__shadow-host) { display: block; min-inline-size: 26rem; min-block-size: 5rem; border: 1px dashed GrayText; } :where(.scroll-area-callback__readout) { display: grid; grid-template-columns: max-content 1fr; gap: 0.375rem 1rem; margin: 0; } :where(.scroll-area-callback__readout dt) { font-weight: 700; } :where(.scroll-area-callback__readout dd) { margin: 0; } """ preview = ScrollAreaNativeCallback() preview # noqa: B018 ```` Native root events remain Alpine listeners in `attrs`: ```citry-html
    ...
    ``` Native listeners observe every browser event, including an event produced by component-owned coordinate repair. ScrollArea dispatches no custom DOM event and exposes no public method. A listener on a component root has Citry's isolated component scope, so it cannot read ancestor-local `x-data` identifiers directly. Use `$event`, `$dispatch`, `$store`, or another explicit global bridge; use `onScrollChange` for owner-local callback state. Application controls can use an ordinary DOM ref and the native `scrollTo()` or `scrollBy()` method. ## Customize standards-based styling Public variables control the viewport's size, colors, border, radius, padding, focus ring, scroll padding, and complete standard `scrollbar-color` value. The one stable selector targets the same native viewport. ### Public variables and the viewport selector [Open the rendered preview](/ui-library/components/scroll-area/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaCustomization(Component): template = """

    Orchard notes

    Pear block: pollinator rows checked.

    North field: irrigation pressure normal.

    West field: pruning review scheduled.

    Harvest window: seven days remaining.

    Cold store: capacity confirmed.

    Harbor notes

    North berth: loading complete.

    East pier: tide window confirmed.

    Customs desk: manifest approved.

    Harbor pilot: departure booked.

    Weather station: visibility clear.

    """ css = """ :where(.scroll-area-customization) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-brand) { display: grid; gap: 0.75rem; padding: 1rem; border-radius: 1rem; } :where(.scroll-area-brand h3) { margin: 0; } :where(.scroll-area-brand--orchard) { background: #f5f0df; color: #203422; --cui-scroll-area-max-block-size: 10rem; --cui-scroll-area-background: #fffdf5; --cui-scroll-area-foreground: #203422; --cui-scroll-area-border-color: #78916d; --cui-scroll-area-focus-color: #315f37; --cui-scroll-area-radius: 1rem; } :where(.scroll-area-brand--harbor) { background: #102b38; color: #eefaff; --cui-scroll-area-max-block-size: 10rem; --cui-scroll-area-background: #173c4c; --cui-scroll-area-foreground: #eefaff; --cui-scroll-area-border-color: #72b5ce; --cui-scroll-area-focus-color: #c6ecff; --cui-scroll-area-scrollbar-color: #9eddf4 #173c4c; } .scroll-area-brand .brand-scroll[data-citry-ui-part="scroll-area"] { border-width: 2px; } :where(.scroll-area-customization__notes) { display: grid; gap: 0.75rem; padding: 1rem; } :where(.scroll-area-customization__notes p) { margin: 0; } @media (forced-colors: active) { :where(.scroll-area-brand) { border: 1px solid CanvasText; } } @media print { :where(.scroll-area-brand) { background: transparent; color: black; } } """ preview = ScrollAreaCustomization() preview # noqa: B018 ```` Citry uses `scrollbar-width`, `scrollbar-color`, and `scrollbar-gutter`. Vendor scrollbar pseudo-elements are not public API. Forced colors restore platform scrollbar, border, and focus colors. Unlayered application rules override the Citry UI theme layer whether loaded before or after the component stylesheet. A named application layer must be ordered after `citry-ui.theme`. ## Respect the clipping boundary Native overflow clips ordinary positioned descendants. A dropdown, tooltip, or menu cannot escape merely because it appears in the default slot. Compose a supported Citry overlay or native top-layer element whose own contract defines its host, focus, and layering. ### Clipped content and an independently owned overlay [Open the rendered preview](/ui-library/components/scroll-area/_previews/overlay-boundary/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaOverlayBoundary(Component): template = """

    Credential review

    The red sample is ordinary positioned content and clips at the viewport. The Popover follows its own anchored-layer contract.

    Ordinary positioned note

    Confirm the token owner and intended service boundary.

    Review the current scopes before granting another permission.

    Open scope help Credential scope Grant only the permissions this worker needs.

    Record the approval before rotating the credential.

    Archive the previous key after the overlap window closes.

    """ css = """ :where(.scroll-area-overlay-boundary) { display: grid; gap: 0.75rem; max-inline-size: 38rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-overlay-boundary h2, .scroll-area-overlay-boundary p) { margin: 0; } :where(.scroll-area-overlay-boundary__content) { position: relative; display: grid; gap: 1rem; min-block-size: 22rem; padding: 1rem; } :where(.scroll-area-overlay-boundary__clipped) { position: absolute; inset-block-start: 1rem; inset-inline-end: -5rem; inline-size: 8rem; padding: 0.5rem; border: 2px solid #b42318; background: Canvas; color: #b42318; } """ preview = ScrollAreaOverlayBoundary() preview # noqa: B018 ```` ScrollArea does not register as an overlay owner, lock page scroll, make siblings inert, or create a stacking context. ## Preserve only a retained root A correlated Citry morph that retains the same root preserves valid client configuration, cached logical position, and focus on that root. Incoming server values become new fallbacks for fields without client ownership. ### Retained-root morph and replacement scope [Open the rendered preview](/ui-library/components/scroll-area/_previews/lifecycle/) ````citry from __future__ import annotations import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ScrollAreaLifecycle(Component): class Kwargs: step: int = 0 replacement: int = 0 class Slots: pass class Events: def refresh(self) -> ScrollAreaLifecycle: return ScrollAreaLifecycle(step=1, replacement=0) def replace(self) -> ScrollAreaLifecycle: return ScrollAreaLifecycle(step=2, replacement=1) def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, object]: # noqa: ARG002 return { "root_key": f"scroll-area-lifecycle-{kwargs.replacement}", "step": kwargs.step, } template = """

    Server step: {{ step }}

    Last user scroll offset 0
    """ css = """ :where(.scroll-area-lifecycle) { display: grid; gap: 1rem; max-inline-size: 40rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.scroll-area-lifecycle__controls) { display: flex; flex-wrap: wrap; gap: 0.75rem; } :where(.scroll-area-lifecycle p) { margin: 0; } :where(.scroll-area-lifecycle__content) { display: grid; grid-template-columns: repeat(2, minmax(16rem, 1fr)); gap: 1rem; inline-size: 42rem; min-block-size: 24rem; padding: 1rem; } :where(.scroll-area-lifecycle__content p) { padding: 0.75rem; border-radius: 0.5rem; background: color-mix(in srgb, Highlight 10%, Canvas); } """ preview = ScrollAreaLifecycle() preview # noqa: B018 ```` A replacement root, even with the same authored ID, starts with native browser position. Removal cancels pending callbacks and lifecycle work. Restoring a new root does not inherit the removed instance's offsets or focus. ## Keep the native fallback useful Without JavaScript, server output is already one focusable native viewport with its configured axis, standard scrollbar, gutter, overscroll, colors, and slot content. A supplied name already emits the region and naming attribute. Client enhancement only adds reactive configuration, normalized callbacks, direction repair, and retained-root lifecycle behavior. ## Treat root attributes as trusted configuration `class_`, `style`, and `attrs` all target the native viewport. `attrs` accepts ordinary descriptive attributes, `dir`, language hints, nonreserved `data-*`, and native Alpine event listeners that respect the isolated scope boundary. It rejects values that replace the root ID, role, focusability, region name, part marker, reflected state, lifecycle, or owned scrolling policy. Slotted text and components follow Citry's normal trusted content boundary. ScrollArea does not evaluate content as HTML, URLs, selectors, or Alpine expressions. ## API reference ### Inputs #### CScrollArea server inputs Server inputs are passed in a template through `` or in Python through `CScrollArea(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `id` | `str | None` | generated | Sets the native viewport ID. | | `aria_label` | `str | None` | `None` | Adds a nonempty direct region name and the region role; mutually exclusive with aria_labelledby. | | `aria_labelledby` | `str | None` | `None` | Adds a validated IDREF-list region name and the region role; mutually exclusive with aria_label. | | `axis` | `"block" | "inline" | "both"` ([`CScrollAreaAxis`](#scroll-area-interface-axis)) | `"block"` | Selects logical native overflow axes. | | `scrollbar_width` | `"auto" | "thin"` ([`CScrollAreaScrollbarWidth`](#scroll-area-interface-scrollbar-width)) | `"auto"` | Selects the standard native scrollbar width policy without hiding it. | | `scrollbar_gutter` | `"auto" | "stable" | "stable-both-edges"` ([`CScrollAreaScrollbarGutter`](#scroll-area-interface-scrollbar-gutter)) | `"auto"` | Selects standard native scrollbar-space reservation. | | `overscroll` | `"auto" | "contain" | "none"` ([`CScrollAreaOverscroll`](#scroll-area-interface-overscroll)) | `"auto"` | Selects logical overscroll policy on enabled axes. | | `class_` | `CClassValue | None` ([`CClassValue`](#scroll-area-interface-class-value)) | `None` | Adds native viewport classes and merges them with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#scroll-area-interface-style-value)) | `None` | Adds native viewport styles and merges them with attrs before the owned scrolling policy. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed descriptive attributes and isolated-scope native listeners that may use event magics, dispatch, stores, or globals. |
    #### CScrollArea client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `axis` | `"block" | "inline" | "both"` ([`CScrollAreaAxis`](#scroll-area-interface-axis)) | Uses the latest server fallback; null has the same effect. | Controls logical overflow axes while valid. | | `scrollbarWidth` | `"auto" | "thin"` ([`CScrollAreaScrollbarWidth`](#scroll-area-interface-scrollbar-width)) | Uses the latest server fallback; null has the same effect. | Controls standard native scrollbar width policy while valid. | | `scrollbarGutter` | `"auto" | "stable" | "stable-both-edges"` ([`CScrollAreaScrollbarGutter`](#scroll-area-interface-scrollbar-gutter)) | Uses the latest server fallback; null has the same effect. | Controls standard native scrollbar-space reservation while valid. | | `overscroll` | `"auto" | "contain" | "none"` ([`CScrollAreaOverscroll`](#scroll-area-interface-overscroll)) | Uses the latest server fallback; null has the same effect. | Controls logical overscroll policy while valid. | | `onScrollChange` | `function` | Omission or null selects no component callback. | Receives one event-scoped normalized snapshot for the latest native scroll event in a frame. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CScrollArea slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `none` | Renders an empty focusable native viewport. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CScrollArea events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onScrollChange` | `(detail: CScrollAreaScrollDetail) => void` ([`CScrollAreaScrollDetail`](#scroll-area-interface-cscroll-area-scroll-detail)) | One or more actual native scroll events occur on the valid initialized viewport. | `{inlineOffset, blockOffset, source}` ([`CScrollAreaScrollDetail`](#scroll-area-interface-cscroll-area-scroll-detail)) | Runs at most once per animation frame with the latest event. Return values do not cancel native scrolling; controlled state does not exist. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CScrollArea CSS variables Apply these variables to `CScrollArea` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-scroll-area-max-block-size` | `length or none` | Maximum block size for block and both-axis viewports. | `20rem` | | `--cui-scroll-area-background` | `color` | Native viewport background. | `Canvas` | | `--cui-scroll-area-foreground` | `color` | Inherited viewport foreground. | `CanvasText` | | `--cui-scroll-area-border-color` | `color` | Native viewport border. | `color-mix(in srgb, currentColor 24%, transparent)` | | `--cui-scroll-area-border-width` | `length` | Native viewport border width. | `1px` | | `--cui-scroll-area-radius` | `length` | Native viewport corner radius. | `0.75rem` | | `--cui-scroll-area-padding` | `length` | Content inset inside the native viewport. | `0px` | | `--cui-scroll-area-scrollbar-color` | `complete scrollbar-color value` | Standard native thumb and track colors as one property value. | `auto` | | `--cui-scroll-area-focus-color` | `color` | Viewport focus-visible ring. | `#2563eb` | | `--cui-scroll-area-scroll-padding` | `length` | Native focus and anchor scroll padding. | `0px` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CScrollArea attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `id` | Native viewport div | `supplied or generated string` | Identifies the single component root and viewport. | | `tabindex` | Native viewport div | `"0"` | Places the viewport in sequential keyboard focus order. | | `role` | Native viewport div | `absent | "region"` | Present only when exactly one naming input is supplied. | | `aria-label` | Native viewport div | `string | absent` | Supplies the direct region name only when aria_label is used. | | `aria-labelledby` | Native viewport div | `IDREF list | absent` | Supplies the referenced region name only when aria_labelledby is used. | | `data-axis` | Native viewport div | `"block" | "inline" | "both"` ([`CScrollAreaAxis`](#scroll-area-interface-axis)) | Mirrors the effective logical axis policy. | | `data-scrollbar-width` | Native viewport div | `"auto" | "thin"` ([`CScrollAreaScrollbarWidth`](#scroll-area-interface-scrollbar-width)) | Mirrors the effective standard scrollbar width policy. | | `data-scrollbar-gutter` | Native viewport div | `"auto" | "stable" | "stable-both-edges"` ([`CScrollAreaScrollbarGutter`](#scroll-area-interface-scrollbar-gutter)) | Mirrors the effective standard gutter policy. | | `data-overscroll` | Native viewport div | `"auto" | "contain" | "none"` ([`CScrollAreaOverscroll`](#scroll-area-interface-overscroll)) | Mirrors the effective logical overscroll policy. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CScrollArea selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="scroll-area"]` | Native viewport div | The focusable scroll viewport and class_, style, and attrs destination. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CScrollAreaAxis` | `Literal["block", "inline", "both"]` | | `CScrollAreaScrollbarWidth` | `Literal["auto", "thin"]` | | `CScrollAreaScrollbarGutter` | `Literal["auto", "stable", "stable-both-edges"]` | | `CScrollAreaOverscroll` | `Literal["auto", "contain", "none"]` |
    #### `CScrollAreaScrollDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `inlineOffset` | `float` | - | Logical horizontal distance from inline start, clamped to the current native range. | | `blockOffset` | `float` | - | Vertical distance from the top, clamped to the current native range. | | `source` | `Event` | - | Latest native scroll event coalesced into this callback frame. |
    ### Translation keys - --- # Splitter Source: https://citry.dev/ui-library/components/splitter/ # Splitter Use `CSplitter` when adjacent regions need user-adjustable space. Every `CSplitterPanel` has stable identity, an accessible name, and percentage constraints. Persist accepted sizes in application state through `onResizeEnd` when a layout should survive navigation. ## Splitter at a glance ### Splitter at a glance [Open the rendered preview](/ui-library/components/splitter/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SplitterAtAGlance(Component): template = """ Navigation

    Projects, files, and saved views.

    Workspace

    Resize with the separator or its Arrow keys.

    """ preview = SplitterAtAGlance() preview # noqa: B018 ```` ## Resize multiple panels ### Resize three panels [Open the rendered preview](/ui-library/components/splitter/_previews/multiple/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class MultiplePanels(Component): template = """ Outline Editor Preview """ preview = MultiplePanels() preview # noqa: B018 ```` ## Stack and nest Splitters ### Stack and nest Splitters [Open the rendered preview](/ui-library/components/splitter/_previews/vertical-nested/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class VerticalNested(Component): template = """ Header preview Source Result """ preview = VerticalNested() preview # noqa: B018 ```` ## Constrain keyboard resizing Arrow keys move by `keyboard_step` percentage points, Shift uses four times the step, and Home or End reaches the adjacent pair constraint. ### Constrain panel sizes [Open the rendered preview](/ui-library/components/splitter/_previews/constraints-keyboard/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ConstrainedSplitter(Component): template = """ Focus the separator. Arrow keys move 5%; Shift moves 20%; Home and End use the limits. Canvas """ preview = ConstrainedSplitter() preview # noqa: B018 ```` ## Control and persist sizes Client `sizes` is controlled while supplied. The owner accepts resize requests by updating the vector and can persist the final vector from `onResizeEnd`. ### Control Splitter sizes [Open the rendered preview](/ui-library/components/splitter/_previews/controlled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledSplitter(Component): template = """
    Filters Results
    """ preview = ControlledSplitter() preview # noqa: B018 ```` ## Disable resizing ### Disable Splitter [Open the rendered preview](/ui-library/components/splitter/_previews/disabled/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class DisabledSplitter(Component): template = """
    Locked layout Summary Details
    """ preview = DisabledSplitter() preview # noqa: B018 ```` ## Customize Splitter ### Customize Splitter [Open the rendered preview](/ui-library/components/splitter/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CustomizedSplitter(Component): template = """
    Branded index Branded article
    """ preview = CustomizedSplitter() preview # noqa: B018 ```` ## Accessibility and behavior Each resize handle is a focusable ARIA separator with its current percentage, allowed range, physical orientation, and the IDs of its adjacent panels. Side-by-side layouts use Left and Right; stacked layouts use Up and Down. Pointer and keyboard interaction change only the adjacent pair, preserving its combined size. Controls inside panels retain their ordinary form behavior. ## API reference ### Inputs #### CSplitter server inputs Server inputs are passed in a template through `` or in Python through `CSplitter(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `sizes` | `Sequence[int | float] | None` | `None` | Sets initial percentage sizes totaling 100; omission divides space equally. | | `orientation` | `"horizontal" | "vertical"` ([`CSplitterOrientation`](#splitter-interface-csplitter-orientation)) | `"horizontal"` | Places panels side by side or stacked. | | `disabled` | `bool` | `False` | Disables every resize handle. | | `keyboard_step` | `float` | `2` | Sets Arrow-key movement in percentage points. | | `variant` | `"plain" | "soft" | "outline"` ([`CSplitterVariant`](#splitter-interface-csplitter-variant)) | `"plain"` | Selects surface treatment. | | `size` | `"sm" | "md" | "lg"` ([`CSplitterSize`](#splitter-interface-csplitter-size)) | `"md"` | Selects handle geometry. | | `class_` | `CClassValue | None` ([`CClassValue`](#splitter-interface-csplitter-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#splitter-interface-csplitter-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted root attributes without replacing owned structure state or runtime. |
    #### CSplitter client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `sizes` | `number[] | null` | Uses uncontrolled committed sizes. | Controls percentages while supplied; null releases control. | | `orientation` | `"horizontal" | "vertical"` ([`CSplitterOrientation`](#splitter-interface-csplitter-orientation)) | Uses the server value. | Reactively changes layout and keyboard axis. | | `disabled` | `bool` | Uses the server value. | Reactively disables resizing. | | `keyboardStep` | `number` | Uses the server value. | Reactively changes Arrow-key movement. | | `variant` | `"plain" | "soft" | "outline"` ([`CSplitterVariant`](#splitter-interface-csplitter-variant)) | Uses the server value. | Reactively changes surface treatment. | | `size` | `"sm" | "md" | "lg"` ([`CSplitterSize`](#splitter-interface-csplitter-size)) | Uses the server value. | Reactively changes handle geometry. | | `onResizeStart` | `((detail: CSplitterResizeDetail) => void) | undefined` | No component callback runs. | Receives the beginning of a pointer or keyboard transaction. | | `onResize` | `((sizes: number[], detail: CSplitterResizeDetail) => void) | undefined` | No component callback runs. | Receives each valid adjacent-pair resize request. | | `onResizeEnd` | `((sizes: number[], detail: CSplitterResizeDetail) => void) | undefined` | No component callback runs. | Receives the settled end of a resize transaction. |
    #### CSplitterPanel server inputs Server inputs are passed in a template through `` or in Python through `CSplitterPanel(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `id` | `str` | required | Supplies stable panel identity and relationship targets. | | `label` | `str` | required | Supplies the panel and adjacent separator accessible names. | | `min_size` | `float` | `10` | Sets the minimum percentage. | | `max_size` | `float` | `100` | Sets the maximum percentage. | | `class_` | `CClassValue | None` ([`CClassValue`](#splitter-interface-csplitter-class-value)) | `None` | Adds classes to the concrete panel. | | `style` | `CStyleValue | None` ([`CStyleValue`](#splitter-interface-csplitter-style-value)) | `None` | Adds inline styles to the concrete panel. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted panel attributes without replacing owned semantics identity or sizing. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CSplitter slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CSplitterDefaultSlotData`](#splitter-interface-csplitter-default-slot-data)) | None. Requires two or more direct CSplitterPanel declarations. |
    #### CSplitterPanel slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{id, index, size, is_first, is_last}` ([`CSplitterPanelDefaultSlotData`](#splitter-interface-csplitter-panel-default-slot-data)) | None. Supplies panel content. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CSplitter events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onResizeStart` | `(detail: CSplitterResizeDetail) => void` ([`CSplitterResizeDetail`](#splitter-interface-csplitter-resize-detail)) | Pointerdown or an accepted keyboard resize. | `{sizes, previousSizes, handleIndex, controlled, source, sourceEvent}` ([`CSplitterResizeDetail`](#splitter-interface-csplitter-resize-detail)) | Begins a resize transaction. | | `onResize` | `(sizes: number[], detail: CSplitterResizeDetail) => void` ([`CSplitterResizeDetail`](#splitter-interface-csplitter-resize-detail)) | Each accepted pointer or keyboard resize request. | `{sizes, previousSizes, handleIndex, controlled, source, sourceEvent}` ([`CSplitterResizeDetail`](#splitter-interface-csplitter-resize-detail)) | Commits immediately when uncontrolled and waits for owner acceptance when controlled. | | `onResizeEnd` | `(sizes: number[], detail: CSplitterResizeDetail) => void` ([`CSplitterResizeDetail`](#splitter-interface-csplitter-resize-detail)) | Pointerup pointercancel disability or an accepted keyboard resize. | `{sizes, previousSizes, handleIndex, controlled, source, sourceEvent}` ([`CSplitterResizeDetail`](#splitter-interface-csplitter-resize-detail)) | Ends the transaction and is the persistence composition point. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CSplitter CSS variables Apply these variables to `CSplitter` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-splitter-min-block-size` | `length` | Minimum root block size. | `12rem` | | `--cui-splitter-radius` | `length` | Root corner radius. | `0.75rem` | | `--cui-splitter-background` | `color` | Root background. | `plain and outline transparent; soft subtle CanvasText mix` | | `--cui-splitter-border-color` | `color` | Outline root border. | `light #d0d5dd; dark #535862` | | `--cui-splitter-handle-size` | `length` | Handle hit-area thickness. | `sm 0.5rem; md 0.75rem; lg 1rem` | | `--cui-splitter-handle-color` | `color` | Inactive line and grip. | `light #98a2b3; dark #717680` | | `--cui-splitter-handle-active-color` | `color` | Hover and active grip. | `light #175cd3; dark #84adff` | | `--cui-splitter-focus-color` | `color` | Keyboard focus outline. | `Highlight` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CSplitter attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Panel or separator div | `group | separator` | Gives panels and resize handles their owned semantics. | | `tabindex` | Separator div | `0 | -1` | Includes enabled handles in Tab order and removes disabled handles. | | `aria-label` | Panel or separator div | `string` | Names each panel and each adjacent-pair handle. | | `aria-disabled` | Separator div | `true | false` | Reflects effective resize availability. | | `data-orientation` | Root div | `horizontal | vertical` | Mirrors effective layout. | | `data-disabled` | Root or handle | `present-or-absent` | Reflects effective resizing unavailability. | | `data-resizing` | Root div | `present-or-absent` | Present during pointer resizing. | | `data-variant` | Root div | `plain | soft | outline` | Mirrors effective surface treatment. | | `data-size` | Root div | `sm | md | lg` | Mirrors effective geometry. | | `data-panel-id` | Panel div | `string` | Exposes canonical panel identity. | | `data-index` | Panel div | `nonnegative-integer-string` | Exposes settled order. | | `data-size-percent` | Panel div | `number-string` | Mirrors effective percentage. | | `data-min-size` | Panel div | `number-string` | Exposes minimum percentage. | | `data-max-size` | Panel div | `number-string` | Exposes maximum percentage. | | `data-handle-index` | Separator div | `nonnegative-integer-string` | Identifies the adjacent pair. | | `data-active` | Separator div | `present-or-absent` | Present during its pointer transaction. | | `aria-controls` | Separator div | `IDREF-list` | Identifies both adjacent panels. | | `aria-orientation` | Separator div | `vertical | horizontal` | Exposes physical separator orientation. | | `aria-valuemin` | Separator div | `number-string` | Exposes pair minimum. | | `aria-valuemax` | Separator div | `number-string` | Exposes pair maximum. | | `aria-valuenow` | Separator div | `number-string` | Exposes the preceding panel percentage. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CSplitter selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="splitter"]` | Root div | Stable root and attrs destination. | | `[data-citry-ui-part="panel"]` | Panel div | Stable content and panel attrs destination. | | `[data-citry-ui-part="handle"]` | Separator div | Stable focusable resize control. | | `[data-citry-ui-part="handle-line"]` | Decorative span | Stable separator line. | | `[data-citry-ui-part="handle-grip"]` | Decorative span | Stable resize affordance. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CSplitterOrientation` | `Literal["horizontal", "vertical"]` | | `CSplitterVariant` | `Literal["plain", "soft", "outline"]` | | `CSplitterSize` | `Literal["sm", "md", "lg"]` | | `CSplitterResizeSource` | `Literal["pointer", "keyboard"]` |
    #### `CSplitterDefaultSlotData` Empty dataclass: `{}`. #### `CSplitterPanelDefaultSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `id` | `str` | - | Canonical panel identity. | | `index` | `int` | - | Zero-based settled panel index. | | `size` | `float` | - | Server-rendered percentage. | | `is_first` | `bool` | - | Whether this is the first panel. | | `is_last` | `bool` | - | Whether this is the last panel. |
    #### `CSplitterResizeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `sizes` | `number[]` | - | Requested effective vector. | | `previousSizes` | `number[]` | - | Vector before this transaction step. | | `handleIndex` | `int` | - | Zero-based changed separator index. | | `controlled` | `bool` | - | Whether client sizes currently controls state. | | `source` | `"pointer" | "keyboard"` ([`CSplitterResizeSource`](#splitter-interface-csplitter-resize-source)) | - | Interaction source. | | `sourceEvent` | `Event` | - | Native PointerEvent or KeyboardEvent. |
    ### Translation keys - --- # Avatar Source: https://citry.dev/ui-library/components/avatar/ # Avatar Use `CAvatar` for a compact image identity. Supply an explicit accessible name, then choose an image, authored fallback, or built-in generic silhouette. ## Avatar at a glance ### Avatar at a glance [Open the rendered preview](/ui-library/components/avatar/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarAtAGlance(Component): template = """

    Moonfen field guide

    Night expedition

    MVMira
    OMOrrin
    Guide
    """ css = """ :where(.avatar-guide) { max-inline-size: 28rem; padding: 1.25rem; border: 1px solid light-dark(#a8c7b5, #426151); border-radius: 0.9rem; background: light-dark(#f4fbf6, #15241c); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.avatar-guide h2, .avatar-guide p) { margin: 0; } :where(.avatar-guide h2) { margin-block: 0.2rem 1rem; font-size: 1.1rem; } :where(.avatar-guide__eyebrow) { color: light-dark(#35624b, #a9d7bc); font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } :where(.avatar-guide__row) { display: flex; gap: 1rem; } :where(.avatar-guide__row > div) { display: grid; justify-items: center; gap: 0.35rem; font-size: 0.8rem; } """ preview = AvatarAtAGlance() preview # noqa: B018 ```` ## Choose images and fallbacks `src` shows one image. The default slot remains behind it and appears when the source is absent or fails. Without a slot, Avatar uses a generic silhouette. ### Compare image and fallback paths [Open the rendered preview](/ui-library/components/avatar/_previews/images-and-fallbacks/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) PORTRAIT = ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80'%3E" "%3Crect width='80' height='80' fill='%23365f50'/%3E" "%3Ccircle cx='40' cy='31' r='14' fill='%23f4d6b0'/%3E" "%3Cpath d='M13 80c4-22 15-32 27-32s23 10 27 32' fill='%238fc5a8'/%3E%3C/svg%3E" ) class AvatarImages(Component): class Kwargs: pass class Slots: pass template = """
    FCLoaded
    MS Error fallback
    Generic fallback
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"portrait": PORTRAIT} css = """ :where(.avatar-image-grid) { display: flex; flex-wrap: wrap; gap: 1rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.avatar-image-grid > div) { display: grid; justify-items: center; gap: 0.35rem; color: light-dark(#315546, #b7ddc8); font-size: 0.75rem; } """ preview = AvatarImages() preview # noqa: B018 ```` ```citry-html MV ``` Python composition uses the same surface: ```python from citry_ui import CAvatar avatar = CAvatar(src="/portraits/mira.jpg", alt="Mira Vale") ``` ## Provide an accessible name Use `alt` for the identity conveyed by Avatar. An empty value is deliberately decorative. The internal image never duplicates the root name. ### Compare named and decorative Avatars [Open the rendered preview](/ui-library/components/avatar/_previews/accessible-names/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarNames(Component): template = """
    MVNamed identity
    Decorative companion
    """ css = """ :where(.avatar-name-list) { display: grid; gap: 0.75rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.avatar-name-list > div) { display: flex; align-items: center; gap: 0.75rem; } """ preview = AvatarNames() preview # noqa: B018 ```` ## Choose appearance Variants style the fallback. Sizes and shapes control the fixed visual frame. ### Compare Avatar variants and sizes [Open the rendered preview](/ui-library/components/avatar/_previews/variants-and-sizes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarVariants(Component): class Kwargs: pass class Slots: pass template = """
    {{ variant }} MF
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"variants": ("soft", "solid", "outline"), "sizes": ("sm", "md", "lg")} css = """ :where(.avatar-variants) { display: grid; gap: 0.8rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.avatar-variants > div) { display: flex; align-items: center; gap: 0.65rem; } :where(.avatar-variants strong) { inline-size: 4.5rem; color: light-dark(#315546, #b7ddc8); font-size: 0.75rem; text-transform: capitalize; } """ preview = AvatarVariants() preview # noqa: B018 ```` ### Compare Avatar shapes [Open the rendered preview](/ui-library/components/avatar/_previews/shapes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarShapes(Component): class Kwargs: pass class Slots: pass template = """
    SG {{ shape }}
    """ def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return {"shapes": ("circle", "rounded", "square")} css = """ :where(.avatar-shapes) { display: flex; gap: 1rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.avatar-shapes > div) { display: grid; justify-items: center; gap: 0.35rem; font-size: 0.75rem; text-transform: capitalize; } """ preview = AvatarShapes() preview # noqa: B018 ```` ## Update the image in the browser Client inputs are passed through `$c-props="{...}"`. `src` accepts a URL or `null`; `onStatusChange` reports fallback, loading, loaded, and error states. ### Change an Avatar source [Open the rendered preview](/ui-library/components/avatar/_previews/reactive-sources/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarReactive(Component): template = """
    ML

    Status: fallback

    Try missing image Use fallback
    """ css = """ :where(.avatar-reactive) { display: grid; justify-items: start; gap: 0.75rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.avatar-reactive p) { margin: 0; font-size: 0.8rem; } :where(.avatar-reactive__actions) { display: flex; flex-wrap: wrap; gap: 0.5rem; } """ preview = AvatarReactive() preview # noqa: B018 ```` ## Compose adjacent UI Avatar does not own presence, badges, or overlapping groups. Compose those jobs with `CBadge`, `CRow`, and application layout. ### Compose Avatar with badges and groups [Open the rendered preview](/ui-library/components/avatar/_previews/composition/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarComposition(Component): template = """
    MV Ready
    OM SR TW
    """ css = """ :where(.avatar-party) { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; } :where(.avatar-party__member) { display: flex; align-items: center; gap: 0.5rem; } :where(.avatar-party__group) { display: flex; padding-inline-start: 0.5rem; } :where(.avatar-party__group [data-citry-ui-part="avatar"]) { margin-inline-start: -0.5rem; border-color: Canvas; border-width: 2px; } """ preview = AvatarComposition() preview # noqa: B018 ```` ## Customize Avatar Override public variables on a scope or instance. Stable selectors target the root, fallback, and image without relying on private classes. ### Customize Avatar with public CSS [Open the rendered preview](/ui-library/components/avatar/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class AvatarCustomization(Component): template = """
    MR RO
    """ css = """ :where(.avatar-moonlit) { --cui-avatar-background: light-dark(#d9f1e4, #234738); --cui-avatar-foreground: light-dark(#174b35, #c9f4dd); --cui-avatar-border-color: light-dark(#4b8a69, #83c9a3); --cui-avatar-radius: 35% 65% 58% 42%; display: flex; gap: 0.75rem; } :where(.avatar-moonlit [data-citry-ui-part="fallback"]) { letter-spacing: 0.06em; } """ preview = AvatarCustomization() preview # noqa: B018 ```` ## Accessibility and loading behavior A nonempty `alt` makes the root one named image semantic. The internal HTML image and fallback are decorative, avoiding duplicate announcements. Empty `alt` makes the entire Avatar decorative. Avatar owns no focus or keyboard behavior. Failed images are hidden after client activation; the fallback remains mounted throughout loading. ## API reference ### Inputs #### CAvatar server inputs Server inputs are passed in a template through `` or in Python through `CAvatar(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `src` | `str | None` | `None` | Sets one escaped image URL. `None` shows the fallback only. | | `alt` | `str` | `""` | Names the Avatar as one image semantic. Empty text makes the Avatar decorative. | | `variant` | `"soft" | "solid" | "outline"` ([`CAvatarVariant`](#avatar-interface-variant)) | `"soft"` | Selects fallback visual emphasis. | | `size` | `"sm" | "md" | "lg"` ([`CAvatarSize`](#avatar-interface-size)) | `"md"` | Selects the size preset. | | `shape` | `"circle" | "rounded" | "square"` ([`CAvatarShape`](#avatar-interface-shape)) | `"circle"` | Selects clipping geometry. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#avatar-interface-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#avatar-interface-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted root attributes without replacing Avatar semantics, focus, children, reflections, or Citry runtime fields. | | `img_attrs` | `Mapping[str, object] | None` | `None` | Adds copied inert image attributes such as `loading`, `decoding`, and `referrerpolicy` without replacing source, alternative text, events, or ownership. |
    #### CAvatar client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `src` | `string | null` | Uses the server input. | Replaces the current image URL or switches to fallback-only output. | | `alt` | `string` | Uses the server input. | Updates the root accessible name; empty text makes it decorative. | | `variant` | `"soft" | "solid" | "outline"` ([`CAvatarVariant`](#avatar-interface-variant)) | Uses the server input. | Controls fallback visual emphasis. | | `size` | `"sm" | "md" | "lg"` ([`CAvatarSize`](#avatar-interface-size)) | Uses the server input. | Controls size. | | `shape` | `"circle" | "rounded" | "square"` ([`CAvatarShape`](#avatar-interface-shape)) | Uses the server input. | Controls clipping geometry. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CAvatar slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CAvatarDefaultSlotData`](#avatar-interface-default-slot-data)) | Generic decorative person silhouette. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CAvatar events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onStatusChange` | `(detail: {status: CAvatarStatus, src: string | null}) => void` | The committed image status changes. | `{status: "fallback" | "loading" | "loaded" | "error", src: string | null}` | Runs after the image visibility and root status reflection synchronize; return values do not cancel the transition. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CAvatar CSS variables Apply these variables to `CAvatar` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-avatar-size` | `length` | Root inline and block size. | `Size-derived 2rem, 2.5rem, or 3rem.` | | `--cui-avatar-background` | `color` | Fallback surface. | `Variant- and scheme-derived color.` | | `--cui-avatar-foreground` | `color` | Fallback text and icon foreground. | `Variant- and scheme-derived color.` | | `--cui-avatar-border-color` | `color` | Root boundary color. | `Transparent except outline.` | | `--cui-avatar-border-width` | `length` | Root boundary width. | `1px` | | `--cui-avatar-radius` | `length` | Root clipping radius. | `Shape-derived.` | | `--cui-avatar-font-size` | `length` | Authored fallback text size. | `Size-derived.` | | `--cui-avatar-font-weight` | `font-weight` | Authored fallback text emphasis. | `700` | | `--cui-avatar-image-fit` | `keyword` | Internal image object fit. | `cover` | | `--cui-avatar-image-position` | `position` | Internal image object position. | `center` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CAvatar attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-variant` | Root | `"soft" | "solid" | "outline"` | Mirrors effective fallback emphasis. | | `data-size` | Root | `"sm" | "md" | "lg"` | Mirrors effective size. | | `data-shape` | Root | `"circle" | "rounded" | "square"` | Mirrors effective clipping geometry. | | `data-status` | Root | `"fallback" | "loading" | "loaded" | "error"` ([`CAvatarStatus`](#avatar-interface-status)) | Mirrors the current image lifecycle state. | | `role` | Named root | `"img"` | Exposes the Avatar as one image semantic when `alt` is nonempty. | | `aria-label` | Named root | `string` | Uses the exact nonempty `alt` input as the Avatar name. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CAvatar selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="avatar"]` | Root span | Stable Avatar surface and `attrs` destination. | | `[data-citry-ui-part="fallback"]` | Decorative fallback wrapper | Authored or generic fallback styling. | | `[data-citry-ui-part="image"]` | Decorative image | Image presentation and `img_attrs` destination. |
    ### 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]` | | `CAvatarVariant` | `Literal["soft", "solid", "outline"]` | | `CAvatarSize` | `Literal["sm", "md", "lg"]` | | `CAvatarShape` | `Literal["circle", "rounded", "square"]` | | `CAvatarStatus` | `Literal["fallback", "loading", "loaded", "error"]` |
    #### `CAvatarDefaultSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Badge Source: https://citry.dev/ui-library/components/badge/ # Badge Use `CBadge` for short inline status, category, count, or metadata text. Badge is a visual label, not a Button, selectable Chip, removable Tag, or live announcement region. ## Badge at a glance ### Badge at a glance [Open the rendered preview](/ui-library/components/badge/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeAtAGlance(Component): template = """

    Mineral archive · specimen 184

    Azurite rosette

    Copper carbonate Verified 3 fragments
    """ css = """ :where(.badge-glance) { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 1rem; max-inline-size: 38rem; padding: 1.25rem; border: 1px solid light-dark(#b7c6cf, #526873); border-radius: 0.85rem; background: light-dark(#f5fbff, #17232a); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.badge-glance h2, .badge-glance p) { margin: 0; } :where(.badge-glance p) { color: light-dark(#496471, #a9c5d2); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } :where(.badge-glance h2) { margin-block-start: 0.25rem; font-size: 1.1rem; } """ preview = BadgeAtAGlance() preview # noqa: B018 ```` ## Compose a Badge The default slot supplies the visible meaning. It is required. ### Compose short inline labels [Open the rendered preview](/ui-library/components/badge/_previews/basic-badges/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BasicBadges(Component): template = """

    Fluorite New

    Cabinet 7 24

    Catalog record Draft

    """ css = """ :where(.badge-basic) { display: grid; gap: 0.75rem; max-inline-size: 24rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.badge-basic p) { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: 0; padding-block-end: 0.5rem; border-block-end: 1px solid light-dark(#d8d2c6, #4f4a42); } """ preview = BasicBadges() preview # noqa: B018 ```` ```citry-html Verified ``` Compose the same result in Python: ```python from citry_ui import CBadge verified = CBadge(intent="success", slots={"default": "Verified"}) ``` ## Carry meaning with text Intent selects a palette. The visible label must still explain the state, so the result remains understandable without color. ### Compare Badge intents [Open the rendered preview](/ui-library/components/badge/_previews/intents/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeIntents(Component): template = """ Unsorted In study Verified Handle carefully Restricted """ css = """ :where(.badge-intents) { max-inline-size: 34rem; padding: 1rem; border-radius: 0.75rem; background: light-dark(#f5f1e8, #25221e); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = BadgeIntents() preview # noqa: B018 ```` ## Choose visual emphasis Use `soft` for quiet metadata, `solid` for stronger emphasis, and `outline` when the surrounding surface should remain visible. ### Compare Badge variants [Open the rendered preview](/ui-library/components/badge/_previews/variants/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeVariants(Component): template = """ SoftLapis SolidLapis OutlineLapis """ css = """ :where(.badge-variants) { max-inline-size: 20rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.badge-variants > [data-citry-ui-part="row"]) { justify-content: space-between; padding: 0.75rem; border: 1px solid light-dark(#d4cabc, #514940); border-radius: 0.6rem; } """ preview = BadgeVariants() preview # noqa: B018 ```` ## Choose size and shape Sizes change compact type and spacing. Shape changes only the corner radius. ### Compare sizes and shapes [Open the rendered preview](/ui-library/components/badge/_previews/sizes-and-shapes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeSizesAndShapes(Component): template = """ Small Medium Large Rounded Pill """ css = """ :where(.badge-sizes) { max-inline-size: 28rem; padding: 1rem; border: 1px solid light-dark(#cbd5d9, #475a62); border-radius: 0.75rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = BadgeSizesAndShapes() preview # noqa: B018 ```` ## Add registered icons Use the `start` and `end` slots for short decorative content. Keep the default label meaningful without the icon. ### Add registered icons [Open the rendered preview](/ui-library/components/badge/_previews/icons/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeIcons(Component): template = """ Verified origin Requires gloves """ css = """ :where(.badge-icons) { max-inline-size: 30rem; padding: 1rem; background: light-dark(#f4f0e7, #29251f); color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } """ preview = BadgeIcons() preview # noqa: B018 ```` ## Give counts context A lone number is ambiguous. Put counts beside understandable owner text and include the count's meaning in the owner accessible name when needed. ### Present counts in context [Open the rendered preview](/ui-library/components/badge/_previews/counts-and-context/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeCountsAndContext(Component): template = """ """ css = """ :where(.badge-counts) { display: grid; gap: 0.375rem; max-inline-size: 22rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.badge-counts a) { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem; border-radius: 0.6rem; color: CanvasText; text-decoration: none; } :where(.badge-counts a:hover) { background: light-dark(#ece6da, #322d27); } """ preview = BadgeCountsAndContext() preview # noqa: B018 ```` Badge does not cap large values or hide zero. Format the slot content in your application so display and accessible context stay under one policy. ## Position a Badge around an owner Badge owns no positioning or overlap. Use ordinary CSS when a count belongs at the corner of a Button, Avatar, or other item. ### Position a Badge with consumer CSS [Open the rendered preview](/ui-library/components/badge/_previews/positioning/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgePositioning(Component): template = """
    Field notes 7
    """ css = """ :where(.badge-positioning) { min-block-size: 7rem; padding: 1.5rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.badge-positioning [data-citry-ui-part="button"]) { position: relative; } :where(.badge-positioning [data-citry-ui-part="badge"]) { position: absolute; inset-block-start: 0; inset-inline-end: 0; translate: 45% -45%; } """ preview = BadgePositioning() preview # noqa: B018 ```` ## Customize Badge Override public variables on an ancestor or one Badge. Stable part selectors support local geometry without relying on private classes. ### Customize Badge with public CSS [Open the rendered preview](/ui-library/components/badge/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BadgeCustomization(Component): template = """
    Quartz archive
    Basalt archive
    """ css = """ :where(.badge-themes > div) { padding: 1.25rem; border-radius: 0.75rem; } :where(.badge-themes__quartz) { --cui-badge-background: #f0e7ff; --cui-badge-foreground: #4c1d75; --cui-badge-radius: 0.2rem; background: #faf7ff; } :where(.badge-themes__basalt) { color-scheme: dark; --cui-badge-background: #1e2930; --cui-badge-foreground: #d7edf2; --cui-badge-border-color: #72a8b5; --cui-badge-radius: 999px; background: #10171b; } """ preview = BadgeCustomization() preview # noqa: B018 ```` ## Accessibility and behavior Badge renders a neutral, unfocusable `span` with no JavaScript. Do not place Buttons, links, inputs, or other controls inside it. Put Badge inside the interactive owner instead. Changing Badge text does not create a live announcement. Use a persistent status or Alert surface when a browser update must be announced. ## API reference ### Inputs #### CBadge server inputs Server inputs are passed in a template through `` or in Python through `CBadge(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `variant` | `"soft" | "solid" | "outline"` ([`CBadgeVariant`](#badge-interface-input-type-aliases-cbadge-variant)) | `"soft"` | Selects quiet fill, strong fill, or outlined emphasis. | | `intent` | `"neutral" | "primary" | "success" | "warn" | "danger"` ([`CBadgeIntent`](#badge-interface-input-type-aliases-cbadge-intent)) | `"neutral"` | Selects a visual palette; authored text must still carry status meaning. | | `size` | `"sm" | "md" | "lg"` ([`CBadgeSize`](#badge-interface-input-type-aliases-cbadge-size)) | `"md"` | Sets compact height, type, padding, icon size, and gap. | | `shape` | `"rounded" | "pill"` ([`CBadgeShape`](#badge-interface-input-type-aliases-cbadge-shape)) | `"rounded"` | Selects compact rounded or fully pill-shaped geometry. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#badge-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#badge-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied trusted native, data, and targeted Alpine root attributes without replacing Badge anatomy, semantics, or Citry runtime fields. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CBadge slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CBadgeDefaultSlotData`](#badge-interface-cbadge-default-slot-data)) | Missing fill raises before rendering. | | `start` | no | `{}` ([`CBadgeStartSlotData`](#badge-interface-cbadge-start-slot-data)) | Leading wrapper omitted. | | `end` | no | `{}` ([`CBadgeEndSlotData`](#badge-interface-cbadge-end-slot-data)) | Trailing wrapper omitted. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CBadge CSS variables Apply these variables to `CBadge` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-badge-background` | `color` | Root fill. | `Variant- and intent-derived color.` | | `--cui-badge-foreground` | `color` | Label and icon foreground. | `Contrast-checked variant and intent color.` | | `--cui-badge-border-color` | `color` | Root border. | `Variant-derived transparent or currentColor.` | | `--cui-badge-radius` | `length` | Root corner radius. | `Shape-derived 0.375rem or 999px.` | | `--cui-badge-min-height` | `length` | Compact minimum block size. | `Size-derived length.` | | `--cui-badge-padding-inline` | `length` | Root inline padding. | `Size-derived length.` | | `--cui-badge-gap` | `length` | Space between supplied slot wrappers. | `Size-derived length.` | | `--cui-badge-font-size` | `length` | Label font size. | `Size-derived length.` | | `--cui-badge-font-weight` | `font-weight` | Label weight. | `650` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CBadge attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-variant` | Root | `"soft" | "solid" | "outline"` | Reflects the selected emphasis treatment. | | `data-intent` | Root | `"neutral" | "primary" | "success" | "warn" | "danger"` | Reflects the selected visual palette. | | `data-size` | Root | `"sm" | "md" | "lg"` | Reflects compact geometry. | | `data-shape` | Root | `"rounded" | "pill"` | Reflects corner geometry. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CBadge selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="badge"]` | Native span root | Stable Badge root and `attrs` destination. | | `[data-citry-ui-part="start"]` | Optional leading wrapper | Leading icon/content layout. | | `[data-citry-ui-part="label"]` | Required label wrapper | Visible meaning-bearing content. | | `[data-citry-ui-part="end"]` | Optional trailing wrapper | Trailing icon/content layout. |
    ### 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]` | | `CBadgeVariant` | `Literal["soft", "solid", "outline"]` | | `CBadgeIntent` | `Literal["neutral", "primary", "success", "warn", "danger"]` | | `CBadgeSize` | `Literal["sm", "md", "lg"]` | | `CBadgeShape` | `Literal["rounded", "pill"]` |
    #### `CBadgeDefaultSlotData` Empty dataclass: `{}`. #### `CBadgeStartSlotData` Empty dataclass: `{}`. #### `CBadgeEndSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Card Source: https://citry.dev/ui-library/components/card/ # Card Use `CCard` to present one subject as a contained visual unit. Its sections are optional, so a Card can be a short note, a media object, or a complete summary with header and footer actions. ## Card at a glance ### Card at a glance [Open the rendered preview](/ui-library/components/card/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardAtAGlance(Component): template = """

    Sunroom

    Window reading chair

    Oak arms, woven rush, and a linen cushion for slow afternoons. Natural oak · 76 cm wide View chair

    Studio

    Cloud pendant

    Save A softly diffused shade for desks, drawing tables, and late-night sketches.

    Library

    Walnut wall shelf

    Three slim shelves keep favorite books close without crowding the room. See dimensions Add to room
    """ css = """ :where(.card-glance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; max-width: 68rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-glance [data-citry-ui-part="card"]) { align-self: start; } :where(.card-glance h2, .card-glance p) { margin: 0; } :where(.card-glance h2) { font-size: 1.05rem; } :where(.card-glance__eyebrow) { margin-block-end: 0.25rem; color: light-dark(#72531b, #e4bd70); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } :where(.card-glance__scene) { position: relative; block-size: 8.5rem; overflow: hidden; } :where(.card-glance__scene::before) { position: absolute; inset: 0; content: ""; } :where(.card-glance__scene--sunroom::before) { background: linear-gradient(90deg, transparent 66%, rgb(255 255 255 / 46%) 66% 70%, transparent 70%), linear-gradient(160deg, #e9cfa0, #8aaa79); } :where(.card-glance__scene--studio::before) { background: radial-gradient(circle at 62% 38%, #fff1c7 0 13%, transparent 14%), linear-gradient(145deg, #8ba4bd, #3f5068); } :where(.card-glance__scene span) { position: absolute; inset-inline: 18%; inset-block-end: 16%; block-size: 30%; border-radius: 999px 999px 0.35rem 0.35rem; background: rgb(255 255 255 / 62%); } :where(.card-glance__sr-only) { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } """ preview = CardAtAGlance() preview # noqa: B018 ```` The smallest Card needs only content: ```citry-html A quiet place to read beside the window. ``` Compose the same result in Python: ```python from citry_ui import CCard reading_note = CCard(slots={"default": "A quiet place to read beside the window."}) ``` ## Compose the sections you need Every slot is optional, but a Card must supply at least one. Omitted sections produce no empty wrapper. ### Compose optional Card sections [Open the rendered preview](/ui-library/components/card/_previews/basic-card/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class BasicCards(Component): template = """
    The south window gets soft light from breakfast until noon.

    Washed linen

    Warm white · 140 g/m²

    Hand-thrown stoneware · one of twelve Reserve vase
    """ css = """ :where(.card-basics) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; max-width: 62rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-basics h2, .card-basics p) { margin: 0; } :where(.card-basics h2) { font-size: 1rem; } :where(.card-basics p) { margin-block-start: 0.25rem; color: light-dark(#6b6257, #cfc5b8); font-size: 0.82rem; } """ preview = BasicCards() preview # noqa: B018 ```` Use `header_actions` for controls beside a heading. Use `footer` for metadata and `actions` for controls at the end. Card supplies the alignment; your slot content supplies headings, landmarks, links, and accessible names. ## Choose visual emphasis `elevated` lifts a Card with shadow, `outline` draws a boundary, and `subtle` adds a quiet system-color tint. ### Compare Card variants [Open the rendered preview](/ui-library/components/card/_previews/variants/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardVariants(Component): template = """

    Elevated

    A focal surface for the linen floor lamp.

    Outline

    A clear boundary for the oak side table.

    Subtle

    A quiet grouping for woven storage baskets.
    """ css = """ :where(.card-variants) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1.25rem; max-width: 62rem; padding: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-variants h2) { margin: 0; font-size: 1rem; } """ preview = CardVariants() preview # noqa: B018 ```` Variants describe surface emphasis, not meaning. Use semantic HTML for success, warning, or error feedback instead of assigning semantic color to Card. ## Choose spacing `sm`, `md`, and `lg` adjust section padding and action gaps. Typography remains owned by your content. ### Compare Card sizes [Open the rendered preview](/ui-library/components/card/_previews/sizes/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardSizes(Component): template = """

    Small

    Cedar drawer label and finish sample. Open

    Medium

    A balanced surface for a lamp, book, and cup. Open

    Large

    Room for textile notes, dimensions, and a longer material story. Open
    """ css = """ :where(.card-sizes) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1rem; align-items: start; max-width: 64rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-sizes h2) { margin: 0; font-size: 1rem; } """ preview = CardSizes() preview # noqa: B018 ```` ## Add media Media appears first and clips to the Card's top edge, or to every edge when it is the only section. Card makes direct images, pictures, and videos block-level and prevents intrinsic overflow. It does not choose an aspect ratio, crop, or `object-fit`. ### Add consumer-owned media [Open the rendered preview](/ui-library/components/card/_previews/media/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardMedia(Component): template = """

    Breakfast nook

    Card preserves the illustration's own aspect ratio and accessible name.
    Clay Linen Moss Walnut

    Autumn materials

    Multiple consumer-owned nodes can define their own media layout.
    """ css = """ :where(.card-media) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; max-width: 52rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-media h2) { margin: 0; font-size: 1rem; } :where(.card-media__illustration) { display: block; inline-size: 100%; block-size: auto; } :where(.card-media__swatches) { display: grid; grid-template-columns: repeat(4, 1fr); min-block-size: 10rem; } :where(.card-media__swatches span) { display: grid; place-items: end center; padding: 0.5rem 0.2rem; color: #ffffff; font-size: 0.72rem; font-weight: 700; } :where(.card-media__clay) { background: #a75f46; } :where(.card-media__linen) { background: #b8a98c; color: #241f18; } :where(.card-media__moss) { background: #66704a; } :where(.card-media__walnut) { background: #5d3a2a; } """ preview = CardMedia() preview # noqa: B018 ```` Keep menus and popups outside `media`: clipping is intentional there. Place escaping interactive content in the header, body, or footer. ## Align metadata and actions Header and footer action slots keep direct controls together and wrap when space runs out. The companion content stays in its own flexible column. ### Compose header and footer actions [Open the rendered preview](/ui-library/components/card/_previews/actions/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardActions(Component): template = """

    Library

    Floating walnut shelf

    Save Hidden steel brackets keep the profile light while supporting a row of hardbacks. 90 by 18 cm · walnut veneer Add to room Compare finishes Dimensions
    """ css = """ :where(.card-actions) { max-width: 42rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-actions h2, .card-actions p) { margin: 0; } :where(.card-actions h2) { font-size: 1.05rem; } :where(.card-actions__eyebrow) { margin-block-end: 0.25rem; color: light-dark(#7c4f28, #e2b581); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } :where(.card-actions__sr-only) { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } """ preview = CardActions() preview # noqa: B018 ```` Pass `header_actions_attrs` or `actions_attrs` when the control cluster needs group semantics, an accessible label, data, or a trusted Alpine binding. A nonempty part mapping fails if its destination slot is absent. ## Put interactive content inside Card Card has no client state and does not intercept nested controls. Its root, header, body, and footer stay unclipped and create no stacking context. ### Use interactive content inside Card [Open the rendered preview](/ui-library/components/card/_previews/nested-content/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardNestedContent(Component): class Kwargs: pass class Slots: pass def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, object]: return { "rooms": ( citry_ui.CComboboxOption("sunroom", "Sunroom"), citry_ui.CComboboxOption("library", "Library"), citry_ui.CComboboxOption("studio", "Studio"), ) } template = """

    Place the reading chair

    Room Check dimensions Reading chair dimensions Measure doorways and the chosen corner before delivery. The chair is 76 cm wide, 84 cm deep, and 92 cm tall.
    """ css = """ :where(.card-nested) { max-width: 34rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-nested h2) { margin: 0; font-size: 1.05rem; } """ preview = CardNestedContent() preview # noqa: B018 ```` Card itself is not one large action. Use real links and Buttons inside it. A whole-Card link needs its own focus, layering, and nested-control contract and is not supported by `CCard`. ## Customize layout and theme Override public variables on an ancestor or one Card. Stable part selectors support responsive layouts without turning orientation into a server input. ### Customize Card with public CSS [Open the rendered preview](/ui-library/components/card/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardCustomization(Component): template = """

    Linen house

    Soft edges and warm neutrals made entirely with public variables and parts. Natural flax · washed finish

    Night studio

    Crisp geometry and cool contrast adapt through the same stable contract. Open palette
    """ css = """ :where(.card-customization) { display: grid; gap: 1rem; max-width: 62rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-customization > div) { padding: 1rem; border-radius: 1rem; } :where(.card-customization__linen) { --cui-card-background: #fffaf0; --cui-card-foreground: #3d3328; --cui-card-border-color: #d8c8ad; --cui-card-radius: 1.1rem; --cui-card-shadow: 0 0.8rem 2rem rgb(96 71 39 / 14%); background: #efe4d0; } :where(.card-customization__studio) { color-scheme: dark; --cui-card-background: #182235; --cui-card-foreground: #e7eefc; --cui-card-border-color: #607aa5; --cui-card-radius: 0.35rem; --cui-card-shadow: none; background: #0d1421; } :where(.card-customization__horizontal) { display: grid; grid-template-columns: minmax(8rem, 32%) 1fr; } :where(.card-customization__horizontal > [data-citry-ui-part="media"]) { grid-row: 1 / -1; border-start-start-radius: var(--cui-card-radius); border-start-end-radius: 0; border-end-start-radius: var(--cui-card-radius); border-end-end-radius: 0; } :where(.card-customization__horizontal > :not([data-citry-ui-part="media"])) { grid-column: 2; } :where(.card-customization h2) { margin: 0; font-size: 1.05rem; } :where(.card-customization__weave, .card-customization__grid) { min-block-size: 100%; } :where(.card-customization__weave) { background: repeating-linear-gradient(0deg, rgb(255 255 255 / 20%) 0 2px, transparent 2px 6px), #9f7950; } :where(.card-customization__grid) { background: linear-gradient(#5b78a8 1px, transparent 1px), linear-gradient(90deg, #5b78a8 1px, transparent 1px), #24324b; background-size: 1.5rem 1.5rem; } @media (max-width: 36rem) { :where(.card-customization__horizontal) { display: block; } :where(.card-customization__horizontal > [data-citry-ui-part="media"]) { border-start-start-radius: var(--cui-card-radius, 0.75rem); border-start-end-radius: var(--cui-card-radius, 0.75rem); border-end-start-radius: 0; border-end-end-radius: 0; } } """ preview = CardCustomization() preview # noqa: B018 ```` The example shows two independent brand treatments and a horizontal layout that returns to vertical at narrow width. `.cui-*` classes and `--_cui-*` variables are private. ## Choose root semantics The default `div` makes no document-structure claim. Choose `article` for an independently reusable composition, `section` for a named document section, or `li` inside a list. ### Choose native Card semantics [Open the rendered preview](/ui-library/components/card/_previews/semantics/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CardSemantics(Component): template = """

    Neutral group

    Decorative cushions can sit in an ordinary layout without becoming a document section.

    Independent article

    The spindle chair returns

    A complete journal note with its own heading and subject.

    Named section

    Wool upholstery

    A subsection of the wider materials guide.

    List item

      Oak side table Linen floor lamp
    """ css = """ :where(.card-semantics) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1.25rem; max-width: 68rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.card-semantics h2, .card-semantics h3) { margin-block-start: 0; } :where(.card-semantics h2) { font-size: 1rem; } :where(.card-semantics h3) { margin-block-end: 0; font-size: 0.95rem; } :where(.card-semantics ul) { display: grid; gap: 0.5rem; margin: 0; padding: 0; list-style: none; } """ preview = CardSemantics() preview # noqa: B018 ```` `CCard` adds no role, focus stop, keyboard behavior, or accessible name. The selected native root and your content own those semantics. ## Accessibility, trust, and server rendering Card renders completely without JavaScript. Slot text uses ordinary Citry escaping. Attribute maps accept native, ARIA, data, and trusted Alpine attributes, but reserve Card's reflected fields, part markers, and Citry's runtime ownership namespace. Card follows nested `color-scheme`, keeps a visible forced-colors boundary, removes decorative shadow in print, and uses logical layout for right-to-left content. ## API reference ### Inputs #### CCard server inputs Server inputs are passed in a template through `` or in Python through `CCard(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `tag` | `"div" | "article" | "section" | "li"` ([`CCardTag`](#card-interface-input-type-aliases-ccard-tag)) | `"div"` | Selects the native root. Use the neutral default unless the Card content satisfies stronger document semantics. | | `variant` | `"elevated" | "outline" | "subtle"` ([`CCardVariant`](#card-interface-input-type-aliases-ccard-variant)) | `"elevated"` | Selects shadow, border, and background emphasis. | | `size` | `"sm" | "md" | "lg"` ([`CCardSize`](#card-interface-input-type-aliases-ccard-size)) | `"md"` | Sets section padding and action gaps without changing consumer typography. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#card-interface-input-type-aliases-class-value)) | `None` | Adds root classes and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#card-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine root attributes. It cannot replace the public part, variant, size, or Citry runtime ownership fields. | | `media_attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine attributes to the media wrapper. A nonempty mapping requires the media slot. | | `header_attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine attributes to the header row. A nonempty mapping requires header or header_actions. | | `header_actions_attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine attributes to the header action group. A nonempty mapping requires header_actions. | | `body_attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine attributes to the body wrapper. A nonempty mapping requires the default slot. | | `footer_attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine attributes to the footer row. A nonempty mapping requires footer or actions. | | `actions_attrs` | `Mapping[str, object] | None` | `None` | Adds native, ARIA, data, and trusted Alpine attributes to the footer action group. A nonempty mapping requires actions. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CCard slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `media` | no | `{}` ([`CCardMediaSlotData`](#card-interface-ccard-media-slot-data)) | Wrapper omitted. | | `header` | no | `{}` ([`CCardHeaderSlotData`](#card-interface-ccard-header-slot-data)) | Content wrapper omitted. | | `header_actions` | no | `{}` ([`CCardHeaderActionsSlotData`](#card-interface-ccard-header-actions-slot-data)) | Action wrapper omitted. | | `default` | no | `{}` ([`CCardDefaultSlotData`](#card-interface-ccard-default-slot-data)) | Body wrapper omitted. | | `footer` | no | `{}` ([`CCardFooterSlotData`](#card-interface-ccard-footer-slot-data)) | Content wrapper omitted. | | `actions` | no | `{}` ([`CCardActionsSlotData`](#card-interface-ccard-actions-slot-data)) | Action wrapper omitted. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CCard CSS variables Apply these variables to `CCard` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-card-background` | `color` | Root background. | `Variant-derived Canvas or system-color mix.` | | `--cui-card-foreground` | `color` | Inherited content color. | `CanvasText` | | `--cui-card-border-color` | `color` | Root border color. | `Variant-derived transparent or system-color mix.` | | `--cui-card-shadow` | `shadow` | Root elevation shadow. | `Variant-derived shadow or none.` | | `--cui-card-radius` | `length` | Root and media edge radius. | `0.75rem` | | `--cui-card-padding` | `length` | Header, body, and footer row padding. | `Size-derived length.` | | `--cui-card-section-gap` | `length` | Space between header or footer content and actions. | `Size-derived length.` | | `--cui-card-actions-gap` | `length` | Gap between controls in either action group. | `Size-derived length.` | | `--cui-card-actions-justify` | `justify-content` | Alignment inside action groups. | `flex-start` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CCard attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-variant` | Root | `"elevated" | "outline" | "subtle"` | Reflects the server-selected surface treatment. | | `data-size` | Root | `"sm" | "md" | "lg"` | Reflects the server-selected spacing preset. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CCard selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="card"]` | Native root | Stable Card root and `attrs` destination. | | `[data-citry-ui-part="media"]` | Optional media wrapper | Clipped media edge and `media_attrs` destination. | | `[data-citry-ui-part="header"]` | Optional header row | Header layout and `header_attrs` destination. | | `[data-citry-ui-part="header-actions"]` | Optional header action group | Direct-control layout and `header_actions_attrs` destination. | | `[data-citry-ui-part="body"]` | Optional body wrapper | Main content and `body_attrs` destination. | | `[data-citry-ui-part="footer"]` | Optional footer row | Footer layout and `footer_attrs` destination. | | `[data-citry-ui-part="actions"]` | Optional footer action group | Direct-control layout and `actions_attrs` destination. |
    ### 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]` | | `CCardTag` | `Literal["div", "article", "section", "li"]` | | `CCardVariant` | `Literal["elevated", "outline", "subtle"]` | | `CCardSize` | `Literal["sm", "md", "lg"]` |
    #### `CCardMediaSlotData` Empty dataclass: `{}`. #### `CCardHeaderSlotData` Empty dataclass: `{}`. #### `CCardHeaderActionsSlotData` Empty dataclass: `{}`. #### `CCardDefaultSlotData` Empty dataclass: `{}`. #### `CCardFooterSlotData` Empty dataclass: `{}`. #### `CCardActionsSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Carousel Source: https://citry.dev/ui-library/components/carousel/ # Carousel Use `CCarousel` and `CCarouselSlide` for a named sequence of content cards, stories, or media. It uses native scrolling and Scroll Snap, so touch and trackpad navigation work without an application-widget keyboard model. ## Carousel at a glance ### Carousel at a glance [Open the rendered preview](/ui-library/components/carousel/_previews/at-a-glance/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CarouselAtAGlance(Component): template = """ Aurora field notesA clear night above the northern ridge.Tide field notesA spring tide reshaped the eastern inlet. """ preview = CarouselAtAGlance() preview # noqa: B018 ```` ## Compose content cards ### Carousel content cards [Open the rendered preview](/ui-library/components/carousel/_previews/cards/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CarouselCards(Component): template = """ Canopy

    Listening above the forest floor

    Sensors reveal the canopy's changing rhythm.
    Coast

    Mapping a moving shoreline

    Field teams compare a decade of tidal change.
    """ preview = CarouselCards() preview # noqa: B018 ```` ## Control the current Slide ### Controlled Carousel [Open the rendered preview](/ui-library/components/carousel/_previews/controlled/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ControlledCarousel(Component): template = """

    Slide of 3

    First controlled SlideSecond controlled SlideThird controlled Slide
    """ preview = ControlledCarousel() preview # noqa: B018 ```` ## Choose orientation ### Carousel orientations [Open the rendered preview](/ui-library/components/carousel/_previews/orientation/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class VerticalCarousel(Component): template = """ Morning observationsEvening observations """ preview = VerticalCarousel() preview # noqa: B018 ```` ## Configure controls and indicators ### Carousel controls [Open the rendered preview](/ui-library/components/carousel/_previews/controls/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CarouselControls(Component): template = """ Previous and next controls only.Second Slide.Choose with a named picker.Second picker target. """ preview = CarouselControls() preview # noqa: B018 ```` ## Loop and disable ### Carousel states [Open the rendered preview](/ui-library/components/carousel/_previews/states/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CarouselStates(Component): template = """ Previous wraps to the end.Next wraps to the start.Owned controls are disabled. """ preview = CarouselStates() preview # noqa: B018 ```` ## Variants and sizes ### Carousel variants and sizes [Open the rendered preview](/ui-library/components/carousel/_previews/variants/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CarouselVariants(Component): template = """ Compact contentSpacious content """ preview = CarouselVariants() preview # noqa: B018 ```` ## Put forms in Slides ### Carousel form content [Open the rendered preview](/ui-library/components/carousel/_previews/forms/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CarouselForms(Component): template = """
    Project nameReceive updatesSave profile
    """ preview = CarouselForms() preview # noqa: B018 ```` ## Customize Carousel ### Customize Carousel [Open the rendered preview](/ui-library/components/carousel/_previews/customization/) ````citry # ruff: noqa: E501 import citry_ui from citry import Component, citry citry.register_library(citry_ui) class CustomCarousel(Component): template = """ The northern ridge at blue hour.Reflections on the glacial lake. """ css = """ .aurora-carousel { --cui-carousel-radius:1.25rem; --cui-carousel-indicator-active-color:#7c3aed; --cui-carousel-control-background:#ede9fe; } """ preview = CustomCarousel() preview # noqa: B018 ```` ## Accessibility and interaction Give the root a concise `label` and every Slide a content-specific `label`. Previous/next and picker controls are native Buttons that keep focus in place. The native scroll viewport is also a Tab stop, so keyboard and Safari users can focus and scroll it directly without a scripted Arrow-key model. All Slides remain in the accessibility tree; offscreen content is never incorrectly presented as hidden. Disable indicators for large collections to avoid adding too many Tab stops. Autoplay is intentionally not part of v1. ## API reference ### Inputs #### CCarousel server inputs Server inputs are passed in a template through `` or in Python through `CCarousel(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `label` | `str` | required | Names the carousel region without repeating the word carousel. | | `id` | `str | None` | generated | Sets root identity. | | `index` | `int` | `0` | Selects the initial zero-based Slide and uncontrolled fallback. | | `orientation` | `"horizontal" | "vertical"` ([`CCarouselOrientation`](#carousel-interface-orientation)) | `"horizontal"` | Sets scroll axis. | | `loop` | `bool` | `False` | Allows previous and next controls to wrap. | | `disabled` | `bool` | `False` | Disables owned controls and drag handling. | | `controls` | `bool` | `True` | Shows previous and next Buttons. | | `indicators` | `bool` | `True` | Shows the grouped picker Buttons. | | `draggable` | `bool` | `True` | Enables fine-pointer drag; native touch scroll remains available. | | `variant` | `"plain" | "surface"` ([`CCarouselVariant`](#carousel-interface-variant)) | `"plain"` | Selects root treatment. | | `size` | `"sm" | "md" | "lg"` ([`CCarouselSize`](#carousel-interface-size)) | `"md"` | Selects complete-family geometry. | | `previous_label` | `str` | `"Previous slide"` | Names the previous Button for the current locale. | | `next_label` | `str` | `"Next slide"` | Names the next Button for the current locale. | | `picker_label` | `str` | `"Choose slide"` | Names the picker Button group for the current locale. | | `role_description` | `str | None` | `"carousel"` | Sets the localized `aria-roledescription`; None omits it. | | `class_` | `CClassValue` ([`CClassValue`](#carousel-interface-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue` ([`CStyleValue`](#carousel-interface-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds allowed native and data attributes to the root. |
    #### CCarousel client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `index` | `integer` | Releases control and preserves the current committed index. | Controls the active zero-based Slide while supplied. | | `orientation` | `CCarouselOrientation` | Uses the server input. | Reactively changes scroll axis. | | `loop` | `boolean` | Uses the server input. | Controls boundary wrapping. | | `disabled` | `boolean` | Uses the server input. | Controls owned interaction availability. | | `controls` | `boolean` | Uses the server input. | Shows or hides previous and next controls. | | `indicators` | `boolean` | Uses the server input. | Shows or hides picker controls. | | `draggable` | `boolean` | Uses the server input. | Controls fine-pointer dragging. | | `variant` | `CCarouselVariant` | Uses the server input. | Changes root treatment. | | `size` | `CCarouselSize` | Uses the server input. | Changes complete-family geometry. | | `onIndexChange` | `function` | Does not notify a component callback. | Receives navigation scroll and structural index requests. |
    #### CCarouselSlide server inputs Server inputs are passed in a template through `` or in Python through `CCarouselSlide(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | required | Sets unique stable Slide identity. | | `label` | `str` | required | Names Slide content without repeating the word slide. | | `role_description` | `str | None` | `"slide"` | Sets the localized `aria-roledescription`; None omits it. | | `class_` | `CClassValue` ([`CClassValue`](#carousel-interface-class-value)) | `None` | Adds Slide classes. | | `style` | `CStyleValue` ([`CStyleValue`](#carousel-interface-style-value)) | `None` | Adds Slide inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds allowed native and data attributes to the Slide. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CCarousel slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CCarouselDefaultSlotData`](#carousel-interface-carousel-slot)) | none |
    #### CCarouselSlide slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CCarouselSlideDefaultSlotData`](#carousel-interface-slide-slot)) | none |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CCarousel events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onIndexChange` | `(index: integer, detail: CCarouselIndexChangeDetail) => void` ([`CCarouselIndexChangeDetail`](#carousel-interface-index-change-detail)) | Previous next picker native-scroll or structure requests a different index. | `{index, previousIndex, value, reason, controlled, forced, source}` ([`CCarouselIndexChangeDetail`](#carousel-interface-index-change-detail)) | Uncontrolled requests commit before notification; controlled requests wait for acceptance; removal fallback is forced. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CCarousel CSS variables Apply these variables to `CCarousel` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-carousel-background` | `color` | Root background. | `transparent` | | `--cui-carousel-foreground` | `color` | Root foreground. | `CanvasText` | | `--cui-carousel-border-color` | `color` | Surface boundaries. | `Scheme-aware neutral.` | | `--cui-carousel-radius` | `length` | Root viewport and Slide radius. | `0.9rem` | | `--cui-carousel-gap` | `length` | Slide and region gap. | `Size-derived.` | | `--cui-carousel-padding` | `length` | Root padding. | `Size-derived.` | | `--cui-carousel-block-size` | `length` | Vertical viewport and Slide block size. | `20rem` | | `--cui-carousel-control-background` | `color` | Previous and next Button background. | `Scheme-aware neutral.` | | `--cui-carousel-control-foreground` | `color` | Previous and next Button foreground. | `CanvasText` | | `--cui-carousel-control-size` | `length` | Previous and next Button size. | `Size-derived.` | | `--cui-carousel-focus-color` | `color` | Focus ring. | `Highlight` | | `--cui-carousel-indicator-size` | `length` | Picker dot size. | `0.65rem` | | `--cui-carousel-indicator-color` | `color` | Inactive picker color. | `Scheme-aware neutral.` | | `--cui-carousel-indicator-active-color` | `color` | Current picker color. | `Highlight` | | `--cui-carousel-duration` | `time` | Reserved scroll transition duration and reduced-motion input. | `260ms` | | `--cui-carousel-easing` | `easing` | Reserved scroll transition easing. | `ease-out` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CCarousel attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Root and Slide | `"region" | "group"` | Identifies carousel region and Slide groups. | | `aria-label` | Root Slide controls and picker group | `string` | Names each owned semantic surface. | | `aria-roledescription` | Root and Slide | `"carousel" | "slide"` | Supplies concise role descriptions. | | `data-orientation` | Root | `CCarouselOrientation` | Reflects scroll axis. | | `data-loop` | Root | `present | absent` | Reflects boundary wrapping. | | `data-disabled` | Root | `present | absent` | Reflects owned disabledness. | | `data-draggable` | Root | `present | absent` | Reflects fine-pointer drag availability. | | `data-variant` | Root | `CCarouselVariant` | Reflects treatment. | | `data-size` | Root | `CCarouselSize` | Reflects geometry. | | `data-index` | Root Slide and picker | `integer string` | Reflects active or collection position according to destination. | | `data-value` | Slide | `string` | Reflects stable Slide identity. | | `data-active` | Current Slide | `present | absent` | Reflects the nearest selected snap point. | | `disabled` | Owned Buttons | `present | absent` | Reflects navigation availability. | | `aria-current` | Current picker | `"true"` | Identifies the picker for the active Slide. | | `tabindex` | Viewport | `"0"` | Makes the native scroll region keyboard reachable. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CCarousel selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="carousel"]` | section | Root state and styling boundary. | | `[data-citry-ui-part="controls"]` | div | Previous and next control row. | | `[data-citry-ui-part="previous"]` | Button | Previous control. | | `[data-citry-ui-part="next"]` | Button | Next control. | | `[data-citry-ui-part="viewport"]` | div | Native Scroll Snap viewport. | | `[data-citry-ui-part="track"]` | div | Direct Slide layout track. | | `[data-citry-ui-part="slide"]` | div | Named composed Slide. | | `[data-citry-ui-part="indicators"]` | div | Picker Button group. | | `[data-citry-ui-part="indicator"]` | Button | Runtime picker for one Slide. |
    ### 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]` | | `CCarouselOrientation` | `Literal["horizontal", "vertical"]` | | `CCarouselVariant` | `Literal["plain", "surface"]` | | `CCarouselSize` | `Literal["sm", "md", "lg"]` |
    #### `CCarouselDefaultSlotData` Empty dataclass: `{}`. #### `CCarouselSlideDefaultSlotData` Empty dataclass: `{}`. #### `CCarouselIndexChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `index` | `int` | - | Requested active index. | | `previousIndex` | `int` | - | Previously effective index. | | `value` | `str` | - | Stable requested Slide value. | | `reason` | `string` | - | Request source. | | `controlled` | `boolean` | - | Whether client index owns state. | | `forced` | `boolean` | - | Whether structure forced fallback. | | `source` | `EventTarget | null` | - | Browser source. |
    ### 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. #### CCarousel translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-carousel-previous` | Names the previous-slide control. | `None` | `previous_label` input | $c-tr updates `aria-label`. | | `citry-ui-carousel-next` | Names the next-slide control. | `None` | `next_label` input | $c-tr updates `aria-label`. | | `citry-ui-carousel-picker` | Names the slide-picker group. | `None` | `picker_label` input | $c-tr updates `aria-label`. | | `citry-ui-carousel-role` | Describes the carousel region role. | `None` | `role_description` input | $c-tr updates `aria-roledescription`. |
    #### CCarouselSlide translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-carousel-slide-role` | Describes each slide group role. | `None` | `role_description` input | $c-tr updates `aria-roledescription`. |
    --- # Data Grid Source: https://citry.dev/ui-library/components/data-grid/ # Data Grid Use `CDataGrid` for application data that benefits from one composite Tab stop, cell navigation, row selection, accepted complete-collection sorting, or fixed-height server windowing. Use `CTable` instead for document-like tables, ordinary links and controls in cells, spans, footers, and print-first reading. ## Build a complete grid Columns and rows are immutable Python records. Every Row supplies exactly one Cell value for every Column key, and every key is a stable nonempty string. ### Navigate a complete Data Grid [Open the rendered preview](/ui-library/components/data-grid/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow citry.register_library(citry_ui) class DataGridAtAGlance(Component): template = """ Current project members """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": ( CDataGridColumn("name", "Name", width=190), CDataGridColumn("role", "Role", width=180), CDataGridColumn("status", "Status", width=130), ), "rows": ( CDataGridRow("ada", {"name": "Ada Lovelace", "role": "Engineer", "status": "Active"}), CDataGridRow("grace", {"name": "Grace Hopper", "role": "Admiral", "status": "Active"}), CDataGridRow("katherine", {"name": "Katherine Johnson", "role": "Mathematician", "status": "Away"}), ), } preview = DataGridAtAGlance() preview # noqa: B018 ```` The server output is a native table with exact row and column positions. Once enhanced, one Header or Cell is in the page Tab order. Arrow keys move between rendered Cells; Home, End, Page Up, Page Down, Ctrl/Cmd+Home, and Ctrl/Cmd+End provide larger movement. ## Request sorting and select rows Set `sortable=True` on Columns that can be sorted. Header activation cycles ascending, descending, then unsorted. `onSortChange` receives the requested model, and accepted `sort` state must come back from the owner. When that accepted request belongs to a complete supplied collection, the grid visibly reorders Rows by rendered Cell text. It uses locale-aware numeric comparison, applies sort entries in priority order, and restores server order when sorting is cleared. Shift preserves other Columns when `multi_sort=True`. ### Sort and select people [Open the rendered preview](/ui-library/components/data-grid/_previews/sorting-selection/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow, CDataGridSort citry.register_library(citry_ui) class DataGridSortingSelection(Component): template = """
    Activate a sortable header or select a row
    """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": ( CDataGridColumn("name", "Name", sortable=True, width=190), CDataGridColumn("team", "Team", sortable=True, width=150), CDataGridColumn("score", "Score", sortable=True, width=100, align="end"), ), "rows": ( CDataGridRow("ada", {"name": "Ada Lovelace", "team": "Platform", "score": 98}), CDataGridRow("grace", {"name": "Grace Hopper", "team": "Compiler", "score": 95}), CDataGridRow("lin", {"name": "Lin Clark", "team": "Runtime", "score": 91}), ), "sort": (CDataGridSort("name", "asc"),), } preview = DataGridSortingSelection() preview # noqa: B018 ```` `selection="single"` or `selection="multiple"` enables Row selection. Uncontrolled selection commits immediately. A non-null client `selected` array makes selection controlled, so the visible state waits for acceptance. In multiple mode, drag a mouse pointer across loaded Rows to select a range; starting on a selected Row removes the dragged range. Disabled Rows are skipped. Shift+Space toggles the focused Row in either direction. Touch remains ordinary scrolling rather than starting a drag selection. ## Control models from Alpine Pass `sort`, `selected`, and callbacks through `$c-props`. Invalid client models are diagnosed and the last valid state remains active. ### Control Data Grid models [Open the rendered preview](/ui-library/components/data-grid/_previews/controlled/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow citry.register_library(citry_ui) class ControlledDataGrid(Component): template = """
    """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": ( CDataGridColumn("name", "Name", sortable=True, width=190), CDataGridColumn("role", "Role", sortable=True, width=170), ), "rows": ( CDataGridRow("ada", {"name": "Ada Lovelace", "role": "Engineer"}), CDataGridRow("grace", {"name": "Grace Hopper", "role": "Admiral"}), ), } preview = ControlledDataGrid() preview # noqa: B018 ```` Sort remains request/accept so an application can reject it or wait for a server response. Initial and programmatic models describe the server-authored order; local reordering occurs when the browser accepts a Header request for a complete collection. A server window cannot sort Rows it does not have, so its owner must return the newly ordered range. Selection becomes uncontrolled again when client `selected` is omitted or null. Accepted changes are announced politely. ## Supply a server window Set `total_count` and `start_index` when `rows` is one contiguous window of a larger collection. `row_height` is fixed geometry. `onRangeChange` receives a half-open desired range when scrolling, resizing, or navigation leaves the supplied range. ### Request Data Grid windows [Open the rendered preview](/ui-library/components/data-grid/_previews/windowed/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow citry.register_library(citry_ui) class WindowedDataGrid(Component): template = """
    This static preview supplies one complete window
    """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": ( CDataGridColumn("number", "Record", width=120), CDataGridColumn("action", "Action", width=230), CDataGridColumn("actor", "Actor", width=160), ), "rows": tuple( CDataGridRow( f"audit-{index}", {"number": f"#{index + 1:05d}", "action": "Signed deployment record", "actor": "Release bot"}, ) for index in range(16) ), } preview = WindowedDataGrid() preview # noqa: B018 ```` The component does not fetch. The owner handles supersession, retries, offline state, and replacement. Keep Row keys stable across windows. This first version does not select unloaded rows or expose a remote select-all operation. ## Edit cells in place Set `editable=True` on a `CDataGridColumn`, then choose its `editor` from `text`, `number`, `checkbox`, or `select`. Select editors require named `CDataGridEditOption` records. `editor_attrs` accepts only the documented attributes for that native control, such as `min`, `max`, `step`, `maxlength`, and `placeholder`. ### Edit project assignments [Open the rendered preview](/ui-library/components/data-grid/_previews/editing/) ````citry # ruff: noqa: ANN001, ANN201 - public snippets keep focus on component use import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridEditOption, CDataGridRow citry.register_library(citry_ui) class EditableDataGrid(Component): def template_data(self, _kwargs, _slots): return { "columns": ( CDataGridColumn("name", "Name", editable=True, editor_attrs={"maxlength": 80}), CDataGridColumn( "role", "Role", editable=True, editor="select", editor_options=( CDataGridEditOption("engineer", "Engineer"), CDataGridEditOption("designer", "Designer"), CDataGridEditOption("lead", "Lead"), ), ), CDataGridColumn( "allocation", "Allocation", editable=True, editor="number", editor_attrs={"min": 0, "max": 100, "step": 5}, ), CDataGridColumn("active", "Active", editable=True, editor="checkbox"), ), "rows": ( CDataGridRow("ada", {"name": "Ada", "role": "engineer", "allocation": 80, "active": True}), CDataGridRow("mira", {"name": "Mira", "role": "designer", "allocation": 60, "active": True}), ), } template = """
    Double-click or press Enter to edit
    """ preview = EditableDataGrid() preview # noqa: B018 ```` Enter, F2, typing, Backspace, Delete, or double-click enters edit mode where appropriate. Enter commits, Escape cancels, and Tab commits before moving to the adjacent Cell. `onCellEditCommit` receives the typed value and stable Row and Column details. Returning `False` rejects the value and keeps the editor open. The component does not mutate server Rows: update the owner and render the accepted value back into `rows`. The static documentation preview uses a small self-contained range with no omitted leading or trailing Rows. It therefore never exposes scrollable blank space that the static page cannot replace. In an application, a partial range keeps its spacers only until the owner replaces it after `onRangeChange`. ## Loading, empty, and error states `state="loading"` and `state="error"` replace ready Rows with one spanning state output. Ready with `total_count=0` becomes empty. Fill the corresponding Slot for richer server content, or override the plain localized label. ### Render Data Grid states [Open the rendered preview](/ui-library/components/data-grid/_previews/states/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow citry.register_library(citry_ui) class DataGridStates(Component): template = """
    Records are unavailable. Try again from the toolbar.
    """ css = ":where(.grid-states){display:grid;gap:1rem}" def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": (CDataGridColumn("name", "Name"), CDataGridColumn("status", "Status")), "rows": (CDataGridRow("placeholder", {"name": "Placeholder", "status": "Pending"}),), } preview = DataGridStates() preview # noqa: B018 ```` ## Accessibility and Cell content The family follows the ARIA data-grid interaction model. Header and Cell Slot content cannot contain caller-authored links, buttons, inputs, editable content, or another Tab stop; focus remains on the Header or Cell. Built-in editors temporarily move focus into one owned native control. Use `onCellActivate` for Enter and double-click activation on noneditable Cells. ### Use exact positions and disabled Rows [Open the rendered preview](/ui-library/components/data-grid/_previews/accessibility/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow citry.register_library(citry_ui) class AccessibleDataGrid(Component): template = """ Use Arrow keys to move and Shift+Space to select an enabled Row. """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": ( CDataGridColumn("change", "Change", width=240), CDataGridColumn("owner", "Owner", width=160), CDataGridColumn("status", "Approval status", width=160), ), "rows": ( CDataGridRow("api", {"change": "API release", "owner": "Ada", "status": "Approved"}), CDataGridRow( "locked", {"change": "Security policy", "owner": "Grace", "status": "Locked"}, disabled=True, ), CDataGridRow("docs", {"change": "Guide update", "owner": "Lin", "status": "Review"}), ), } preview = AccessibleDataGrid() preview # noqa: B018 ```` Column labels and Cell values belong to the application and should already be localized. State labels and browser announcements use the Citry UI catalog by default. Explicit label overrides remain caller-owned and do not switch with the client locale. ## Styling and scope boundaries Use `density`, `striped`, `column_borders`, and `sticky_header` for common presentation. Customize the root and native table separately with `attrs` and `table_attrs`, or use the documented public variables and part selectors. ### Customize a Data Grid [Open the rendered preview](/ui-library/components/data-grid/_previews/customization/) ````citry import citry_ui from citry import Component, citry from citry_ui import CDataGridColumn, CDataGridRow citry.register_library(citry_ui) class CustomizedDataGrid(Component): template = """
    """ css = """ :where(.custom-grid [data-citry-ui-part="header-cell"]) { text-transform:uppercase;letter-spacing:.04em; } :where(.custom-grid [data-column-key="value"]) { font-variant-numeric:tabular-nums; } """ def template_data(self, _kwargs: object, _slots: object) -> dict[str, object]: return { "columns": ( CDataGridColumn("metric", "Metric", width=220), CDataGridColumn("value", "Value", width=120, align="end"), ), "rows": ( CDataGridRow("latency", {"metric": "P95 latency", "value": "128 ms"}), CDataGridRow("errors", {"metric": "Error rate", "value": "0.04%"}), CDataGridRow("uptime", {"metric": "Uptime", "value": "99.99%"}), ), } preview = CustomizedDataGrid() preview # noqa: B018 ```` Arbitrary caller-authored Cell widgets, built-in filtering, grouping, aggregation, pivoting, tree Rows, pinning, reordering, resizing, clipboard mutation, export, and browser-owned data sources are outside this first family. Compose application controls around the grid instead. ## API reference ### Inputs #### CDataGrid server inputs Server inputs are passed in a template through `` or in Python through `CDataGrid(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `columns` | `Sequence[CDataGridColumn]` | required | Defines the nonempty ordered structural Column schema. | | `rows` | `Sequence[CDataGridRow]` | required | Supplies a complete collection or one contiguous server window. | | `label` | `str` | required | Supplies the required accessible grid name. | | `id` | `str | None` | generated | Sets root identity and bases stable Header Row and Cell IDs. | | `state` | `CDataGridState` ([`CDataGridState`](#data-grid-interface-state)) | `"ready"` | Selects ready loading or error output; zero ready Rows become empty. | | `sort` | `Sequence[CDataGridSort]` | `"()"` | Supplies the initial server-authored ordered sort model. | | `multi_sort` | `bool` | `True` | Allows Shift-modified sort requests to preserve other Columns. | | `selection` | `CDataGridSelection` ([`CDataGridSelection`](#data-grid-interface-selection)) | `"none"` | Selects no single or multiple supplied-Row selection. | | `selected` | `Sequence[str]` | `"()"` | Supplies unique initially selected Row keys. | | `disabled` | `bool` | `False` | Blocks sorting selection activation editing and navigation. | | `total_count` | `int | None` | `None` | Sets logical Row count; omission means the complete supplied collection. | | `start_index` | `int` | `0` | Sets the zero-based logical index of the first supplied Row. | | `row_height` | `int` | `48` | Sets the fixed Row stride in CSS pixels. | | `viewport_size` | `int` | `400` | Sets initial scroll-viewport block size in CSS pixels. | | `overscan` | `int` | `3` | Adds 0 through 100 Rows around each desired range. | | `initial_index` | `int` | `0` | Performs one initial scroll to a clamped logical Row. | | `density` | `CDataGridDensity` ([`CDataGridDensity`](#data-grid-interface-density)) | `"comfortable"` | Selects compact comfortable or spacious Row presentation. | | `striped` | `bool` | `False` | Adds alternate supplied-Row surfaces. | | `column_borders` | `bool` | `False` | Shows boundaries between Columns. | | `sticky_header` | `bool` | `True` | Keeps Headers at the viewport block start. | | `loading_label` | `str` | `"Loading data..."` | Overrides the localized loading state. | | `empty_label` | `str` | `"No data."` | Overrides the localized empty state. | | `error_label` | `str` | `"Unable to load data."` | Overrides the localized error state. | | `sort_ascending_label` | `str` | `"{column} sorted ascending"` | Overrides ascending-sort announcements and must retain column. | | `sort_descending_label` | `str` | `"{column} sorted descending"` | Overrides descending-sort announcements and must retain column. | | `sort_cleared_label` | `str` | `"Sort cleared for {column}"` | Overrides cleared-sort announcements and must retain column. | | `selected_one_label` | `str` | `"One row selected"` | Overrides the one-Row selection announcement. | | `selected_label` | `str` | `"{count} rows selected"` | Overrides multi-Row selection announcements and must retain count. | | `edit_label` | `str` | `"Edit {column}"` | Overrides owned editor names and must retain column. | | `editing_label` | `str` | `"Editing {column}"` | Overrides edit-start announcements and must retain column. | | `edit_submitted_label` | `str` | `"Changes submitted for {column}"` | Overrides edit-commit announcements and must retain column. | | `edit_cancelled_label` | `str` | `"Changes cancelled for {column}"` | Overrides edit-cancel announcements and must retain column. | | `edit_invalid_label` | `str` | `"Enter a valid value for {column}"` | Overrides rejected or invalid edit announcements and must retain column. | | `class_` | `CClassValue | None` ([`CClassValue`](#data-grid-interface-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#data-grid-interface-style-value)) | `None` | Adds root styles merged with owned geometry variables. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes without replacing state or runtime ownership. | | `table_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed native table attributes without replacing Grid semantics or positions. |
    #### CDataGrid client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `sort` | `Array<{key: string, direction: "asc" | "desc"}> | null` | Uses the server sort model. | Accepts sort indicators and reorders a complete collection after a matching Header request. | | `selected` | `string[] | null` | Omission or null releases control to committed selection. | Controls unique supplied-Row selection while supplied. | | `disabled` | `boolean` | Uses the server value. | Reactively disables owned interaction. | | `overscan` | `number` | Uses the server value. | Reactively changes desired range buffering. | | `onSortChange` | `function` | Sort activation emits no callback. | Receives request-only sort changes. | | `onSelectionChange` | `function` | Selection still commits when uncontrolled. | Receives selection requests or commits. | | `onRangeChange` | `function` | Uncovered ranges only reflect pending state. | Receives animation-frame-coalesced desired ranges. | | `onCellActivate` | `function` | Enter and double-click have no activation callback. | Receives enabled Cell activation. | | `onCellEditStart` | `function` | No edit-start callback runs. | Receives entry into an owned native editor. | | `onCellEditCommit` | `function` | Valid edits close without a data-change request. | Receives changed typed values; returning false rejects synchronously. | | `onCellEditCancel` | `function` | No edit-cancel callback runs. | Receives Escape and reactive disabling cancellations. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CDataGrid slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `caption` | no | `{}` ([`CDataGridCaptionSlotData`](#data-grid-interface-cdata-grid-caption-slot-data)) | Omitted. | | `toolbar` | no | `{}` ([`CDataGridToolbarSlotData`](#data-grid-interface-cdata-grid-toolbar-slot-data)) | Omitted before the viewport. | | `header` | no | `{column, column_index, sort_direction, sort_priority}` ([`CDataGridHeaderSlotData`](#data-grid-interface-cdata-grid-header-slot-data)) | Escaped Column label plus owned sort indicator. | | `cell` | no | `{row, column, cell, row_index, column_index, selected}` ([`CDataGridCellSlotData`](#data-grid-interface-cdata-grid-cell-slot-data)) | Escaped or component-like Cell value. | | `loading` | no | `{}` ([`CDataGridLoadingSlotData`](#data-grid-interface-cdata-grid-loading-slot-data)) | Localized loading label. | | `empty` | no | `{}` ([`CDataGridEmptySlotData`](#data-grid-interface-cdata-grid-empty-slot-data)) | Localized empty label. | | `error` | no | `{}` ([`CDataGridErrorSlotData`](#data-grid-interface-cdata-grid-error-slot-data)) | Localized error label. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CDataGrid events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onSortChange` | `(sort: sort[], detail: CDataGridSortChangeDetail) => void` ([`CDataGridSortChangeDetail`](#data-grid-interface-cdata-grid-sort-change-detail)) | Click or Enter activates an enabled sortable Header. | `{sort, previousSort, columnKey, direction, source, sourceEvent}` ([`CDataGridSortChangeDetail`](#data-grid-interface-cdata-grid-sort-change-detail)) | Request-only until accepted; a matching accepted request reorders complete supplied Rows by rendered text while a partial window waits for server replacement. | | `onSelectionChange` | `(selected: string[], detail: CDataGridSelectionChangeDetail) => void` ([`CDataGridSelectionChangeDetail`](#data-grid-interface-cdata-grid-selection-change-detail)) | Click Shift+click Shift+Space or mouse drag requests a supplied-Row selection change. | `{selected, previousSelected, changed, rowKey, selectedRow, controlled, source, sourceEvent}` ([`CDataGridSelectionChangeDetail`](#data-grid-interface-cdata-grid-selection-change-detail)) | Uncontrolled state commits first; controlled state waits for acceptance; disabled Rows are skipped. | | `onRangeChange` | `(detail: CDataGridRangeChangeDetail) => void` ([`CDataGridRangeChangeDetail`](#data-grid-interface-cdata-grid-range-change-detail)) | Scroll resize configuration or navigation exposes an uncovered desired range. | `{startIndex, endIndex, visibleStartIndex, visibleEndIndex, requestId, reason, sourceEvent}` ([`CDataGridRangeChangeDetail`](#data-grid-interface-cdata-grid-range-change-detail)) | Coalesced per animation frame with a monotonic request ID. | | `onCellActivate` | `(detail: CDataGridCellActivateDetail) => void` ([`CDataGridCellActivateDetail`](#data-grid-interface-cdata-grid-cell-activate-detail)) | Enter or double-click activates an enabled noneditable body Cell. | `{rowKey, columnKey, rowIndex, columnIndex, source, sourceEvent}` ([`CDataGridCellActivateDetail`](#data-grid-interface-cdata-grid-cell-activate-detail)) | Does not mutate data. | | `onCellEditStart` | `(detail: CDataGridCellEditDetail) => void` ([`CDataGridCellEditDetail`](#data-grid-interface-cdata-grid-cell-edit-detail)) | Enter F2 typing Backspace Delete or double-click enters an editable Cell. | `{rowKey, columnKey, rowIndex, columnIndex, editor, previousValue, source, reason, sourceEvent}` ([`CDataGridCellEditDetail`](#data-grid-interface-cdata-grid-cell-edit-detail)) | The owned native editor receives focus and grid navigation pauses. | | `onCellEditCommit` | `(value: string | number | boolean, detail: CDataGridCellEditDetail) => boolean | void` ([`CDataGridCellEditDetail`](#data-grid-interface-cdata-grid-cell-edit-detail)) | Enter F2 Tab outside pointer or a replacement editor submits a valid changed value. | `{rowKey, columnKey, rowIndex, columnIndex, editor, previousValue, source, reason, sourceEvent}` ([`CDataGridCellEditDetail`](#data-grid-interface-cdata-grid-cell-edit-detail)) | Returning false or throwing keeps the editor open and invalid; otherwise the editor closes while server Rows remain authoritative. | | `onCellEditCancel` | `(detail: CDataGridCellEditDetail) => void` ([`CDataGridCellEditDetail`](#data-grid-interface-cdata-grid-cell-edit-detail)) | Escape or reactive disabling cancels an active edit. | `{rowKey, columnKey, rowIndex, columnIndex, editor, previousValue, source, reason, sourceEvent}` ([`CDataGridCellEditDetail`](#data-grid-interface-cdata-grid-cell-edit-detail)) | Restores server-rendered Cell content without a commit callback. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CDataGrid CSS variables Apply these variables to `CDataGrid` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-data-grid-viewport-size` | `length` | Maximum scroll-viewport block size. | `Server viewport_size or 400px` | | `--cui-data-grid-row-height` | `length` | Fixed ready-Row and Cell block size. | `Server row_height or 48px` | | `--cui-data-grid-min-width` | `length` | Horizontal overflow threshold. | `Sum of Column widths` | | `--cui-data-grid-background` | `color` | Grid surface. | `Canvas` | | `--cui-data-grid-foreground` | `color` | Primary text. | `CanvasText` | | `--cui-data-grid-muted` | `color` | State and secondary text. | `Accessible CanvasText mix` | | `--cui-data-grid-border-color` | `color` | Row Column and viewport borders. | `Adaptive neutral` | | `--cui-data-grid-header-background` | `color` | Header surface. | `Adaptive neutral` | | `--cui-data-grid-selected-background` | `color` | Selected Row surface. | `Adaptive blue` | | `--cui-data-grid-striped-background` | `color` | Alternate Row surface. | `Subtle neutral` | | `--cui-data-grid-hover-background` | `color` | Pointer Row feedback. | `Subtle Highlight mix` | | `--cui-data-grid-focus-color` | `color` | Active Header and Cell outline. | `Highlight` | | `--cui-data-grid-radius` | `length` | Viewport corners. | `0.625rem` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CDataGrid attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `role` | Native table | `grid` | Exposes one composite data grid. | | `aria-rowcount` | Native table | `integer` | Reports logical Rows plus the Header Row. | | `aria-colcount` | Native table | `integer` | Reports logical Column count. | | `aria-rowindex` | Header and supplied Rows | `positive integer` | Reports exact one-based logical position. | | `aria-colindex` | Headers and Cells | `positive integer` | Reports exact one-based Column position. | | `aria-sort` | Sorted Header | `ascending | descending | absent` | Reflects accepted sort direction. | | `aria-selected` | Supplied Row | `boolean-string | absent` | Reflects accepted selection when selection is enabled. | | `data-row-key` | Supplied Row and Cells | `string` | Exposes stable Row identity. | | `data-column-key` | Header and Cells | `string` | Exposes stable Column identity. | | `data-row-index` | Supplied Row and Cells | `nonnegative integer` | Exposes zero-based logical Row position for owned navigation. | | `data-column-index` | Header and Cells | `nonnegative integer` | Exposes zero-based Column position for owned navigation. | | `data-selected` | Supplied Row | `present | absent` | Reflects accepted selection for styling. | | `data-pending` | Root | `present | absent` | Marks a desired range outside the supplied window. | | `data-selecting` | Root | `present | absent` | Marks an active mouse drag-selection gesture. | | `data-editable` | Root and editable Cells | `present | absent` | Marks that editing is available. | | `data-editing` | Root and active Cell | `present | absent` | Marks the active edit session. | | `data-editor` | Editable Cell | `CDataGridEditor` ([`CDataGridEditor`](#data-grid-interface-editor)) | Reflects the owned editor kind. | | `data-state` | Root | `ready | loading | empty | error` | Reflects settled server output state. | | `tabindex` | Viewport Header or Cell | `0 | -1` | Maintains one composite page Tab stop. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CDataGrid selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="data-grid"]` | Root div | State reflections attrs and theme destination. | | `[data-citry-ui-part="toolbar"]` | Optional toolbar wrapper | Application controls before the viewport. | | `[data-citry-ui-part="status"]` | Visually hidden polite live region | Accepted sort and selection announcements. | | `[data-citry-ui-part="viewport"]` | Scroll div | Horizontal and vertical scroll ownership. | | `[data-citry-ui-part="table"]` | Native table Grid | Semantic and keyboard owner. | | `[data-citry-ui-part="caption"]` | Optional native caption | Supplementary visible description. | | `[data-citry-ui-part="header"]` | thead | Header Row group. | | `[data-citry-ui-part="header-row"]` | Header tr | Exact Header Row position. | | `[data-citry-ui-part="header-cell"]` | th | Navigable sortable Column Header. | | `[data-citry-ui-part="sort-indicator"]` | Decorative span | Accepted sort direction glyph. | | `[data-citry-ui-part="body"]` | tbody | Supplied Rows spacers and state output. | | `[data-citry-ui-part="row"]` | Supplied tr | Stable selection and Row customization. | | `[data-citry-ui-part="cell"]` | Supplied td | Navigable application Cell. | | `[data-citry-ui-part="editor"]` | Runtime-owned input or select | Temporarily edits one checked Cell value. | | `[data-citry-ui-part="loading"]` | Loading td | Localized loading output. | | `[data-citry-ui-part="empty"]` | Empty td | Localized empty output. | | `[data-citry-ui-part="error"]` | Error td | Localized failure output. | | `[data-citry-ui-part="state-row"]` | State tr | Loading empty or error output. | | `[data-citry-ui-part="spacer-row"]` | Presentation tr | Represents omitted fixed-height Rows. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` | | `CDataGridState` | `Literal["ready", "loading", "error"]` | | `CDataGridDensity` | `Literal["comfortable", "compact", "spacious"]` | | `CDataGridSelection` | `Literal["none", "single", "multiple"]` | | `CDataGridAlign` | `Literal["start", "center", "end"]` | | `CDataGridSortDirection` | `Literal["asc", "desc"]` | | `CDataGridSortSource` | `Literal["pointer", "keyboard", "client"]` | | `CDataGridSelectionSource` | `Literal["pointer", "keyboard", "client"]` | | `CDataGridRangeReason` | `Literal["initial", "scroll", "resize", "configuration", "navigation"]` | | `CDataGridEditor` | `Literal["text", "number", "checkbox", "select"]` | | `CDataGridEditSource` | `Literal["pointer", "keyboard"]` |
    #### `CDataGridColumn`
    | Field | Type | Default | Meaning | |---|---|---|---| | `key` | `str` | - | Unique stable Column identity and Row mapping key. | | `label` | `str` | - | Application-localized accessible Header label. | | `sortable` | `bool` | - | Whether Header activation can request sorting. | | `width` | `int` | - | Initial 40 through 2000 CSS-pixel Column width. | | `align` | `CDataGridAlign` ([`CDataGridAlign`](#data-grid-interface-align)) | - | Logical Cell text alignment. | | `header_attrs` | `Mapping[str, object] | None` | - | Copied allowed Header attributes. | | `cell_attrs` | `Mapping[str, object] | None` | - | Copied allowed attributes merged into every Cell in the Column. | | `editable` | `bool` | - | Enables the built-in Cell editor for this Column. | | `editor` | `CDataGridEditor` ([`CDataGridEditor`](#data-grid-interface-editor)) | - | Selects text number checkbox or select control. | | `editor_options` | `Sequence[CDataGridEditOption]` | - | Supplies unique application-localized select options. | | `editor_attrs` | `Mapping[str, str | int | float | bool] | None` | - | Adds checked kind-specific attributes such as min max step maxlength and placeholder. |
    #### `CDataGridCell`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `object` | - | Escaped or component-like server Cell output. | | `attrs` | `Mapping[str, object] | None` | - | Copied allowed attributes merged after Column Cell attributes. | | `editable` | `bool | None` | - | Overrides the Column editing policy for this Cell. |
    #### `CDataGridEditOption`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Unique submitted select value. | | `label` | `str` | - | Application-localized visible option label. | | `disabled` | `bool` | - | Prevents selection of this option. |
    #### `CDataGridRow`
    | Field | Type | Default | Meaning | |---|---|---|---| | `key` | `str` | - | Unique stable supplied-Row identity. | | `cells` | `Mapping[str, object | CDataGridCell]` | - | Exact one-to-one mapping for every Column key. | | `disabled` | `bool` | - | Blocks selection and activation for this Row. | | `attrs` | `Mapping[str, object] | None` | - | Copied allowed Row attributes. |
    #### `CDataGridSort`
    | Field | Type | Default | Meaning | |---|---|---|---| | `key` | `str` | - | Known sortable Column key. | | `direction` | `CDataGridSortDirection` ([`CDataGridSortDirection`](#data-grid-interface-sort-direction)) | - | Accepted ascending or descending direction. |
    #### `CDataGridCaptionSlotData` Empty dataclass: `{}`. #### `CDataGridToolbarSlotData` Empty dataclass: `{}`. #### `CDataGridHeaderSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `column` | `CDataGridColumn` | - | Current Column record. | | `column_index` | `int` | - | Zero-based Column position. | | `sort_direction` | `CDataGridSortDirection | None` | - | Accepted direction or none. | | `sort_priority` | `int | None` | - | One-based multi-sort priority or none. |
    #### `CDataGridCellSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `row` | `CDataGridRow` | - | Current Row record. | | `column` | `CDataGridColumn` | - | Current Column record. | | `cell` | `CDataGridCell` | - | Normalized Cell record. | | `row_index` | `int` | - | Zero-based logical Row position. | | `column_index` | `int` | - | Zero-based Column position. | | `selected` | `bool` | - | Initial accepted supplied-Row selection. |
    #### `CDataGridLoadingSlotData` Empty dataclass: `{}`. #### `CDataGridEmptySlotData` Empty dataclass: `{}`. #### `CDataGridErrorSlotData` Empty dataclass: `{}`. #### `CDataGridSortChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `sort` | `list[dict[str, str]]` | - | Requested ordered sort model. | | `previousSort` | `list[dict[str, str]]` | - | Accepted model before the request. | | `columnKey` | `str` | - | Activated Column key. | | `direction` | `CDataGridSortDirection | None` | - | Requested direction or none when cleared. | | `source` | `CDataGridSortSource` | - | Pointer keyboard or client cause. | | `sourceEvent` | `object | None` | - | Native source Event. |
    #### `CDataGridSelectionChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `selected` | `list[str]` | - | Requested or committed selected supplied-Row keys. | | `previousSelected` | `list[str]` | - | Accepted selection before the request. | | `changed` | `list[str]` | - | Keys whose selection changed. | | `rowKey` | `str | None` | - | Directly activated Row key. | | `selectedRow` | `bool | None` | - | Requested state of the directly activated Row. | | `controlled` | `bool` | - | Whether client selected owns the model. | | `source` | `CDataGridSelectionSource` | - | Pointer keyboard or client cause. | | `sourceEvent` | `object | None` | - | Native source Event. |
    #### `CDataGridRangeChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `startIndex` | `int` | - | Desired inclusive logical start. | | `endIndex` | `int` | - | Desired exclusive logical end. | | `visibleStartIndex` | `int` | - | Estimated visible inclusive start. | | `visibleEndIndex` | `int` | - | Estimated visible exclusive end. | | `requestId` | `int` | - | Monotonic instance-local request ID. | | `reason` | `CDataGridRangeReason` | - | Initial scroll resize configuration or navigation cause. | | `sourceEvent` | `object | None` | - | Native source Event when available. |
    #### `CDataGridCellActivateDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `rowKey` | `str` | - | Activated Row key. | | `columnKey` | `str` | - | Activated Column key. | | `rowIndex` | `int` | - | Zero-based logical Row position. | | `columnIndex` | `int` | - | Zero-based Column position. | | `source` | `keyboard | pointer` | - | Activation cause. | | `sourceEvent` | `object` | - | Native source Event. |
    #### `CDataGridCellEditDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `rowKey` | `str` | - | Edited Row key. | | `columnKey` | `str` | - | Edited Column key. | | `rowIndex` | `int` | - | Zero-based logical Row position. | | `columnIndex` | `int` | - | Zero-based Column position. | | `editor` | `CDataGridEditor` | - | Active native control kind. | | `previousValue` | `str | float | bool` | - | Server-authored value at edit start. | | `source` | `CDataGridEditSource` | - | Keyboard or pointer entry source. | | `reason` | `str` | - | Entry commit or cancellation reason. | | `sourceEvent` | `object` | - | Native source Event. |
    ### 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. #### CDataGrid translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-data-grid-loading` | Labels the loading state. | `None.` | `loading_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-data-grid-empty` | Labels the empty state. | `None.` | `empty_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-data-grid-error` | Labels the error state. | `None.` | `error_label` | Stable `$c-tr` text follows client locale changes. | | `citry-ui-data-grid-sort-ascending` | Announces accepted ascending sorting. | `column: str` | `sort_ascending_label` with `{column}` | One-shot `i18n.tr()` writes the live region after acceptance. | | `citry-ui-data-grid-sort-descending` | Announces accepted descending sorting. | `column: str` | `sort_descending_label` with `{column}` | One-shot `i18n.tr()` writes the live region after acceptance. | | `citry-ui-data-grid-sort-cleared` | Announces accepted cleared sorting. | `column: str` | `sort_cleared_label` with `{column}` | One-shot `i18n.tr()` writes the live region after acceptance. | | `citry-ui-data-grid-selected-one` | Announces one selected supplied Row. | `None.` | `selected_one_label` | One-shot `i18n.tr()` writes the live region after commit or acceptance. | | `citry-ui-data-grid-selected` | Announces multiple selected supplied Rows. | `count: str` | `selected_label` with `{count}` | One-shot `i18n.tr()` writes the live region after commit or acceptance. | | `citry-ui-data-grid-edit` | Names the runtime-owned editor. | `column: str` | `edit_label` with `{column}` | One-shot `i18n.tr()` applies when an editor is created. | | `citry-ui-data-grid-editing` | Announces edit entry. | `column: str` | `editing_label` with `{column}` | One-shot `i18n.tr()` writes the live region. | | `citry-ui-data-grid-edit-submitted` | Announces a submitted edit request. | `column: str` | `edit_submitted_label` with `{column}` | One-shot `i18n.tr()` writes the live region. | | `citry-ui-data-grid-edit-cancelled` | Announces edit cancellation. | `column: str` | `edit_cancelled_label` with `{column}` | One-shot `i18n.tr()` writes the live region. | | `citry-ui-data-grid-edit-invalid` | Announces native validation or callback rejection. | `column: str` | `edit_invalid_label` with `{column}` | One-shot `i18n.tr()` writes the live region. |
    --- # Icon Source: https://citry.dev/ui-library/components/icon/ # Icon Use `CIcon` for a bundled symbol that follows the surrounding text color and size. It renders inline SVG from Citry UI itself, so it needs no font, network request, client runtime, or JavaScript icon package. ## Icon at a glance Icons are decorative by default. Put them beside visible text and let that text carry the meaning. ### Icon at a glance [Open the rendered preview](/ui-library/components/icon/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class IconAtAGlance(Component): template = """
    Canopy survey Search the northern transect
    Silver fern Three new fronds recorded
    Next observation At first light on 14 August
    Specimen verified Matched to the field key
    """ css = """ :where(.icon-glance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 0.75rem; max-width: 68rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.icon-glance article) { display: flex; gap: 0.75rem; align-items: flex-start; min-width: 0; padding: 1rem; border: 1px solid light-dark(#cbd5c0, #3e5b3a); border-radius: 0.75rem; background: Canvas; } :where(.icon-glance [data-citry-ui-part="icon"]) { margin-block-start: 0.1rem; color: light-dark(#2f6f3e, #80d49a); } :where(.icon-glance strong, .icon-glance span) { display: block; } :where(.icon-glance span) { margin-block-start: 0.2rem; color: light-dark(#52604e, #b8c9b5); font-size: 0.875rem; } """ preview = IconAtAGlance() preview # noqa: B018 ```` ```citry-html

    Silver fern

    ``` Compose the same Icon in Python: ```python from citry_ui import CIcon leaf = CIcon(name="leaf") ``` ## Browse the catalog The initial catalog favors common actions, navigation, status, and objects. Semantic aliases such as `success`, `warn`, and `close` keep application code about meaning rather than one particular drawing. ### Browse bundled Icons [Open the rendered preview](/ui-library/components/icon/_previews/catalog/) ````citry from dataclasses import dataclass import citry_ui from citry import Component, citry citry.register_library(citry_ui) @dataclass(frozen=True, slots=True) class IconGroup: title: str names: tuple[str, ...] class IconCatalog(Component): class Kwargs: pass class Slots: pass template = """

    {{ group.title }}

    • {{ name }}
    """ css = """ :where(.icon-catalog) { display: grid; gap: 1.25rem; max-width: 72rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.icon-catalog h2) { margin: 0 0 0.6rem; color: light-dark(#285c36, #8bdd9f); font-size: 0.875rem; letter-spacing: 0.06em; text-transform: uppercase; } :where(.icon-catalog ul) { display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); gap: 0.35rem; margin: 0; padding: 0; list-style: none; } :where(.icon-catalog li) { display: flex; gap: 0.55rem; align-items: center; min-width: 0; padding: 0.55rem 0.65rem; border: 1px solid light-dark(#d9e2d4, #38513a); border-radius: 0.5rem; background: Canvas; } :where(.icon-catalog code) { overflow-wrap: anywhere; font-size: 0.75rem; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, object]: # noqa: ARG002 return { "groups": ( IconGroup( "Actions", ( "check", "close", "copy", "download", "edit", "plus", "minus", "refresh-cw", "search", "trash", "upload", ), ), IconGroup( "Navigation", ( "arrow-down", "arrow-left", "arrow-right", "arrow-up", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "back", "forward", "prev", "next", "external-link", "home", "menu", "more-horizontal", "more-vertical", ), ), IconGroup( "Status and meaning", ( "circle-check", "circle-help", "circle-info", "circle-x", "triangle-alert", "success", "info", "warn", "danger", "expand", "collapse", "dropdown", "clear", ), ), IconGroup( "Objects", ( "calendar", "clock", "eye", "eye-off", "file", "folder", "heart", "leaf", "link", "lock", "mail", "settings", "star", "unlock", "user", "x", ), ), ) } preview = IconCatalog() preview # noqa: B018 ```` Names are a versioned public contract. Unknown names fail during server render instead of leaving a blank placeholder. ## Match size and color `sm`, `md`, and `lg` scale with nearby type. Icons inherit `currentColor`, so ordinary text color utilities and component intent colors work without an Icon-specific color input. ### Set Icon size and color [Open the rendered preview](/ui-library/components/icon/_previews/size-and-color/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class IconSizeAndColor(Component): template = """

    Preset sizes

    Small seedling

    Mature frond

    Canopy specimen

    Inherited color

    Spring

    Summer

    Autumn

    Exact local override

    Alpine frond

    """ css = """ :where(.icon-scale) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1rem; max-width: 64rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.icon-scale article) { padding: 1rem; border: 1px solid light-dark(#d4ddce, #40533e); border-radius: 0.75rem; background: Canvas; } :where(.icon-scale h2) { margin: 0 0 0.75rem; font-size: 0.9rem; } :where(.icon-scale p) { display: flex; gap: 0.55rem; align-items: center; margin: 0.6rem 0; } :where(.icon-scale__spring) { color: #16a34a; } :where(.icon-scale__summer) { color: #15803d; } :where(.icon-scale__autumn) { color: #c2410c; } """ preview = IconSizeAndColor() preview # noqa: B018 ```` Set `--cui-icon-size` for an exact local size. Use `class_` and `style` directly for routine root styling. ## Give standalone Icons meaning Pass `label` only when the Icon must communicate without nearby text. It adds `role="img"` and `aria-label`. Without `label`, the Icon has `aria-hidden="true"`. ### Choose decorative or meaningful semantics [Open the rendered preview](/ui-library/components/icon/_previews/meaning/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class IconMeaning(Component): template = """

    Visible text carries meaning

    Frost is expected above the tree line.

    aria-hidden="true"

    Icon stands alone

    role="img" aria-label="Good growing conditions"
    """ css = """ :where(.icon-meaning) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; max-width: 58rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.icon-meaning article) { padding: 1rem; border: 1px solid light-dark(#d8dac7, #55563a); border-radius: 0.75rem; background: Canvas; } :where(.icon-meaning h2) { margin: 0 0 0.75rem; font-size: 0.95rem; } :where(.icon-meaning__notice) { display: flex; gap: 0.6rem; align-items: center; color: light-dark(#9a3412, #fdba74); } :where(.icon-meaning__weather) { display: grid; place-items: center; min-block-size: 4rem; color: light-dark(#15803d, #86efac); font-size: 2rem; } :where(.icon-meaning code) { font-size: 0.72rem; overflow-wrap: anywhere; } """ preview = IconMeaning() preview # noqa: B018 ```` Do not repeat visible text in `label`. An Icon never enters the focus order and does not own a click action. ## Compose Icons with controls Put Icons inside the decoration slots of the component that owns the action. The Button keeps the accessible name, focus, keyboard behavior, loading state, and target size. ### Compose Icons with Buttons [Open the rendered preview](/ui-library/components/icon/_previews/composition/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class IconComposition(Component): template = """
    Search specimens Save field note Next trail

    The western footbridge is closed after rain.

    """ css = """ :where(.icon-composition) { display: grid; gap: 1rem; max-width: 62rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.icon-composition__actions) { display: flex; flex-wrap: wrap; gap: 0.65rem; align-items: center; } :where(.icon-composition__warning) { display: flex; gap: 0.55rem; align-items: center; width: fit-content; margin: 0; padding: 0.75rem 0.9rem; border-inline-start: 0.25rem solid light-dark(#d97706, #fbbf24); color: light-dark(#78350f, #fde68a); background: light-dark(#fffbeb, #451a03); } """ preview = IconComposition() preview # noqa: B018 ```` For an icon-only action, name the Button through its `attrs`. Do not attach an event listener or `tabindex` to `CIcon`. ## Use physical and logical direction Physical names such as `arrow-left` always point the same way. Logical names `back`, `forward`, `prev`, and `next` mirror automatically in right-to-left content. ### Compare physical and logical direction [Open the rendered preview](/ui-library/components/icon/_previews/direction/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class IconDirection(Component): template = """

    Left to right

    Physical left
    Back
    Forward
    Next

    Right to left

    Physical left
    Back
    Forward
    Next
    """ css = """ :where(.icon-direction) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; max-width: 52rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.icon-direction article) { padding: 1rem; border: 1px solid light-dark(#d4ddce, #40533e); border-radius: 0.75rem; background: Canvas; } :where(.icon-direction h2) { margin: 0 0 0.8rem; font-size: 0.95rem; } :where(.icon-direction dl) { display: grid; gap: 0.45rem; margin: 0; } :where(.icon-direction dl div) { display: flex; justify-content: space-between; gap: 1rem; padding: 0.4rem 0.55rem; border-radius: 0.4rem; background: light-dark(#f1f6ee, #233526); } :where(.icon-direction dt) { font-size: 0.85rem; } :where(.icon-direction dd) { margin: 0; color: light-dark(#236538, #7bd596); } """ preview = IconDirection() preview # noqa: B018 ```` Choose a logical name for reading or navigation order. Choose a physical name when the direction itself is the content, such as a compass or diagram. ## Theme and customize Icon Icon follows the surrounding `color-scheme`. Override the two documented CSS variables on an ancestor or one Icon; use the public part selector for targeted root styling. ### Customize Icon [Open the rendered preview](/ui-library/components/icon/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class IconCustomization(Component): template = """

    Day survey key

    Native species

    Identity uncertain

    Habitat under pressure

    Night survey key

    Native species

    Identity uncertain

    Habitat under pressure

    """ css = """ :where(.field-keys) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; max-width: 54rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.field-key) { --cui-icon-size: 1.25rem; --cui-icon-stroke-width: 1.6; padding: 1rem; border: 1px solid currentColor; border-radius: 0.75rem; } :where(.field-key--light) { color-scheme: light; color: #20452a; background: #f4f9f1; } :where(.field-key--dark) { color-scheme: dark; color: #d7f3dc; background: #172b1c; } :where(.field-key h2) { margin: 0 0 0.8rem; font-size: 0.95rem; } :where(.field-key p) { display: flex; gap: 0.65rem; align-items: center; margin: 0.6rem 0; } :where(.field-key [data-citry-ui-part="icon"]) { color: light-dark(#15803d, #86efac); } :where(.field-key [data-name="warn"]) { color: light-dark(#b45309, #fcd34d); } """ preview = IconCustomization() preview # noqa: B018 ```` ```css .field-key { --cui-icon-size: 1.4rem; --cui-icon-stroke-width: 1.6; } .field-key [data-citry-ui-part="icon"] { color: #15803d; } ``` The documented variables, part, and reflected attributes are public CSS API. `.cui-*` classes and `--_cui-*` variables are private. ## Accessibility and security Decorative and meaningful semantics are decided on the server and work without JavaScript. The SVG is non-interactive, ignores pointer events, and contains only reviewed package-owned geometry. `attrs` accepts inert metadata but rejects executable Alpine and Citry directives, event attributes, geometry, focus controls, and accessible-name overrides. Citry runtime data namespaces are reserved. Trusted `Markup`/`__html__` values are rejected across every input, including nested class, style, and attribute structures. `CIcon` is not a raw SVG escape hatch. ## API reference ### Inputs #### CIcon server inputs Server inputs are passed in a template through `` or in Python through `CIcon(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `name` | `CIconName` ([`CIconName`](#icon-interface-input-type-aliases-cicon-name)) | required | Selects one bundled visual glyph or semantic alias. | | `label` | `str | None` | `None` | Gives a standalone Icon `img` semantics and this escaped accessible name. Omit it when visible text already explains the Icon. Trusted HTML values are rejected. | | `size` | `"sm" | "md" | "lg"` ([`CIconSize`](#icon-interface-input-type-aliases-cicon-size)) | `"md"` | Sets the Icon to 0.875em, 1em, or 1.25em before CSS-variable overrides. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#icon-interface-input-type-aliases-class-value)) | `None` | Adds root SVG classes from a string, conditional mapping, or nested sequence and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#icon-interface-input-type-aliases-style-value)) | `None` | Adds root SVG inline styles from CSS text, a property mapping, or nested sequence and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds inert root SVG metadata such as `id`, `lang`, `dir`, `hidden`, `aria-describedby`, `aria-details`, and consumer `data-*` attributes. Geometry, naming, focus, executable directives, event bindings, reserved Citry runtime attributes, and trusted HTML values at any supported nesting depth are rejected. |
    ### Slots - ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CIcon CSS variables Apply these variables to `CIcon` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-icon-size` | `length` | Overrides the rendered inline and block size. | `Size-derived em length.` | | `--cui-icon-stroke-width` | `number` | Overrides the Lucide line weight. | `2` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CIcon attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-name` | Root SVG | `CIconName` | Reflects the requested public glyph or alias name. | | `data-size` | Root SVG | `"sm" | "md" | "lg"` | Reflects the size preset before CSS-variable overrides. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CIcon selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="icon"]` | Root SVG | Stable Icon root and `attrs` destination. |
    ### 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]` | | `CIconSize` | `Literal["sm", "md", "lg"]` | | `CIconName` | `Literal["arrow-down", "arrow-left", "arrow-right", "arrow-up", "calendar", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "circle-check", "circle-help", "circle-info", "circle-x", "clock", "copy", "download", "edit", "external-link", "eye", "eye-off", "file", "folder", "heart", "home", "leaf", "link", "lock", "mail", "menu", "minus", "more-horizontal", "more-vertical", "plus", "refresh-cw", "search", "settings", "star", "trash", "triangle-alert", "unlock", "upload", "user", "x", "back", "forward", "prev", "next", "close", "clear", "success", "info", "warn", "danger", "expand", "collapse", "dropdown"]` |
    ### Translation keys - --- # Image Source: https://citry.dev/ui-library/components/image/ # Image Use `CImage` when content needs one native image with an explicit text alternative, intrinsic dimensions, responsive candidates, and optional visual loading or error treatments. The browser still owns fetching, candidate selection, decoding, caching, CSP, CORS, and native image behavior. Use the [WAI alternative-text decision tree](https://www.w3.org/WAI/tutorials/images/decision-tree/) when the image's purpose is not obvious. ## Start with alternative text and geometry `src`, `alt`, `width`, and `height` are required. Use concise meaningful text for informative images. Use `alt=""` only when the image is truly decorative or repeats nearby content. The dimensions reserve the native aspect ratio before bytes arrive and do not force the final CSS size. ### Render a native image with stable geometry [Open the rendered preview](/ui-library/components/image/_previews/basic-image/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CImage citry.register_library(citry_ui) class BasicImage(Component): class Kwargs: pass class Slots: pass def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, Any]: return { "python_image": CImage( src="/static/img/ui/image/orion-nebula-1280.jpg", alt="Orion Nebula, captured from Northstar Ridge", width=1280, height=720, loading="eager", ) } template = """

    Template composition

    Python composition

    {{ python_image }}

    Both forms keep one native image semantic, required alternative text, and a reserved 16:9 box before the files finish loading.

    """ css = """ :where(.image-basic) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-basic article) { display: grid; gap: 0.5rem; } :where(.image-basic h3, .image-basic p) { margin: 0; } :where(.image-basic p) { grid-column: 1 / -1; } :where(.image-basic [data-citry-ui-part="image-root"]) { inline-size: 100%; } """ preview = BasicImage() preview # noqa: B018 ```` Choose the alternative for the image's purpose in context. An image-only link needs destination text. A complex chart needs an adjacent data equivalent. A caption does not replace `alt`. ### Compare informative, decorative, functional, and complex images [Open the rendered preview](/ui-library/components/image/_previews/alternative-text/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImageAlternativeText(Component): template = """

    Informative

    The alternative conveys the observation's useful content.

    Decorative

    The nearby heading already supplies all meaning, so alt is empty.

    Functional

    The image is the link's only content, so alt names the destination.

    Complex observation

    Exposure measurements along the lunar terminator.
    Equivalent exposure data
    RegionSeconds
    North rim0.008
    South basin0.013
    """ css = """ :where(.image-alt) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-alt article) { display: grid; gap: 0.5rem; align-content: start; } :where(.image-alt h3, .image-alt p, .image-alt figure) { margin: 0; } :where(.image-alt [data-citry-ui-part="image-root"]) { inline-size: 100%; } :where(.image-alt a:focus-visible) { outline: 3px solid Highlight; outline-offset: 3px; } :where(.image-alt table) { border-collapse: collapse; font-size: 0.8rem; } :where(.image-alt th, .image-alt td) { border: 1px solid GrayText; padding: 0.25rem; } """ preview = ImageAlternativeText() preview # noqa: B018 ```` ## Size and crop the rendered image Use ordinary CSS to constrain rendered size. `fit` and `position` control the pixels inside that box. Native `width` and `height` remain intrinsic metadata. Public variables can override the aspect ratio, crop, position, radius, and state colors without relying on private classes. ### Compare stable geometry and object fit [Open the rendered preview](/ui-library/components/image/_previews/fit-and-geometry/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImageFitAndGeometry(Component): template = """

    Contain

    Cover, left focus

    Scale down

    """ css = """ :where(.image-fit-grid) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-fit-grid article) { display: grid; gap: 0.5rem; } :where(.image-fit-grid h3) { margin: 0; } :where(.image-fit-grid__frame) { inline-size: 100%; --cui-image-aspect-ratio: 4 / 3; --cui-image-background: light-dark(#e7e5e4, #1c1917); } :where(.image-fit-grid__square) { --cui-image-aspect-ratio: 1 / 1; } """ preview = ImageFitAndGeometry() preview # noqa: B018 ```` ## Author responsive sources Pass ordered frozen `CImageSource` records to emit a native ``. The records are data, not component declarations. Native first-match order matters. Width-descriptor `srcset` requires `sizes`, and arbitrary `media` text remains browser syntax that the application must validate. ### Use srcset, sizes, art direction, and AVIF [Open the rendered preview](/ui-library/components/image/_previews/responsive-sources/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CImage, CImageSource citry.register_library(citry_ui) class ResponsiveImageSources(Component): class Kwargs: pass class Slots: pass def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, Any]: sources = ( CImageSource( media="(max-width: 47.99rem)", srcset="/static/img/ui/image/observatory-portrait-640.jpg", width=640, height=960, ), CImageSource( media="(min-width: 64rem)", type="image/avif", srcset="/static/img/ui/image/observatory-wide-1280.avif", width=1280, height=720, ), ) return { "sources": sources, "responsive_srcset": ( "/static/img/ui/image/observatory-portrait-640.jpg 640w, " "/static/img/ui/image/observatory-wide-1280.jpg 1280w" ), "python_image": CImage( src="/static/img/ui/image/observatory-wide-1280.jpg", alt="Northstar Observatory beneath the Milky Way", width=1280, height=720, srcset=( "/static/img/ui/image/observatory-portrait-640.jpg 640w, " "/static/img/ui/image/observatory-wide-1280.jpg 1280w" ), sizes="(max-width: 48rem) 100vw, 48rem", sources=sources, ), } template = """

    Resize across 48rem and 64rem. The browser selects the first matching source, then the final image candidates.

    Template-fed records

    Python records

    {{ python_image }}
    Selected local fixture: Waiting for native selection
    """ css = """ :where(.image-responsive) { display: grid; gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-responsive__pair) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; } :where(.image-responsive article) { display: grid; gap: 0.5rem; } :where(.image-responsive h3, .image-responsive p) { margin: 0; } :where(.image-responsive [data-citry-ui-part="image-root"]) { inline-size: 100%; } """ preview = ResponsiveImageSources() preview # noqa: B018 ```` ## Choose native loading and priority hints Use `loading="eager"` and `fetch_priority="high"` only for a genuinely important above-fold image. Keep ordinary archive media at native lazy or auto priority. Image adds no observer, data-src indirection, preload, or custom decode gate, so the resource stays discoverable in server HTML. ### Compare eager and lazy native loading [Open the rendered preview](/ui-library/components/image/_previews/loading-priority/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImageLoadingPriority(Component): template = """

    Above the fold

    Tonight's featured field

    Reserve high priority for the small number of likely LCP images.

    Below the fold

    Archive plate

    Lazy loading remains a native hint and needs no data-src indirection.

    """ css = """ :where(.image-priority) { display: grid; gap: 1rem; max-inline-size: 46rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-priority article) { display: grid; gap: 0.5rem; } :where(.image-priority h3, .image-priority p) { margin: 0; } :where(.image-priority__eyebrow) { color: GrayText; font-weight: 700; } :where(.image-priority__spacer) { block-size: 70vh; border-block: 1px dashed GrayText; } :where(.image-priority [data-citry-ui-part="image-root"]) { inline-size: 100%; } """ preview = ImageLoadingPriority() preview # noqa: B018 ```` ## Add visual loading and error treatments The `placeholder` and `fallback` slots are inert visual layers. They never replace the native `` or its `alt`. With JavaScript disabled, both custom layers stay hidden and the browser shows the native image or broken-image text fallback. Put meaningful error copy, retry controls, and live announcements outside Image. ### Handle loading, error, and recovery [Open the rendered preview](/ui-library/components/image/_previews/placeholder-and-error/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImagePlaceholderAndError(Component): template = """

    Visual placeholder and fallback

    Plate unavailable
    Normalized status: waiting

    Native broken-image fallback

    No custom fallback is supplied, so native broken rendering and alt remain.

    """ css = """ :where(.image-feedback) { display: grid; gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-feedback__controls) { display: flex; flex-wrap: wrap; gap: 0.5rem; } :where(.image-feedback__grid) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); gap: 1rem; } :where(.image-feedback article) { display: grid; gap: 0.5rem; align-content: start; } :where(.image-feedback h3, .image-feedback p) { margin: 0; } :where(.image-feedback [data-citry-ui-part="image-root"]) { inline-size: 100%; } :where(.image-feedback__placeholder, .image-feedback__fallback) { display: grid; place-items: center; inline-size: 100%; block-size: 100%; } :where(.image-feedback__fallback) { padding: 1rem; font-weight: 700; } @media (forced-colors: active) { :where(.image-feedback__fallback) { border: 1px solid CanvasText; } } @media print { :where(.image-feedback__controls, .image-feedback output) { display: none; } } """ preview = ImagePlaceholderAndError() preview # noqa: B018 ```` ## Observe and update a request Client props can update the resource, semantics, dimensions, hints, fit, and callback. `onStatusChange` reports normalized `loading`, `loaded`, and `error` settlement plus `current_src`, `natural_width`, and `natural_height`. The `current_src` value snapshots the native `currentSrc`; treat it as potentially sensitive application data and redact it before logging. Responsive settlement follows native event truth. A browser may select a broken `` candidate without emitting `error`; in that case Image keeps the last accepted status and callback ledger. It does not invent an observer or synthetic failure signal. Native `@load` and `@error` listeners belong in `img_attrs`. Those events do not bubble to root `attrs`. Native listeners run in isolated expression scope, where `$event`, `$store`, `$dispatch`, and globals work but an ancestor's local `x-data` identifiers do not cross the component boundary. The component callback is the owner-local surface and also covers cached completion. ### Switch resources and inspect normalized status [Open the rendered preview](/ui-library/components/image/_previews/reactive-image/) ````citry from typing import Any import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ReactiveImage(Component): class Kwargs: pass class Slots: pass def template_data( self, kwargs: Kwargs, # noqa: ARG002 slots: Slots, # noqa: ARG002 ) -> dict[str, Any]: return { "image_attrs": { "@load": "$dispatch('image-native-load')", "@error": "$dispatch('image-native-error')", "data-native-events": "bridged", } } template = """
    Survey frame unavailable Status waiting; selected none; callbacks 0; native load/error 0/0

    The output redacts paths to filenames. Native events use an img_attrs $dispatch bridge; onStatusChange is the owner-local cached-race surface.

    """ css = """ :where(.image-reactive) { display: grid; gap: 1rem; max-inline-size: 44rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-reactive__controls) { display: flex; flex-wrap: wrap; gap: 0.5rem; } :where(.image-reactive p) { margin: 0; } :where(.image-reactive [data-citry-ui-part="image-root"]) { inline-size: 100%; } """ preview = ReactiveImage() preview # noqa: B018 ```` ## Compose with native and Citry structure Image is not a figure, Card, link, button, Skeleton, lightbox, or gallery. Wrap it in those structures when they own the semantic job. A neighboring Skeleton remains decorative, while the real image retains its alternative text. ### Compose Image with Card, Skeleton, figure, and link [Open the rendered preview](/ui-library/components/image/_previews/image-composition/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImageComposition(Component): template = """

    Northstar Ridge

    A clear archive exposure from the western dome.

    Loading layout reservation

    Exposure notes: 0.008 seconds at the north rim.
    """ css = """ :where(.image-composition) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-composition h3, .image-composition figure) { margin: 0; } :where(.image-composition [data-citry-ui-part="image-root"]) { inline-size: 100%; } :where(.image-composition__pair) { display: grid; gap: 0.5rem; } :where(.image-composition__link) { align-self: start; border-radius: 0.75rem; } :where(.image-composition__link:focus-visible) { outline: 3px solid Highlight; outline-offset: 3px; } """ preview = ImageComposition() preview # noqa: B018 ```` ## Keep delivery policy explicit `cross_origin` and `referrer_policy` select native request modes; they do not grant canvas access or repair server headers. `img-src` CSP remains authoritative. Relative, HTTP, HTTPS, data, blob, raster, and SVG image URLs are consumer-owned resource references, not sanitized or fetched by Citry. Active `javascript:` and `vbscript:` schemes are rejected. Blob lifetime, data-URL size, metadata privacy, origin policy, and remote tracking remain application responsibilities. ### Review CORS, referrer policy, CSP, and URL trust [Open the rendered preview](/ui-library/components/image/_previews/delivery-and-security/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImageDeliveryAndSecurity(Component): template = """

    Same-origin archive

    Native request policy applies before src. Citry does not proxy bytes.

    Credential-free CORS mode

    Cross-origin plate unavailable in this preview

    The response still needs matching server headers for CORS use.

    CSP and unavailable resources

    Blocked or unavailable plate

    Browser CSP remains authoritative and failures settle as image error.

    """ css = """ :where(.image-delivery) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); gap: 1rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-delivery article, .image-delivery aside) { display: grid; gap: 0.5rem; align-content: start; } :where(.image-delivery h3, .image-delivery p, .image-delivery ul) { margin: 0; } :where(.image-delivery [data-citry-ui-part="image-root"]) { inline-size: 100%; } """ preview = ImageDeliveryAndSecurity() preview # noqa: B018 ```` ## Understand lifecycle and fallback Equal retained-node server morphs preserve the active request and status. Changing request fields starts one new generation. Replacing the native image, removing the owner, invalid structure, a closed ShadowRoot, or cross-document adoption requires fresh ownership. Late work from an old generation cannot notify a replacement owner. ### Inspect retained, replaced, removed, and restored images [Open the rendered preview](/ui-library/components/image/_previews/image-lifecycle/) ````citry from __future__ import annotations import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ImageLifecycle(Component): class Kwargs: step: int = 0 class Slots: pass class Events: def retain(self) -> ImageLifecycle: return ImageLifecycle(step=1) def change_resource(self) -> ImageLifecycle: return ImageLifecycle(step=2) def replace(self) -> ImageLifecycle: return ImageLifecycle(step=3) def remove(self) -> ImageLifecycle: return ImageLifecycle(step=4) def restore(self) -> ImageLifecycle: return ImageLifecycle(step=5) def remove_again(self) -> ImageLifecycle: return ImageLifecycle(step=6) def restore_again(self) -> ImageLifecycle: return ImageLifecycle(step=7) def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, object]: # noqa: ARG002 source = "/static/img/ui/image/horsehead-nebula-1280.jpg?plate=baseline" if kwargs.step >= 2: source = "/static/img/ui/image/orion-nebula-1280.jpg?plate=changed" image_key = "image-lifecycle-retained" if kwargs.step < 3 else f"image-lifecycle-{kwargs.step}" return { "image_key": image_key, "include_image": kwargs.step not in {4, 6}, "source": source, "step": kwargs.step, } template = """

    Signed server step: {{ step }}

    Loading calibration plate Calibration plate unavailable Status waiting; selected none
    """ css = """ :where(.image-lifecycle) { display: grid; gap: 1rem; max-inline-size: 44rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.image-lifecycle__controls) { display: flex; flex-wrap: wrap; gap: 0.5rem; } :where(.image-lifecycle p) { margin: 0; } :where(.image-lifecycle [data-citry-ui-part="image-root"]) { inline-size: 100%; } """ preview = ImageLifecycle() preview # noqa: B018 ```` Without JavaScript, the server-rendered native image, ordered responsive sources, required `alt`, dimensions, loading hints, CORS mode, and referrer policy remain useful. Custom placeholder and fallback slots stay hidden so they cannot cover the native result. Image is not form-associated and adds no keyboard, focus, overlay, gesture, retry, upload, editing, canvas, image-map, or lightbox behavior. Important print images should use eager loading because printing does not guarantee a lazy request will start before pagination. ## API reference ### Inputs #### CImage server inputs Server inputs are passed in a template through `` or in Python through `CImage(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `src` | `str` | required | Sets the required nonempty escaped fallback image URL. | | `alt` | `str` | required | Sets the required native text alternative; an exact empty string is an intentional decorative choice. | | `width` | `positive int` | required | Sets native intrinsic width and reserves geometry; bool is rejected. | | `height` | `positive int` | required | Sets native intrinsic height and reserves geometry; bool is rejected. | | `srcset` | `str | None` | `None` | Adds native responsive candidates; width descriptors require sizes. | | `sizes` | `str | None` | `None` | Adds native image source sizes; auto sizes require lazy loading. | | `sources` | `Sequence[CImageSource]` | () | Snapshots up to 32 ordered source records and emits picture only when nonempty. | | `loading` | `"eager" | "lazy"` ([`CImageLoading`](#image-interface-loading)) | `"eager"` | Selects the native loading hint. | | `decoding` | `"auto" | "sync" | "async"` ([`CImageDecoding`](#image-interface-decoding)) | `"auto"` | Selects the native decoding hint. | | `fetch_priority` | `"auto" | "high" | "low"` ([`CImageFetchPriority`](#image-interface-fetch-priority)) | `"auto"` | Selects native fetchpriority; the application owns scarcity policy. | | `cross_origin` | `CImageCrossOrigin | None` ([`CImageCrossOrigin`](#image-interface-cross-origin)) | `None` | Selects anonymous or credentialed native CORS mode. | | `referrer_policy` | `CImageReferrerPolicy | None` ([`CImageReferrerPolicy`](#image-interface-referrer-policy)) | `None` | Selects the native image request referrer policy. | | `fit` | `"contain" | "cover" | "fill" | "none" | "scale-down"` ([`CImageFit`](#image-interface-fit)) | `"contain"` | Sets effective object fit and the root mirror. | | `position` | `str` | `"50% 50%"` | Sets validated object-position text; the browser owns final CSS grammar. | | `draggable` | `bool` | `False` | Sets the exact native draggable reflection. | | `onStatusChange` | `browser callback | None` | `None` | Sets the owner-local normalized status callback. | | `class_` | `CClassValue | None` ([`CClassValue`](#image-interface-class-value)) | `None` | Adds root classes and merges them with attrs. | | `style` | `CStyleValue | None` ([`CStyleValue`](#image-interface-style-value)) | `None` | Adds root styles and merges them with attrs before owned style fallbacks. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed neutral-root attributes and isolated-scope native listeners. | | `img_attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed native image attributes and load or error listeners without replacing owned resources, semantics, dimensions, or policy. |
    #### CImage client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `src` | `str` | Uses the immutable server baseline; null has the same effect. | Replaces the fallback URL and begins a new request generation. | | `alt` | `str` | Uses the immutable server baseline; null has the same effect. | Updates the native alternative without creating a second semantic owner. | | `width` | `positive integer` | Uses the immutable server baseline; null has the same effect. | Updates native intrinsic width. | | `height` | `positive integer` | Uses the immutable server baseline; null has the same effect. | Updates native intrinsic height. | | `srcset` | `string | null` | Uses the immutable server baseline. | Updates final-image candidates and begins a new request generation when effective selection metadata changes. | | `sizes` | `string | null` | Uses the immutable server baseline. | Updates final-image sizes and request selection. | | `loading` | `"eager" | "lazy"` ([`CImageLoading`](#image-interface-loading)) | Uses the immutable server baseline; null has the same effect. | Updates the native loading hint without fabricating settlement. | | `decoding` | `"auto" | "sync" | "async"` ([`CImageDecoding`](#image-interface-decoding)) | Uses the immutable server baseline; null has the same effect. | Updates the native decoding hint. | | `fetchPriority` | `"auto" | "high" | "low"` ([`CImageFetchPriority`](#image-interface-fetch-priority)) | Uses the immutable server baseline; null has the same effect. | Updates native fetch priority. | | `crossOrigin` | `CImageCrossOrigin | null` ([`CImageCrossOrigin`](#image-interface-cross-origin)) | Uses the immutable server baseline. | Updates or clears native CORS mode before a resource write. | | `referrerPolicy` | `CImageReferrerPolicy | null` ([`CImageReferrerPolicy`](#image-interface-referrer-policy)) | Uses the immutable server baseline. | Updates or clears native referrer policy before a resource write. | | `fit` | `"contain" | "cover" | "fill" | "none" | "scale-down"` ([`CImageFit`](#image-interface-fit)) | Uses the immutable server baseline; null has the same effect. | Updates object fit and the root reflection. | | `position` | `string` | Uses the immutable server baseline; null has the same effect. | Updates validated object-position text. | | `draggable` | `boolean` | Uses the immutable server baseline; null has the same effect. | Updates native draggable. | | `onStatusChange` | `function` | Uses the server callback. | Replaces the owner-local normalized status callback; null clears it. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CImage slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `placeholder` | no | `none` | No custom loading layer; native pending rendering remains visible. | | `fallback` | no | `none` | No custom error layer; native broken-image rendering and alt remain visible. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CImage events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onStatusChange` | `(detail: CImageStatusChangeDetail) => void` ([`CImageStatusChangeDetail`](#image-interface-cimage-status-change-detail)) | Initial loading ownership, a new accepted request generation, cached completion, or matching trusted native success or error settlement for a selected currentSrc change. | `{status, src, current_src, natural_width, natural_height}` ([`CImageStatusChangeDetail`](#image-interface-cimage-status-change-detail)) | Runs after native attributes and public mirrors synchronize; it is not cancelable and stale generations cannot notify. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CImage CSS variables Apply these variables to `CImage` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-image-aspect-ratio` | `positive CSS ratio or auto` | Overrides the rendered native image ratio without changing intrinsic metadata. | `auto` | | `--cui-image-fit` | `object-fit value` | Overrides the effective fit input for pixels inside the media box. | `Effective fit input` | | `--cui-image-position` | `object-position value` | Overrides effective pixel position inside the media box. | `Effective position input` | | `--cui-image-radius` | `length or percentage` | Media box corner radius. | `var(--cui-radius-md)` | | `--cui-image-background` | `color or image` | Loading and native contain-area background. | `transparent` | | `--cui-image-fallback-color` | `color` | Visual fallback foreground. | `var(--cui-color-muted-fg)` | | `--cui-image-fallback-background` | `color or image` | Visual fallback background. | `var(--cui-color-muted-bg)` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CImage attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-status` | Image root | `"loading" | "loaded" | "error"` ([`CImageStatus`](#image-interface-status)) | Mirrors the last accepted normalized status after readiness; a silent browser candidate change does not invent settlement. | | `data-fit` | Image root | `"contain" | "cover" | "fill" | "none" | "scale-down"` ([`CImageFit`](#image-interface-fit)) | Mirrors effective configured fit before a public CSS variable override. | | `data-has-placeholder` | Image root | `present | absent` | Reports whether placeholder slot content exists. | | `data-has-fallback` | Image root | `present | absent` | Reports whether fallback slot content exists. | | `data-citry-image-initialized` | Image root | `present | absent` | Marks a live settled runtime owner; the copyable attribute alone is not authority. |
    #### CImage attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `alt` | Native img | `string` | Required native alternative text and sole image semantic name. | | `width` | Native img or source | `positive integer` | Native intrinsic width metadata. | | `height` | Native img or source | `positive integer` | Native intrinsic height metadata. | | `src` | Native img | `nonempty URL string` | Required fallback request URL. | | `srcset` | Native img or source | `native candidate string` | Browser-owned responsive candidate set. | | `sizes` | Native img or source | `native sizes string` | Browser-owned rendered-size hint. | | `media` | Native source | `media query string` | Browser-owned art-direction discriminator. | | `type` | Native source | `image MIME essence` | Browser-owned format discriminator. | | `loading` | Native img | `"eager" | "lazy"` ([`CImageLoading`](#image-interface-loading)) | Native request scheduling hint. | | `decoding` | Native img | `"auto" | "sync" | "async"` ([`CImageDecoding`](#image-interface-decoding)) | Native decode scheduling hint. | | `fetchpriority` | Native img | `"auto" | "high" | "low"` ([`CImageFetchPriority`](#image-interface-fetch-priority)) | Native relative fetch-priority hint. | | `crossorigin` | Native img | `"anonymous" | "use-credentials" | absent` ([`CImageCrossOrigin`](#image-interface-cross-origin)) | Native CORS request mode. | | `referrerpolicy` | Native img | `CImageReferrerPolicy | absent` ([`CImageReferrerPolicy`](#image-interface-referrer-policy)) | Native request referrer policy. | | `draggable` | Native img | `"true" | "false"` | Exact native drag reflection. | | `hidden` | Placeholder or fallback wrapper | `present in server output` | Keeps custom visual layers from obscuring native no-JavaScript output. | | `aria-hidden` | Placeholder or fallback wrapper | `"true"` | Keeps visual slot copy out of the accessibility tree. | | `inert` | Placeholder or fallback wrapper | `present` | Prevents visual slot descendants from becoming interaction surfaces. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CImage selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="image-root"]` | Neutral root span | Lifecycle owner and class_, style, and attrs destination. | | `[data-citry-ui-part="picture"]` | Native picture | Ordered responsive source-selection context present only when sources is nonempty. | | `[data-citry-ui-part="image"]` | Sole native img | Semantic and request owner plus img_attrs destination. | | `[data-citry-ui-part="placeholder"]` | Inert visual span | Optional loading-only visual layer. | | `[data-citry-ui-part="fallback"]` | Inert visual span | Optional error-only visual layer. |
    ### 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]` | | `CImageFit` | `Literal["contain", "cover", "fill", "none", "scale-down"]` | | `CImageLoading` | `Literal["eager", "lazy"]` | | `CImageDecoding` | `Literal["auto", "sync", "async"]` | | `CImageFetchPriority` | `Literal["auto", "high", "low"]` | | `CImageCrossOrigin` | `Literal["anonymous", "use-credentials"]` | | `CImageReferrerPolicy` | `Literal["no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin", "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url"]` | | `CImageStatus` | `Literal["loading", "loaded", "error"]` |
    #### `CImageSource`
    | Field | Type | Default | Meaning | |---|---|---|---| | `srcset` | `str` | - | Required native candidate string for one ordered source. | | `media` | `str | None` | - | Optional native media discriminator. | | `type` | `str | None` | - | Optional image MIME essence discriminator. | | `sizes` | `str | None` | - | Optional native source sizes; required for width descriptors. | | `width` | `positive int | None` | - | Optional native source width paired with height. | | `height` | `positive int | None` | - | Optional native source height paired with width. |
    #### `CImageStatusChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `status` | `CImageStatus` ([`CImageStatus`](#image-interface-status)) | - | Accepted normalized loading, loaded, or error state at callback time. | | `src` | `string` | - | Current authored fallback URL snapshot. | | `current_src` | `string` | - | Browser-selected absolute URL snapshot, which may be sensitive. | | `natural_width` | `integer` | - | Native selected resource width, including zero for pending, error, or valid zero-size resources. | | `natural_height` | `integer` | - | Native selected resource height, including zero for pending, error, or valid zero-size resources. |
    ### Translation keys - --- # Infinite Scroll Source: https://citry.dev/ui-library/components/infinite-scroll/ # Infinite Scroll `CInfiniteScroll` owns the request boundary around results. Your application still owns the records, request, response, item identity, and rerender. ## Keep a server fallback Set `action_name` inside a form to render a named Load more submit button. It uses `formnovalidate`, so unrelated incomplete fields do not block pagination. ### Load another result page [Open the rendered preview](/ui-library/components/infinite-scroll/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class InfiniteScrollAtAGlance(Component): template = """
    1. Created the project
    2. Invited the design team
    3. Published the brief
    Loaded 3 activities
    """ js = """ $component(({ scope }) => { const pages = [ [ { id: 4, label: 'Received legal approval' }, { id: 5, label: 'Scheduled the launch' }, ], [ { id: 6, label: 'Opened early access' }, { id: 7, label: 'Collected the first responses' }, ], ]; scope.moreActivities = []; scope.loading = false; scope.hasMore = true; let page = 0; scope.loadNextPage = detail => { // This static preview handles the named action locally. A server page // lets the submit continue and returns the next keyed result page. detail.sourceEvent?.preventDefault(); if (scope.loading || !scope.hasMore) return; scope.loading = true; return new Promise(resolve => setTimeout(() => { scope.moreActivities.push(...pages[page]); page += 1; scope.hasMore = page < pages.length; scope.loading = false; resolve(); }, 220)); }; }); """ preview = InfiniteScrollAtAGlance() preview # noqa: B018 ```` The preview intercepts the named action and appends two bounded dummy pages so you can exercise the complete state change here. In an application, let the named submit continue and return the next keyed result page from the server. ## Keep loading automatically Pass `onLoadMore` through `$c-props`. When the sentinel reaches `root_margin`, the callback receives `{reason: 'intersection', sourceEvent: null}`. Button activation uses `button` or `retry` and includes the native event. ### Observe the result boundary [Open the rendered preview](/ui-library/components/infinite-scroll/_previews/automatic/) ````citry # ruff: noqa: E501 - embedded Citry template attributes remain readable as authored HTML import citry_ui from citry import Component, citry citry.register_library(citry_ui) class InfiniteScrollAutomatic(Component): template = """

    Scroll to the end of the clipped result feed.

    1. Search result 1
    2. Search result 2
    3. Search result 3
    4. Search result 4
    5. Search result 5
    6. Search result 6
    7. Search result 7
    8. Search result 8
    Loaded 8 results
    """ js = """ $component(({ scope }) => { scope.moreResults = []; scope.loading = false; scope.hasMore = true; let nextResult = 9; scope.loadNextPage = () => { if (scope.loading) return; scope.loading = true; return new Promise(resolve => setTimeout(() => { const page = Array.from({ length: 4 }, () => { const id = nextResult++; return { id, label: `Search result ${id}` }; }); scope.moreResults.push(...page); scope.loading = false; resolve(); }, 260)); }; }); """ preview = InfiniteScrollAutomatic() preview # noqa: B018 ```` Update `loading`, `error`, or `hasMore` from application state. Only one request may be in progress at a time. A state change, nested content append, or returned Promise settling releases that request lock and permits a later request. The automatic preview uses a clipped result feed. Scroll that feed to its end to append another four generated results. It deliberately has no final page, so every fresh trip to the end loads more. Production code sets `hasMore=False` when its data source returns no continuation. ## Compose with Virtual List Infinite Scroll answers when to load. Virtual List answers which known items to render. Nest either component in the default slot without sharing their state machines. ### Load outside a virtualized collection [Open the rendered preview](/ui-library/components/infinite-scroll/_previews/virtual-list/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class InfiniteScrollVirtualList(Component): template = """
    Signed in Changed billing contact Exported report
    Showing 3 audit records
    """ js = """ $component(({ scope }) => { scope.expanded = false; scope.loading = false; scope.hasMore = true; scope.loadSnapshot = () => { if (scope.loading || !scope.hasMore) return; scope.loading = true; return new Promise(resolve => setTimeout(() => { scope.expanded = true; scope.hasMore = false; scope.loading = false; resolve(); }, 220)); }; }); """ preview = InfiniteScrollVirtualList() preview # noqa: B018 ```` This static preview swaps from a three-record server snapshot to a six-record snapshot. A real owner returns the newly rendered keyed collection. ## Show errors and retry Set `error=True` after a failed request. The same stable action becomes Try again, and its callback reason becomes `retry`. Automatic observation pauses until that explicit retry or another state change clears the error. ### Offer a retry path [Open the rendered preview](/ui-library/components/infinite-scroll/_previews/error-retry/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class InfiniteScrollErrorRetry(Component): template = """
    • Order #1042
    • Order #1041
    Last request failed
    """ js = """ $component(({ scope }) => { scope.recoveredOrders = []; scope.loading = false; scope.error = true; scope.hasMore = true; scope.recovered = false; scope.retryPage = () => { if (scope.loading || !scope.hasMore) return; scope.error = false; scope.loading = true; return new Promise(resolve => setTimeout(() => { scope.recoveredOrders.push( { id: 1040, label: 'Order #1040' }, { id: 1039, label: 'Order #1039' }, ); scope.recovered = true; scope.hasMore = false; scope.loading = false; resolve(); }, 260)); }; }); """ preview = InfiniteScrollErrorRetry() preview # noqa: B018 ```` ## Use named form actions The observer never submits a form. A named action remains explicit and useful without a browser runtime. ### Submit a real Load more action [Open the rendered preview](/ui-library/components/infinite-scroll/_previews/server-action/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class InfiniteScrollServerAction(Component): template = """
    1. Camera body comparison
    2. Lens mount guide
    Waiting for a named action
    """ js = """ $component(({ scope }) => { scope.moreResults = []; scope.loading = false; scope.hasMore = true; scope.acceptedAction = 'Waiting for a named action'; scope.loadServerPage = event => { const submitter = event.submitter; if (!submitter || scope.loading || !scope.hasMore) return; scope.acceptedAction = `${submitter.name}=${submitter.value}`; scope.loading = true; setTimeout(() => { scope.moreResults.push( { id: 3, label: 'Mirrorless travel kit' }, { id: 4, label: 'Low-light autofocus test' }, ); scope.hasMore = false; scope.loading = false; }, 240); }; }); """ preview = InfiniteScrollServerAction() preview # noqa: B018 ```` The preview reports the accepted submitter name and value, then appends its dummy response without leaving the frame. A production form sends that named action to its application endpoint. ## Announce bounded state Give a mixed page an `aria_label`. Loading, error, and end messages use a polite status. The sentinel is hidden from assistive technology and the button stays keyboard reachable. ### Name and finish a result feed [Open the rendered preview](/ui-library/components/infinite-scroll/_previews/accessibility/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class InfiniteScrollAccessibility(Component): template = """
    • Backup completed
    • Invoice sent
    """ preview = InfiniteScrollAccessibility() preview # noqa: B018 ```` The five default strings are Citry UI messages. Explicit label inputs opt that one output out of catalog-driven browser updates. ## API reference ### Inputs #### CInfiniteScroll server inputs Server inputs are passed in a template through `` or in Python through `CInfiniteScroll(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `id` | `str | None` | generated | Sets the root ID. | | `aria_label` | `str | None` | `None` | Names the root and gives it region semantics. | | `has_more` | `bool` | `True` | Controls whether another page exists. | | `loading` | `bool` | `False` | Shows pending state and suppresses requests. | | `error` | `bool` | `False` | Shows error state while changing the action to Retry and pausing automatic observation. | | `disabled` | `bool` | `False` | Disables requests and the action. | | `auto` | `bool` | `True` | Enables Intersection Observer requests when a callback exists and no loading error disabled or end state blocks them. | | `root_margin` | `str` | `"0px 0px 240px 0px"` | Sets the observer prefetch margin. | | `threshold` | `float` | `0` | Sets a finite observer threshold from zero through one. | | `action_name` | `str | None` | `None` | Makes the action a named submit button when supplied. | | `action_value` | `str` | `"load-more"` | Sets the submit button value. | | `load_more_label` | `str` | `"Load more"` | Overrides Load more text. | | `retry_label` | `str` | `"Try again"` | Overrides Retry text. | | `loading_label` | `str` | `"Loading more results"` | Overrides pending status text. | | `error_label` | `str` | `"More results could not be loaded"` | Overrides error status text. | | `end_label` | `str` | `"No more results"` | Overrides end status text. | | `class_` | `CClassValue | None` ([`CClassValue`](#infinite-scroll-interface-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#infinite-scroll-interface-style-value)) | `None` | Adds root styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied allowed root attributes. |
    #### CInfiniteScroll client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `hasMore` | `boolean` | Uses the server value. | Reactively controls whether another page exists. | | `loading` | `boolean` | Uses the server value. | Reactively controls pending state. | | `error` | `boolean` | Uses the server value. | Reactively controls retry state. | | `disabled` | `boolean` | Uses the server value. | Reactively disables requests. | | `auto` | `boolean` | Uses the server value. | Reactively enables observation. | | `onLoadMore` | `function` | The observer stays inactive and the native button remains available. | Receives each load request and may return a Promise. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CInfiniteScroll slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{}` ([`CInfiniteScrollDefaultSlotData`](#infinite-scroll-interface-cinfinite-scroll-default-slot-data)) | Empty result content. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CInfiniteScroll events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onLoadMore` | `(detail: CInfiniteScrollLoadDetail) => void | Promise` ([`CInfiniteScrollLoadDetail`](#infinite-scroll-interface-cinfinite-scroll-load-detail)) | An enabled action is activated or its observed sentinel intersects. | `{reason, sourceEvent}` ([`CInfiniteScrollLoadDetail`](#infinite-scroll-interface-cinfinite-scroll-load-detail)) | Requests data without mutating result content or submitting from the observer. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CInfiniteScroll CSS variables Apply these variables to `CInfiniteScroll` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-infinite-scroll-gap` | `length` | Gap among content status action and sentinel. | `0.875rem` | | `--cui-infinite-scroll-action-border` | `complete border` | Action boundary. | `Adaptive 1px neutral` | | `--cui-infinite-scroll-action-surface` | `color` | Action surface. | `Canvas` | | `--cui-infinite-scroll-action-radius` | `length` | Action corners. | `0.625rem` | | `--cui-infinite-scroll-focus` | `color` | Action focus ring. | `Highlight` | | `--cui-infinite-scroll-muted` | `color` | Status text. | `Adaptive muted CanvasText` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CInfiniteScroll attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `aria-busy` | Content | `true | false` | Reflects loading without delaying sibling status announcements. | | `data-loading` | Root | `present | absent` | Reflects loading. | | `data-error` | Root | `present | absent` | Reflects visible retry state. | | `data-end` | Root | `present | absent` | Reflects exhausted results. | | `data-disabled` | Root | `present | absent` | Reflects disabled requests. | | `data-auto` | Root | `present | absent` | Reflects observation preference. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CInfiniteScroll selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="infinite-scroll"]` | Root | Request boundary and state destination. | | `[data-citry-ui-part="content"]` | Content div | Server-owned results. | | `[data-citry-ui-part="status"]` | Polite status | Pending error and end announcements. | | `[data-citry-ui-part="action"]` | Native button | Explicit Load more or Retry path. | | `[data-citry-ui-part="sentinel"]` | Hidden span | Intersection observation target. |
    ### Interfaces Aliases and data shapes referenced above. #### Input type aliases
    | Interface | Definition | |---|---| | `CInfiniteScrollReason` | `Literal["button", "intersection", "retry"]` | | `CClassValue` | `str | Mapping[str, bool] | Sequence[CClassValue]` | | `CStyleValue` | `str | Mapping[str, object] | Sequence[CStyleValue]` |
    #### `CInfiniteScrollDefaultSlotData` Empty dataclass: `{}`. #### `CInfiniteScrollLoadDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `reason` | `CInfiniteScrollReason` ([`CInfiniteScrollReason`](#infinite-scroll-interface-reason)) | - | Button intersection or retry request source. | | `sourceEvent` | `object | None` | - | Native click Event or null for intersection. |
    ### 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. #### CInfiniteScroll translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-infinite-scroll-load-more` | Labels the ordinary load action. | `None.` | `load_more_label` | Stable `$c-tr` text. | | `citry-ui-infinite-scroll-retry` | Labels the retry action. | `None.` | `retry_label` | Stable `$c-tr` text. | | `citry-ui-infinite-scroll-loading` | Announces a pending request. | `None.` | `loading_label` | Stable `$c-tr` text. | | `citry-ui-infinite-scroll-error` | Announces a failed request. | `None.` | `error_label` | Stable `$c-tr` text. | | `citry-ui-infinite-scroll-end` | Announces exhausted results. | `None.` | `end_label` | Stable `$c-tr` text. |
    --- # List Source: https://citry.dev/ui-library/components/list/ # List Use `CList` and `CListItem` for concise semantic collections. Items can stay static, navigate, or act as native Buttons. ## List at a glance ### List at a glance [Open the rendered preview](/ui-library/components/list/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListGlance(Component): template = """ Aurora over Tromsø Comet C/2026 Q2 Lunar eclipse """ preview = ListGlance() preview # noqa: B018 ```` ## Present semantic content ### Present semantic list content [Open the rendered preview](/ui-library/components/list/_previews/content/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListContent(Component): template = """ Align the telescope Calibrate the camera Begin the exposure """ preview = ListContent() preview # noqa: B018 ```` ## Build navigation Set `href` on an Item for a whole-row link. `current=True` adds `aria-current="page"`. ### Build list navigation [Open the rendered preview](/ui-library/components/list/_previews/navigation/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListNavigation(Component): template = """ """ preview = ListNavigation() preview # noqa: B018 ```` ## Add media, descriptions, and trailing content ### Compose List Item anatomy [Open the rendered preview](/ui-library/components/list/_previews/anatomy/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListAnatomy(Component): template = """ Mare Imbrium basalt Apollo 15 · sample 15555 Lunar Murchison meteorite Carbonaceous chondrite · 1969 12.4 g """ preview = ListAnatomy() preview # noqa: B018 ```` ## Add whole-row and secondary actions Use `action=True` for one whole-row Button. Keep an Item static when its end slot contains a separate control. ### Compose List actions [Open the rendered preview](/ui-library/components/list/_previews/actions/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListActions(Component): template = """ Open current session Nightly calibration Ready to archive Archive """ preview = ListActions() preview # noqa: B018 ```` ## Nest Lists ### Nest semantic Lists [Open the rendered preview](/ui-library/components/list/_previews/nested/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class NestedList(Component): template = """ Inner planets Mercury Venus Earth Outer planets """ preview = NestedList() preview # noqa: B018 ```` ## Choose density and dividers ### Choose List presentation [Open the rendered preview](/ui-library/components/list/_previews/presentation/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListPresentation(Component): template = """ Andromeda Galaxy Triangulum Galaxy Whirlpool Galaxy Sombrero Galaxy """ preview = ListPresentation() preview # noqa: B018 ```` ## Customize List ### Customize List [Open the rendered preview](/ui-library/components/list/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class ListCustomization(Component): template = """ Orion Nebula Lagoon Nebula """ css = """ :where(.violet-list) { --cui-list-current-background: light-dark(#ede9fe, #4c1d95); --cui-list-radius: 1rem; } """ preview = ListCustomization() preview # noqa: B018 ```` ## Accessibility and behavior Lists retain native `ul`/`ol` and `li` semantics. Only links, whole-row Buttons, and authored secondary controls enter Tab order. Use Menu for command popovers, Tabs for view switching, and DataTable for two-dimensional records. ## API reference ### Inputs #### CList server inputs Server inputs are passed in a template through `` or in Python through `CList(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `ordered` | `bool` | `False` | Renders ol instead of ul. | | `start` | `int | None` | `None` | Sets native ordered-list numbering start. | | `reversed` | `bool` | `False` | Reverses native ordered-list numbering. | | `marker` | `"none" | "disc" | "decimal"` ([`CListMarker`](#list-interface-input-type-aliases-list-marker)) | `"none"` | Selects no marker or a semantic unordered/ordered marker. | | `density` | `"comfortable" | "compact"` ([`CListDensity`](#list-interface-input-type-aliases-list-density)) | `"comfortable"` | Selects item spacing. | | `variant` | `"plain" | "surface"` ([`CListVariant`](#list-interface-input-type-aliases-list-variant)) | `"plain"` | Selects transparent or quiet item surfaces. | | `divided` | `bool` | `False` | Draws dividers between direct Items. | | `label` | `str | None` | `None` | Optionally names the list. | | `class_` | `CClassValue | None` ([`CClassValue`](#list-interface-input-type-aliases-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#list-interface-input-type-aliases-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted copied list attributes without replacing semantics, children, or runtime fields. |
    #### CListItem server inputs Server inputs are passed in a template through `` or in Python through `CListItem(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `href` | `str | None` | `None` | Makes the whole Item a native link; disabled Items render static content. | | `action` | `bool` | `False` | Makes the whole Item a native type=button action; cannot combine with href. | | `disabled` | `bool` | `False` | Removes link/action interaction and reflects disabled styling. | | `current` | `bool` | `False` | Emits aria-current=page on an enabled link. | | `class_` | `CClassValue | None` ([`CClassValue`](#list-interface-input-type-aliases-class-value)) | `None` | Adds li classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#list-interface-input-type-aliases-style-value)) | `None` | Adds li inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds copied li attributes without replacing Item semantics. | | `surface_attrs` | `Mapping[str, object] | None` | `None` | Adds copied static/link/Button surface attributes without replacing its identity or behavior. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CList slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CListDefaultSlotData`](#list-interface-clist-default-slot-data)) | None. |
    #### CListItem slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `start` | no | `{}` ([`CListItemStartSlotData`](#list-interface-clist-item-start-slot-data)) | No leading media. | | `default` | yes | `{}` ([`CListItemDefaultSlotData`](#list-interface-clist-item-default-slot-data)) | None. | | `description` | no | `{}` ([`CListItemDescriptionSlotData`](#list-interface-clist-item-description-slot-data)) | No supplemental text. | | `end` | no | `{}` ([`CListItemEndSlotData`](#list-interface-clist-item-end-slot-data)) | No trailing metadata or secondary action. |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CList CSS variables Apply these variables to `CList` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-list-gap` | `length` | Gap between direct Items. | `0.25rem` | | `--cui-list-padding` | `length` | Root padding. | `0.35rem` | | `--cui-list-item-padding` | `length` | Item surface padding. | `Density-derived.` | | `--cui-list-radius` | `length` | Item surface radius. | `0.65rem` | | `--cui-list-foreground` | `color` | Primary foreground. | `CanvasText` | | `--cui-list-muted` | `color` | Description foreground. | `Nested-scheme muted foreground.` | | `--cui-list-background` | `color` | Root background. | `transparent` | | `--cui-list-hover-background` | `color` | Interactive hover and surface variant background. | `Nested-scheme quiet surface.` | | `--cui-list-current-background` | `color` | Current-link background. | `Nested-scheme blue surface.` | | `--cui-list-divider-color` | `color` | Divider color. | `Nested-scheme border color.` | | `--cui-list-marker-color` | `color` | Marker color. | `currentColor` | | `--cui-list-focus-ring` | `color` | Interactive focus outline. | `Highlight` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CList attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-marker` | List root | `"none" | "disc" | "decimal"` | Marker contract. | | `data-density` | List root | `"comfortable" | "compact"` | Spacing density. | | `data-variant` | List root | `"plain" | "surface"` | Surface treatment. | | `data-divided` | List root | `present-or-absent` | Present when direct Items have dividers. |
    #### CListItem attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-current` | li | `present-or-absent` | Present for the current link. | | `data-disabled` | li | `present-or-absent` | Present when a link becomes static or an action Button is natively disabled. | | `data-interactive` | li | `present-or-absent` | Present when the surface is an enabled link or Button. | | `aria-current` | Current link | `"page"` | Exposes current navigation location. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CList selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="list"]` | ul or ol root | Stable list and attrs destination. |
    #### CListItem selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="list-item"]` | li | Stable Item and attrs destination. | | `[data-citry-ui-part="surface"]` | div, a, or button | Stable content/action surface and surface_attrs destination. | | `[data-citry-ui-part="start"]` | Leading wrapper | Leading media surface. | | `[data-citry-ui-part="body"]` | Primary content wrapper | Primary and description layout surface. | | `[data-citry-ui-part="description"]` | Supplemental text wrapper | Muted description surface. | | `[data-citry-ui-part="end"]` | Trailing wrapper | Metadata or secondary action surface. |
    ### 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]` | | `CListMarker` | `Literal["none", "disc", "decimal"]` | | `CListDensity` | `Literal["comfortable", "compact"]` | | `CListVariant` | `Literal["plain", "surface"]` |
    #### `CListDefaultSlotData` Empty dataclass: `{}`. #### `CListItemDefaultSlotData` Empty dataclass: `{}`. #### `CListItemStartSlotData` Empty dataclass: `{}`. #### `CListItemDescriptionSlotData` Empty dataclass: `{}`. #### `CListItemEndSlotData` Empty dataclass: `{}`. ### Translation keys - --- # Sortable Source: https://citry.dev/ui-library/components/sortable/ # 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. ### Prioritize release work [Open the rendered preview](/ui-library/components/sortable/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SortableAtAGlance(Component): template = """ Design review Accessibility pass Implementation Release """ 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. ### Reorder rich task cards [Open the rendered preview](/ui-library/components/sortable/_previews/rich-items/) ````citry # 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 = """ Audit keyboard paths
    Accessibility · 3 points
    Refine theme tokens
    Design system · 2 points
    Publish release
    Fixed until approval
    """ 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. ### Accept controlled reorder requests [Open the rendered preview](/ui-library/components/sortable/_previews/controlled/) ````citry # 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 = """
    No request
    """ 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. ### Reorder a responsive grid [Open the rendered preview](/ui-library/components/sortable/_previews/grid/) ````citry # 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 = """ Revenue
    €42,800
    Orders
    318
    Retention
    91%
    Alerts
    4 open
    """ 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. ### Submit ordered priorities [Open the rendered preview](/ui-library/components/sortable/_previews/forms/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SortableForms(Component): template = """
    """ 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. ### Keep fixed and disabled Items understandable [Open the rendered preview](/ui-library/components/sortable/_previews/accessibility/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class SortableAccessibility(Component): template = """ """ 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 `` 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`](#sortable-interface-layout)) | `"vertical"` | Selects vertical horizontal or responsive-grid collision and layout. | | `disabled` | `bool` | `False` | Disables all handles and form contribution. | | `size` | `CSortableSize` ([`CSortableSize`](#sortable-interface-size)) | `"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`](#sortable-interface-class-value)) | `None` | Adds classes to the root. | | `style` | `CStyleValue | None` ([`CStyleValue`](#sortable-interface-style-value)) | `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 ``.
    | 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 `` 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`](#sortable-interface-class-value)) | `None` | Adds classes to the rendered Item. | | `style` | `CStyleValue | None` ([`CStyleValue`](#sortable-interface-style-value)) | `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 `` tags in a template, or through the `slots={...}` argument in Python. #### CSortable slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CSortableDefaultSlotData`](#sortable-interface-csortable-default-slot-data)) | None; accepts only Item declarations. |
    #### CSortableItem slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | no | `{value, label, disabled, index}` ([`CSortableItemSlotData`](#sortable-interface-csortable-item-slot-data)) | Plain label text. | | `handle` | no | `{value, label, disabled, index}` ([`CSortableItemSlotData`](#sortable-interface-csortable-item-slot-data)) | 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`](#sortable-interface-csortable-order-change-detail)) | A completed pointer keyboard reset or client reconciliation proposes another order. | `{order, previousOrder, value, fromIndex, toIndex, source, controlled, sourceEvent}` ([`CSortableOrderChangeDetail`](#sortable-interface-csortable-order-change-detail)) | 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`](#sortable-interface-layout)) | Reflects current layout and collision profile. | | `data-size` | Root | `CSortableSize` ([`CSortableSize`](#sortable-interface-size)) | 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`](#sortable-interface-change-source)) | - | 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. |
    --- # Table Source: https://citry.dev/ui-library/components/table/ # Table `CTable` renders finite, read-only tabular data with native HTML semantics. It owns structure and presentation, not sorting, selection, editing, pagination, or remote queries. ## Table at a glance Line and outline variants, three densities, stripes, hover, column borders, sticky headers, and explicit loading, empty, and error output share one native Table model. ### Table at a glance [Open the rendered preview](/ui-library/components/table/_previews/at-a-glance/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn, CTableRow citry.register_library(citry_ui) class TableAtAGlance(Component): class Kwargs: pass class Slots: pass template = """

    Inner planets

    Distance from the Sun

    Outer planets

    Distance from the Sun

    Survey pending

    No matching worlds

    """ css = """ :where(.table-glance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 1rem; max-width: 72rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.table-glance article) { min-width: 0; padding: 1rem; border: 1px solid light-dark(#bfdbfe, #1e3a8a); border-radius: 0.875rem; background: Canvas; } :where(.table-glance h2) { margin: 0 0 0.75rem; color: light-dark(#1d4ed8, #93c5fd); font-size: 1rem; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("planet", "Planet", row_header=True), CTableColumn("distance", "Mean distance", align="end"), ), "inner_rows": ( CTableRow("mercury", {"planet": "Mercury", "distance": "57.9 million km"}), CTableRow("venus", {"planet": "Venus", "distance": "108.2 million km"}), CTableRow("earth", {"planet": "Earth", "distance": "149.6 million km"}), ), "outer_rows": ( CTableRow("jupiter", {"planet": "Jupiter", "distance": "778.5 million km"}), CTableRow("saturn", {"planet": "Saturn", "distance": "1.43 billion km"}), CTableRow("uranus", {"planet": "Uranus", "distance": "2.87 billion km"}), ), } preview = TableAtAGlance() preview # noqa: B018 ```` `CTable` has no component JavaScript or client inputs. Every Table input is a server input passed through `` or `CTable(...)`. Controls inside cells keep their own client props and native events. ## Build a Table Declare columns once, then give every keyed row exactly one value per column. ### List the moons of Jupiter [Open the rendered preview](/ui-library/components/table/_previews/moons-of-jupiter/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn, CTableRow citry.register_library(citry_ui) class MoonsOfJupiter(Component): class Kwargs: pass class Slots: pass template = """
    Galilean moons
    """ css = """ :where(.moon-table) { max-width: 48rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.moon-table [data-column-key="diameter"]) { font-variant-numeric: tabular-nums; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("moon", "Moon", row_header=True), CTableColumn("discoverer", "Discoverer"), CTableColumn("diameter", "Diameter", align="end"), ), "rows": ( CTableRow("io", {"moon": "Io", "discoverer": "Galileo", "diameter": "3,643 km"}), CTableRow("europa", {"moon": "Europa", "discoverer": "Galileo", "diameter": "3,122 km"}), CTableRow("ganymede", {"moon": "Ganymede", "discoverer": "Galileo", "diameter": "5,268 km"}), CTableRow("callisto", {"moon": "Callisto", "discoverer": "Galileo", "diameter": "4,821 km"}), ), } preview = MoonsOfJupiter() preview # noqa: B018 ```` ```citry-html Galilean moons ``` ```python from citry_ui import CTable, CTableColumn, CTableRow moon_table = CTable( columns=( CTableColumn("moon", "Moon", row_header=True), CTableColumn("diameter", "Diameter", align="end"), ), rows=( CTableRow("europa", {"moon": "Europa", "diameter": "3,122 km"}), ), slots={"caption": "Galilean moons"}, ) ``` Keys are stable application identity, not display text or array positions. They must be unique and non-empty. Row and column keys are exposed in escaped `data-*` attributes, so do not put secrets in them. Use one `row_header=True` column for the entity or category that identifies each row. `align="end"` follows text direction and suits numeric values. Add tabular numerals through `cell_attrs`, a class, or the public cell selector. ## Present rich cells Raw values are escaped. A `CTableCell` adds attributes to one position, and a component-like value renders directly. Use the generic `cell` fill when output depends on the current row and column. ### Build an observation catalog [Open the rendered preview](/ui-library/components/table/_previews/rich-cells/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableCell, CTableColumn, CTableRow citry.register_library(citry_ui) class ObservationCatalog(Component): class Kwargs: pass class Slots: pass template = """
    Tonight's observation catalog {{ cell.value }} View {{ row.key }} {{ cell.value }}
    """ css = """ :where(.observation-catalog) { max-width: 58rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.visibility) { display: inline-flex; padding: 0.2rem 0.55rem; border-radius: 999px; font-size: 0.75rem; font-weight: 700; text-transform: capitalize; } :where(.visibility--excellent) { color: light-dark(#166534, #bbf7d0); background: light-dark(#dcfce7, #14532d); } :where(.visibility--limited) { color: light-dark(#9a3412, #fed7aa); background: light-dark(#ffedd5, #7c2d12); } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("target", "Target", row_header=True), CTableColumn("type", "Type"), CTableColumn("visibility", "Visibility"), CTableColumn("action", "Actions"), ), "rows": ( CTableRow( "orion-nebula", {"target": "Orion Nebula", "type": "Nebula", "visibility": "excellent", "action": None}, ), CTableRow( "andromeda", { "target": CTableCell("Andromeda Galaxy", attrs={"class": "featured-target"}), "type": "Galaxy", "visibility": "limited", "action": None, }, ), ), } preview = ObservationCatalog() preview # noqa: B018 ```` ```citry-html View {{ row.key }} {{ cell.value }} ``` `header_attrs` targets one column header. `cell_attrs` supplies defaults to every body cell in that column. `CTableCell.attrs` wins for ordinary duplicate attributes while class and style contributions merge. Structural values such as scopes and spans remain Table-owned. Sorting links, row actions, checkboxes, Inputs, and Comboboxes may live in cells, but their behavior belongs to those controls. Hover never makes a row selectable or clickable. ## Add totals and summaries Set one or more column `footer` values to render a native one-row `tfoot`. Footer content may be plain text or another component. `footer_attrs` targets that column's footer cell. ### Summarize telescope time [Open the rendered preview](/ui-library/components/table/_previews/survey-totals/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn, CTableRow citry.register_library(citry_ui) class SurveyTotals(Component): class Kwargs: pass class Slots: pass template = """
    Telescope survey time {{ value }} {{ value }}
    """ css = """ :where(.survey-totals) { max-width: 44rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.survey-totals [data-citry-ui-part="footer-cell"]) { color: light-dark(#1e3a8a, #bfdbfe); } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("program", "Program", row_header=True, footer="Total"), CTableColumn("instrument", "Instrument", footer="3 programs"), CTableColumn( "hours", "Hours", align="end", cell_attrs={"style": {"font-variant-numeric": "tabular-nums"}}, footer="84.5", ), ), "rows": ( CTableRow("aurora", {"program": "Aurora survey", "instrument": "Spectrograph", "hours": "36.0"}), CTableRow("rings", {"program": "Ring survey", "instrument": "Wide-field camera", "hours": "28.5"}), CTableRow("comets", {"program": "Comet survey", "instrument": "Infrared camera", "hours": "20.0"}), ), } preview = SurveyTotals() preview # noqa: B018 ```` The `footer` fill receives `{column, value, column_index}` once per footer cell. Its fallback is the matching column value. The row-header column remains a row header in the footer. Version 1 owns one summary row. Multiple footer rows, grouped headers, `rowspan`, `colspan`, and `colgroup` need a future logical-grid schema. ## Choose appearance ### Compare Table appearance [Open the rendered preview](/ui-library/components/table/_previews/appearance/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn, CTableRow citry.register_library(citry_ui) class TableAppearance(Component): class Kwargs: pass class Slots: pass template = """

    Line · comfortable

    Outline · compact

    Striped · default

    Hover · bottom caption

    Hover highlights, but never selects, a row.
    """ css = """ :where(.table-appearance) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1rem; max-width: 72rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.table-appearance article) { min-width: 0; padding: 1rem; border: 1px solid light-dark(#dbeafe, #1e3a8a); border-radius: 0.875rem; background: Canvas; } :where(.table-appearance h2) { margin: 0 0 0.75rem; font-size: 0.875rem; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("planet", "Planet", row_header=True), CTableColumn("gravity", "Gravity", align="end"), ), "rows": ( CTableRow("mars", {"planet": "Mars", "gravity": "3.71 m/s²"}), CTableRow("neptune", {"planet": "Neptune", "gravity": "11.15 m/s²"}), ), } preview = TableAppearance() preview # noqa: B018 ```` - `variant="line"` separates rows; `outline` also frames the root. - `density` accepts `default`, `comfortable`, or `compact`. - `striped` alternates ready-row surfaces. - `hover` adds pointer feedback without behavior. - `column_borders` adds vertical separators. - `caption_side` places a native caption at the top or bottom. - `layout="fixed"` uses native fixed table layout; set widths through column attribute styles, classes, or public selectors. These are server inputs. Side-by-side examples show their output without pretending that Table owns browser-reactive configuration. ## Show loading, empty, and error output ### Show survey states [Open the rendered preview](/ui-library/components/table/_previews/states/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn citry.register_library(citry_ui) class TableStates(Component): class Kwargs: pass class Slots: pass template = """

    Loading

    Receiving deep-space survey...

    Empty

    Error

    """ css = """ :where(.table-states) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); gap: 1rem; max-width: 68rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.table-states article) { min-width: 0; padding: 1rem; border: 1px solid light-dark(#c7d2fe, #3730a3); border-radius: 0.875rem; background: Canvas; } :where(.table-states h2) { margin: 0 0 0.75rem; font-size: 0.875rem; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("signal", "Signal", row_header=True), CTableColumn("strength", "Strength", align="end"), ), } preview = TableStates() preview # noqa: B018 ```` The header stays visible. Loading, empty, and error replace body rows with one native cell spanning every column. Loading sets `aria-busy` on the Table. Configured footers appear only in ready output, including ready-empty output. The `loading`, `empty`, and `error` slots change visible content. Their matching label inputs also feed a persistent polite live region outside the busy Table. Keep each label consistent with its custom slot. Entering a state removes stale ready rows. Returning to ready renders the next complete keyed collection. ## Keep wide and long Tables usable `overflow="auto"` is the default. It preserves native row and column relationships and lets two-dimensional data scroll horizontally at narrow widths or high zoom. ### Keep headers visible [Open the rendered preview](/ui-library/components/table/_previews/sticky-overflow/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn, CTableRow citry.register_library(citry_ui) class StickyOverflowTable(Component): class Kwargs: pass class Slots: pass template = """

    Bounded catalog

    Scroll this region in either direction.

    Confirmed exoplanets

    Page-sticky mode

    The header follows page scroll instead of an inner scroller.

    Nearby exoplanets
    """ css = """ :where(.sticky-tables) { display: grid; gap: 1.25rem; max-width: 64rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.sticky-tables article) { min-width: 0; padding: 1rem; border: 1px solid light-dark(#bae6fd, #075985); border-radius: 0.875rem; background: Canvas; } :where(.sticky-tables h2, .sticky-tables p) { margin: 0; } :where(.sticky-tables p) { margin-block: 0.25rem 0.75rem; color: color-mix(in srgb, currentColor 68%, transparent); } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 columns = ( CTableColumn("planet", "Planet", row_header=True, cell_attrs={"style": {"width": "12rem"}}), CTableColumn("system", "System", cell_attrs={"style": {"width": "14rem"}}), CTableColumn("distance", "Distance", align="end", cell_attrs={"style": {"width": "10rem"}}), CTableColumn("period", "Orbital period", align="end", cell_attrs={"style": {"width": "10rem"}}), ) rows = tuple( CTableRow( key, {"planet": planet, "system": system, "distance": distance, "period": period}, ) for key, planet, system, distance, period in ( ("proxima-b", "Proxima Centauri b", "Proxima Centauri", "4.2 ly", "11.2 days"), ("barnard-b", "Barnard's Star b", "Barnard's Star", "6.0 ly", "233 days"), ("ross-128-b", "Ross 128 b", "Ross 128", "11.0 ly", "9.9 days"), ("tau-ceti-e", "Tau Ceti e", "Tau Ceti", "11.9 ly", "163 days"), ("gj-1061-d", "GJ 1061 d", "GJ 1061", "12.0 ly", "13.0 days"), ("teegarden-b", "Teegarden's Star b", "Teegarden's Star", "12.5 ly", "4.9 days"), ("wolf-1061-c", "Wolf 1061 c", "Wolf 1061", "14.1 ly", "17.9 days"), ("gliese-667-cc", "Gliese 667 Cc", "Gliese 667 C", "23.6 ly", "28.1 days"), ) ) return {"columns": columns, "rows": rows} preview = StickyOverflowTable() preview # noqa: B018 ```` For a bounded scroller, combine `sticky_header=True` with a block-size limit: ```citry-html ``` For a header that follows page scroll, use `sticky_header=True` with `overflow="visible"`. The two modes have different scroll ancestors. Auto overflow always adds one keyboard focus stop because a zero-JavaScript component cannot measure overflow before deciding. A caption names that region. Without a caption, set `scroll_label` or name the native Table with `table_attrs={"aria-label": ...}`. The focus ring stays visible. An auto-overflow wrapper can clip inline menus, listboxes, and other overlays. Use a top-layer or portaled overlay when available, or choose visible overflow when the page can contain the Table. ## Preserve native semantics and focus Column headers use ``. The optional row-header column uses ``; other cells use ``. A caption supplies the Table's native accessible name. Use `table_attrs` for `aria-label`, `aria-labelledby`, or `aria-describedby` when visible caption text is not appropriate. Table does not use `role="grid"`, move focus with arrow keys, or select rows. Tab order contains the auto-overflow wrapper and focusable content supplied in cells. Native table navigation remains available to assistive technology. Ready rows use private Citry morph keys. Reordering preserves a surviving row subtree and its control state where Citry can preserve the control. Removing a row removes its complete subtree. Table does not guess a new focus target. Sorting, filtering, pagination, and selection belong to controls composed around the Table. Those controls update server state and render the next complete `columns` and `rows`; they are not Table callbacks. ## Theme and customize Table Use `class_`, `style`, public CSS variables, or documented selectors. Do not target private `.cui-*` classes or `--_cui-*` variables. ### Theme observatory Tables [Open the rendered preview](/ui-library/components/table/_previews/theme-customization/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTableColumn, CTableRow citry.register_library(citry_ui) class ObservatoryTables(Component): class Kwargs: pass class Slots: pass template = """

    Night observation

    Winter sky

    Solar observation

    Daylight calibration
    """ css = """ :where(.observatory-tables) { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1rem; max-width: 70rem; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.observatory-tables article) { min-width: 0; padding: 1rem; border-radius: 0.875rem; } :where(.observatory-tables h2) { margin: 0 0 0.75rem; font-size: 1rem; } :where(.observatory-tables__night) { color-scheme: dark; color: #e0f2fe; background: #0c1b33; --cui-table-background: #102a43; --cui-table-foreground: #e0f2fe; --cui-table-border-color: #486581; --cui-table-header-background: #243b53; --cui-table-striped-background: #173a5e; } :where(.observatory-tables__day) { color-scheme: light; color: #422006; background: #fffbeb; --cui-table-border-color: #f59e0b; } :where(.observatory-tables [data-citry-ui-part="footer-cell"]) { letter-spacing: 0.02em; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 return { "columns": ( CTableColumn("star", "Star", row_header=True, footer="Brightest"), CTableColumn("magnitude", "Magnitude", align="end", footer="-1.46"), ), "rows": ( CTableRow("sirius", {"star": "Sirius", "magnitude": "-1.46"}), CTableRow("canopus", {"star": "Canopus", "magnitude": "-0.74"}), CTableRow("arcturus", {"star": "Arcturus", "magnitude": "-0.05"}), ), } preview = ObservatoryTables() preview # noqa: B018 ```` Variables inherit, so one ancestor can theme several Tables. Set a variable on one root for an isolated override. Public selectors such as `[data-citry-ui-part="footer-cell"]` target stable elements. Reflected attributes expose the selected visual configuration for CSS and inspection. Nested Tables resolve their own density and variant rules. Structural styles from an outer Table do not stripe, hover, border, or resize an inner Table. Public color variables may intentionally inherit unless the nested root overrides them. ## Support direction, long content, and print ### Read translated star names [Open the rendered preview](/ui-library/components/table/_previews/environment/) ````citry from typing import Any import citry_ui from citry import Component, citry from citry_ui import CTable, CTableColumn, CTableRow citry.register_library(citry_ui) class TableEnvironment(Component): class Kwargs: pass class Slots: pass template = """

    أسماء النجوم

    أسماء عربية وتقليدية للنجوم
    """ css = """ :where(.table-environment) { max-width: 34rem; color: CanvasText; font-family: ui-sans-serif, system-ui, sans-serif; } :where(.table-environment h2) { margin: 0 0 0.75rem; color: light-dark(#6d28d9, #c4b5fd); font-size: 1rem; } :where(.table-environment [data-column-key="notes"]) { min-width: 18rem; white-space: normal; overflow-wrap: anywhere; } """ def template_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, Any]: # noqa: ARG002 nested = CTable( columns=(CTableColumn("planet", "الكوكب"),), rows=(CTableRow("earth", {"planet": "الأرض"}),), density="compact", overflow="visible", slots={"caption": "نظام نجمي"}, ) return { "columns": ( CTableColumn("name", "الاسم", row_header=True), CTableColumn("meaning", "المعنى"), CTableColumn("notes", "ملاحظات"), ), "rows": ( CTableRow( "betelgeuse", { "name": "منكب الجوزاء", "meaning": "كتف الجبار", "notes": "نجم أحمر فائق الضخامة في كوكبة الجبار، واسمه التقليدي طويل عند نقله بين اللغات.", }, ), CTableRow( "nested", {"name": "الشمس", "meaning": "نجمنا", "notes": nested}, ), ), } preview = TableEnvironment() preview # noqa: B018 ```` Logical alignment follows LTR and RTL. Long text wraps by default; use fixed layout and explicit widths only when truncation or stable columns improve the task. At narrow widths and 400% zoom, surrounding content still reflows while the Table may scroll as a two-dimensional exception. Default colors support light and dark scopes. Forced colors retains text, focus, and borders without using stripes or hover as the only signal. Print removes overflow clipping and sticky positioning. `CTable` targets ordinary finite collections. The repository's diagnostic scaling harness records server rendering at 10, 100, and 1,000 rows; hosted results remain release evidence, not a performance guarantee. Virtualization, grouped headers, interactive grid navigation, editing, and remote collection ownership belong to a future DataTable/DataGrid. ## API reference ### Inputs #### CTable server inputs Server inputs are passed in a template through `` or in Python through `CTable(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `columns` | `Sequence[CTableColumn]` | required | Defines the structural column schema. | | `rows` | `Sequence[CTableRow]` | required | Defines the keyed server-owned collection. | | `state` | `"ready" | "loading" | "error"` ([`CTableState`](#table-interface-input-type-aliases-ctable-state)) | `"ready"` | Selects body output. Ready with no rows selects empty output. | | `id` | `str | None` | generated | Sets wrapper and caption identity. | | `variant` | `"line" | "outline"` ([`CTableVariant`](#table-interface-input-type-aliases-ctable-variant)) | `"line"` | Selects border presentation. | | `density` | `"default" | "comfortable" | "compact"` ([`CTableDensity`](#table-interface-input-type-aliases-ctable-density)) | `"comfortable"` | Selects cell sizing. | | `striped` | `bool` | `False` | Adds alternating ready-row backgrounds. | | `hover` | `bool` | `False` | Adds pointer hover feedback without adding row behavior. | | `sticky_header` | `bool` | `False` | Sticks header cells within the scroll ancestor. | | `column_borders` | `bool` | `False` | Adds vertical separators. | | `layout` | `"auto" | "fixed"` ([`CTableLayout`](#table-interface-input-type-aliases-ctable-layout)) | `"auto"` | Selects native `table-layout`. | | `overflow` | `"auto" | "visible"` ([`CTableOverflow`](#table-interface-input-type-aliases-ctable-overflow)) | `"auto"` | Selects horizontal wrapper behavior. | | `caption_side` | `"top" | "bottom"` ([`CTableCaptionSide`](#table-interface-input-type-aliases-ctable-caption-side)) | `"top"` | Places the native caption. | | `scroll_label` | `non-empty str | None` | Uses the caption or native Table ARIA name when available. | Names the `overflow="auto"` focusable region. | | `loading_label` | `non-empty str` | `"Loading data..."` | Sets the loading fallback and persistent polite announcement text. | | `empty_label` | `non-empty str` | `"No data."` | Sets the empty fallback and persistent polite announcement text. | | `error_label` | `non-empty str` | `"Unable to load data."` | Sets the error fallback and persistent polite announcement text. | | `class_` | `str | Mapping[str, bool] | Sequence[CClassValue] | None` ([`CClassValue`](#table-interface-input-type-aliases-class-value)) | `None` | Adds wrapper classes from a string, conditional mapping, or nested sequence and merges them with `attrs`. | | `style` | `str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None` ([`CStyleValue`](#table-interface-input-type-aliases-style-value)) | `None` | Adds wrapper inline styles from CSS text, a property mapping, or a nested sequence and merges them with `attrs`. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds allowed wrapper attributes; prefer the top-level inputs for class and style. | | `table_attrs` | `Mapping[str, object] | None` | `None` | Adds allowed native table and ARIA attributes. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CTable slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `caption` | no | `{}` ([`CTableCaptionSlotData`](#table-interface-ctable-caption-slot-data)) | No caption. | | `header` | no | `{column: CTableColumn, column_index: int}` ([`CTableHeaderSlotData`](#table-interface-ctable-header-slot-data)) | Escaped column label. | | `cell` | no | `{row: CTableRow, column: CTableColumn, cell: CTableCell, row_index: int, column_index: int}` ([`CTableCellSlotData`](#table-interface-ctable-cell-slot-data)) | Escaped or component-like cell value. | | `footer` | no | `{column: CTableColumn, value: object | None, column_index: int}` ([`CTableFooterSlotData`](#table-interface-ctable-footer-slot-data)) | Escaped or component-like column footer value. | | `empty` | no | `{}` ([`CTableEmptySlotData`](#table-interface-ctable-empty-slot-data)) | `empty_label` | | `loading` | no | `{}` ([`CTableLoadingSlotData`](#table-interface-ctable-loading-slot-data)) | `loading_label` | | `error` | no | `{}` ([`CTableErrorSlotData`](#table-interface-ctable-error-slot-data)) | `error_label` |
    ### Events - ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTable CSS variables Apply these variables to `CTable` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-table-background` | `color` | Table surface. | `Canvas` | | `--cui-table-foreground` | `color` | Primary text. | `CanvasText` | | `--cui-table-muted-foreground` | `color` | Caption and subdued status text. | `Muted CanvasText mix.` | | `--cui-table-border-color` | `color` | Row, outline, footer, and optional column borders. | `Subtle CanvasText mix.` | | `--cui-table-header-background` | `color` | Header surface, including sticky headers. | `Subtle CanvasText/Canvas mix.` | | `--cui-table-footer-background` | `color` | Footer surface. | `Subtle CanvasText/Canvas mix.` | | `--cui-table-striped-background` | `color` | Alternating ready-row surface. | `Subtle CanvasText/Canvas mix.` | | `--cui-table-hover-background` | `color` | Ready-row pointer hover surface. | `Subtle Highlight/Canvas mix.` | | `--cui-table-error-foreground` | `color` | Error status text. | `Scheme-aware negative color.` | | `--cui-table-focus-color` | `color` | Overflow-region focus ring. | `Highlight` | | `--cui-table-radius` | `length` | Outline and wrapper radius. | `0.625rem` | | `--cui-table-cell-block-padding` | `length` | Logical block cell padding. | `Density-derived length.` | | `--cui-table-cell-inline-padding` | `length` | Logical inline cell padding. | `Density-derived length.` | | `--cui-table-caption-padding` | `CSS padding shorthand` | Caption spacing. | `0.75rem 1rem` | | `--cui-table-min-width` | `length` | Minimum width before horizontal overflow. | `32rem` | | `--cui-table-sticky-offset` | `length` | Sticky header block offset. | `0px` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTable attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-state` | Root | `"ready" | "loading" | "error"` | Mirrors effective body-output state. | | `data-variant` | Root | `"line" | "outline"` | Mirrors effective border presentation. | | `data-density` | Root | `"default" | "comfortable" | "compact"` | Mirrors effective cell density. | | `data-striped` | Root | `present | absent` | Mirrors striped-row presentation. | | `data-hover` | Root | `present | absent` | Mirrors pointer-hover presentation. | | `data-sticky-header` | Root | `present | absent` | Mirrors sticky-header configuration. | | `data-column-borders` | Root | `present | absent` | Mirrors column-border presentation. | | `data-layout` | Root | `"auto" | "fixed"` | Mirrors effective native table layout. | | `data-overflow` | Root | `"auto" | "visible"` | Mirrors horizontal overflow behavior. | | `data-caption-side` | Root | `"top" | "bottom"` | Mirrors effective caption placement. |
    #### CTable attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-row-key` | Ready row | `string` | Canonical row identity. |
    #### CTable attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-column-key` | Header, body, or footer cell | `string` | Canonical column identity. | | `data-align` | Header, body, or footer cell | `"start" | "center" | "end"` | Logical cell alignment. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTable selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="root"]` | Root | Wrapper, scroll container, and `attrs` destination. | | `[data-citry-ui-part="table"]` | Native Table | Table and `table_attrs` destination. | | `[data-citry-ui-part="caption"]` | Native caption | Optional caption hook. | | `[data-citry-ui-part="header"]` | Header group | Native header group. | | `[data-citry-ui-part="header-row"]` | Header row | Native header row. | | `[data-citry-ui-part="header-cell"]` | Header cell | Column-header hook. | | `[data-citry-ui-part="body"]` | Body group | Native body group. | | `[data-citry-ui-part="row"]` | Ready row | Keyed row hook. | | `[data-citry-ui-part="cell"]` | Body cell | Ready data-cell or row-header hook. | | `[data-citry-ui-part="state-row"]` | State row | Loading, empty, or error row. | | `[data-citry-ui-part="state-cell"]` | State cell | Cell spanning every column. | | `[data-citry-ui-part="loading"]` | Loading region | Loading status content. | | `[data-citry-ui-part="empty"]` | Empty region | Empty status content. | | `[data-citry-ui-part="error"]` | Error region | Error status content. | | `[data-citry-ui-part="footer"]` | Native footer group | Optional summary group. | | `[data-citry-ui-part="footer-row"]` | Native footer row | One summary row. | | `[data-citry-ui-part="footer-cell"]` | Footer cell | Per-column summary cell. |
    ### 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]` | | `CTableState` | `Literal["ready", "loading", "error"]` | | `CTableVariant` | `Literal["line", "outline"]` | | `CTableDensity` | `Literal["default", "comfortable", "compact"]` | | `CTableAlign` | `Literal["start", "center", "end"]` | | `CTableLayout` | `Literal["auto", "fixed"]` | | `CTableOverflow` | `Literal["auto", "visible"]` | | `CTableCaptionSide` | `Literal["top", "bottom"]` |
    #### `CTableColumn`
    | Field | Type | Default | Meaning | |---|---|---|---| | `key` | `non-empty str` | required | Unique column identity. | | `label` | `non-empty str` | required | Default escaped header content. | | `row_header` | `bool` | False | Renders body cells in this column as ``. | | `align` | `"start" | "center" | "end"` ([`CTableAlign`](#table-interface-input-type-aliases-ctable-align)) | "start" | Sets logical header and cell alignment. | | `header_attrs` | `Mapping[str, object] | None` | None | Adds allowed native attributes to the column header. | | `cell_attrs` | `Mapping[str, object] | None` | None | Adds defaults to every body cell in the column; a specific `CTableCell.attrs` value wins while class and style merge. | | `footer` | `object | None` | None | Supplies fallback content for the optional footer cell. Any non-None value enables the footer. | | `footer_attrs` | `Mapping[str, object] | None` | None | Adds allowed native attributes to the footer cell. |
    #### `CTableRow`
    | Field | Type | Default | Meaning | |---|---|---|---| | `key` | `non-empty str` | required | Unique row and morph identity. | | `cells` | `Mapping[str, object | CTableCell]` | required | Supplies exactly one value for every declared column key. | | `attrs` | `Mapping[str, object] | None` | None | Adds allowed native attributes to the row. |
    #### `CTableCell`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `object` | required | Default escaped or component-like cell content. | | `attrs` | `Mapping[str, object] | None` | None | Adds allowed native cell attributes. |
    #### `CTableCaptionSlotData` Empty dataclass: `{}`. #### `CTableHeaderSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `column` | `CTableColumn` | - | Current column declaration. | | `column_index` | `int` | - | Zero-based column position. |
    #### `CTableCellSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `row` | `CTableRow` | - | Current row declaration. | | `column` | `CTableColumn` | - | Current column declaration. | | `cell` | `CTableCell` | - | Normalized cell declaration. | | `row_index` | `int` | - | Zero-based row position. | | `column_index` | `int` | - | Zero-based column position. |
    #### `CTableFooterSlotData`
    | Field | Type | Default | Meaning | |---|---|---|---| | `column` | `CTableColumn` | - | Current column declaration. | | `value` | `object | None` | - | Current column footer value. | | `column_index` | `int` | - | Zero-based column position. |
    #### `CTableEmptySlotData` Empty dataclass: `{}`. #### `CTableLoadingSlotData` Empty dataclass: `{}`. #### `CTableErrorSlotData` Empty dataclass: `{}`. ### 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. #### CTable translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-table-loading` | Labels and announces the loading state. | `None` | `loading_label` input or `loading` slot | $c-tr updates component fallback text and the announcer. | | `citry-ui-table-empty` | Labels and announces an empty ready state. | `None` | `empty_label` input or `empty` slot | $c-tr updates component fallback text and the announcer. | | `citry-ui-table-error` | Labels and announces the error state. | `None` | `error_label` input or `error` slot | $c-tr updates component fallback text and the announcer. |
    --- # Tag and TagGroup Source: https://citry.dev/ui-library/components/tag/ # Tag and TagGroup Use `CTagGroup` for a labelled collection of compact categories, filters, or keywords. A descriptive group renders list semantics. Selection, actions, or removal switch it to one keyboard-operable grid. ### TagGroup at a glance [Open the rendered preview](/ui-library/components/tag/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagGlance(Component): template = """ CSS HTML Accessibility Wi-Fi Parking Pool """ preview = TagGlance() preview # noqa: B018 ```` ```citry-html CSS HTML ``` ## Select Tags Choose a selection mode and give every Tag a unique value. ### Select Tags [Open the rendered preview](/ui-library/components/tag/_previews/selection/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagSelection(Component): template = """
    Quiet Bright Central
    """ preview = TagSelection() preview # noqa: B018 ```` ```citry-html Wi-Fi Parking Pool ``` A supplied client `value` is authoritative. The callback requests the next selection; it does not mutate a controlled group. Omit the prop to release control while preserving the last effective selection. `mandatory=True` prevents user activation from clearing the final selection. ## Actions and removal `actionable=True` reports enabled Tag activation through `onAction`. `removable=True` adds one form-safe remove Button and enables Delete and Backspace. Removal is a request: update your collection to remove the values. ### Request Tag removal [Open the rendered preview](/ui-library/components/tag/_previews/removal/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagRemoval(Component): template = """
    Design Research Delivery
    """ preview = TagRemoval() preview # noqa: B018 ```` ### Run Tag actions [Open the rendered preview](/ui-library/components/tag/_previews/actions/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagActions(Component): template = """
    Overview Activity Settings
    """ preview = TagActions() preview # noqa: B018 ```` ```citry-html Open Assigned to me ``` When a selected Tag in multiple mode receives Delete, the request includes all selected removable values. Focus follows retained values across reorder and moves to the nearest following Tag after removal. ## Content The default slot is the Tag label. `start` accepts decorative noninteractive phrasing content such as an Icon or Avatar. ### Compose Tag content [Open the rendered preview](/ui-library/components/tag/_previews/content/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagContent(Component): template = """
    A Ava, accessibility research L Leo, design systems
    """ preview = TagContent() preview # noqa: B018 ```` ```citry-html A Ava ``` Tag content must not contain links, Buttons, form controls, focusable content, or nested Tags. Use a native anchor outside TagGroup when the job is navigation. Free-form entry and editing belong to `CTagsInput`. ## Keyboard behavior - Arrow keys move through enabled Tags and wrap. - Home and End move to the first and last enabled Tag. - Typing moves to the next matching Tag label or `text_value`. - Enter and Space activate selection and actions. - Delete and Backspace request removal. - Tab from a removable Tag reaches its remove Button; Shift+Tab returns. The group has one page-tab entry. Descriptive groups remain ordinary lists and do not add keyboard stops. ## Disabledness and forms Group disabledness, item disabledness, `CForm.disabled`, and native disabled fieldsets all dominate interaction. TagGroup is not a form control and adds no FormData. Owned remove Buttons always use `type="button"`. ## Presentation and customization Variants are `soft`, `solid`, and `outline`. Sizes are `sm`, `md`, and `lg`. Customize through public variables or stable part selectors: ### Compare Tag variants and sizes [Open the rendered preview](/ui-library/components/tag/_previews/variants/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagVariants(Component): template = """ SelectedAvailable Sample """ preview = TagVariants() preview # noqa: B018 ```` ### Customize Tags [Open the rendered preview](/ui-library/components/tag/_previews/customization/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TagCustomization(Component): css = """ :where(.forest-tags) { --cui-tag-selected-background: #176b4d; --cui-tag-selected-foreground: #fff; --cui-tag-radius: 0.45rem; } """ template = """ Fern Moss River """ preview = TagCustomization() preview # noqa: B018 ```` ```css .brand-tags { --cui-tag-selected-background: #176b4d; --cui-tag-selected-foreground: #fff; --cui-tag-radius: 0.5rem; } ``` See [`api.yml`](api.yml) for the exhaustive inputs, callbacks, variables, attributes, selectors, slots, and public interfaces. ## API reference ### Inputs #### CTagGroup server inputs Server inputs are passed in a template through `` or in Python through `CTagGroup(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `label` | `str` | required | Supplies the visible fallback label and accessible group name. | | `id` | `str | None` | `None` | Supplies the exact root and relationship prefix. | | `value` | `str | Sequence[str] | None` ([`CTagValue`](#tag-interface-value)) | `None` | Sets initial single or multiple selection. | | `selection_mode` | `"none" | "single" | "multiple"` ([`CTagSelectionMode`](#tag-interface-selection-mode)) | `"none"` | Selects descriptive or selectable behavior. | | `mandatory` | `bool` | `False` | Prevents activation from clearing the final selection. | | `actionable` | `bool` | `False` | Enables Tag action callbacks. | | `removable` | `bool` | `False` | Adds form-safe remove Buttons and deletion keys. | | `remove_label` | `str` | `"Remove"` | Supplies the translated remove action label. | | `disabled` | `bool` | `False` | Disables the owned collection; Form and fieldset disabledness remain dominant. | | `variant` | `"soft" | "solid" | "outline"` ([`CTagVariant`](#tag-interface-variant)) | `"soft"` | Selects visual treatment. | | `size` | `"sm" | "md" | "lg"` ([`CTagSize`](#tag-interface-size)) | `"md"` | Selects Tag geometry. | | `class_` | `CClassValue | None` ([`CClassValue`](#tag-interface-class-value)) | `None` | Adds root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#tag-interface-style-value)) | `None` | Adds root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted root attributes without replacing owned semantics. |
    #### CTagGroup client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `value` | `string | null | string[] | undefined` | Releases control and preserves the last effective selection. | Controls selection while supplied. | | `disabled` | `boolean | undefined` | Uses the server fallback. | Overrides local disabledness while valid. | | `variant` | `"soft" | "solid" | "outline" | undefined` | Uses the server fallback. | Overrides visual treatment while valid. | | `size` | `"sm" | "md" | "lg" | undefined` | Uses the server fallback. | Overrides geometry while valid. | | `onValueChange` | `((value, detail) => void) | undefined` | No selection notification. | Receives selection requests. | | `onAction` | `((value, detail) => void) | undefined` | No action notification. | Receives enabled actionable Tag activation. | | `onRemove` | `((values, detail) => void) | undefined` | No removal notification. | Receives remove Button or deletion-key requests. |
    #### CTag server inputs Server inputs are passed in a template through `` or in Python through `CTag(...)`.
    | Input | Type | Default | Effect | |---|---|---|---| | `value` | `str` | required | Supplies unique canonical identity within the group. | | `disabled` | `bool` | `False` | Disables this Tag. | | `text_value` | `str | None` | `None` | Supplies typeahead text instead of current label text. | | `class_` | `CClassValue | None` ([`CClassValue`](#tag-interface-class-value)) | `None` | Adds Tag-root classes. | | `style` | `CStyleValue | None` ([`CStyleValue`](#tag-interface-style-value)) | `None` | Adds Tag-root inline styles. | | `attrs` | `Mapping[str, object] | None` | `None` | Adds trusted Tag-root attributes without replacing owned semantics. |
    #### CTag client inputs Client inputs are passed in the browser through the `$c-props="{ ... }"` attribute on ``.
    | Input | Type | Omitted behavior | Effect | |---|---|---|---| | `disabled` | `boolean | undefined` | Uses the server fallback. | Overrides item-local disabledness while valid. | | `textValue` | `string | null | undefined` | Uses server text or current label text. | Overrides typeahead text while valid. |
    ### Slots Slots are passed as nested content or `` tags in a template, or through the `slots={...}` argument in Python. #### CTagGroup slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CTagGroupDefaultSlotData`](#tag-interface-group-default-slot)) | None. | | `label` | no | `{}` ([`CTagGroupLabelSlotData`](#tag-interface-group-label-slot)) | Escaped label input. | | `description` | no | `{}` ([`CTagGroupDescriptionSlotData`](#tag-interface-group-description-slot)) | Wrapper omitted. |
    #### CTag slots
    | Slot | Required | Data | Fallback | |---|---|---|---| | `default` | yes | `{}` ([`CTagDefaultSlotData`](#tag-interface-tag-default-slot)) | None. | | `start` | no | `{}` ([`CTagStartSlotData`](#tag-interface-tag-start-slot)) | Wrapper omitted. |
    ### Events Component events are callback inputs supplied through `$c-props`. Native browser events remain available through Alpine `@...` attributes. #### CTagGroup events
    | Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior | |---|---|---|---|---| | `onValueChange` | `(value, detail: CTagValueChangeDetail) => void` ([`CTagValueChangeDetail`](#tag-interface-value-change-detail)) | Enabled selectable Tag proposes a different value. | `{value, previousValue, tagValue, source, controlled, nativeEvent}` ([`CTagValueChangeDetail`](#tag-interface-value-change-detail)) | Runs before onAction; supplied client value remains authoritative. | | `onAction` | `(value: str, detail: CTagActionDetail) => void` ([`CTagActionDetail`](#tag-interface-action-detail)) | Enabled actionable Tag activates. | `{value, source, nativeEvent}` ([`CTagActionDetail`](#tag-interface-action-detail)) | Runs after a selection request. | | `onRemove` | `(values: list[str], detail: CTagRemoveDetail) => void` ([`CTagRemoveDetail`](#tag-interface-remove-detail)) | Remove Button or Delete and Backspace. | `{values, tagValue, source, nativeEvent}` ([`CTagRemoveDetail`](#tag-interface-remove-detail)) | Requests owner collection removal without changing structure. |
    ### Methods - ### CSS CSS variables to theme the components. Set them on an ancestor or the component itself. #### CTagGroup CSS variables Apply these variables to `CTagGroup` or one of its ancestors.
    | Variable | Type | Purpose | Default | |---|---|---|---| | `--cui-tag-gap` | `length` | Inline gap between Tags. | `0.5rem` | | `--cui-tag-row-gap` | `length` | Gap between wrapped rows. | `0.5rem` | | `--cui-tag-background` | `color` | Unselected fill. | `Variant and scheme derived.` | | `--cui-tag-foreground` | `color` | Unselected text. | `Variant and scheme derived.` | | `--cui-tag-border-color` | `color` | Tag border. | `Scheme-derived neutral.` | | `--cui-tag-selected-background` | `color` | Selected fill. | `Scheme-derived primary.` | | `--cui-tag-selected-foreground` | `color` | Selected text. | `White.` | | `--cui-tag-selected-border-color` | `color` | Selected border. | `Selected background.` | | `--cui-tag-focus-color` | `color` | Focus outline. | `Highlight` | | `--cui-tag-radius` | `length` | Tag corner radius. | `999px` | | `--cui-tag-min-height` | `length` | Minimum Tag block size. | `Size derived.` | | `--cui-tag-padding-inline` | `length` | Tag inline padding. | `Size derived.` | | `--cui-tag-internal-gap` | `length` | Gap between internal parts. | `Size derived.` | | `--cui-tag-font-size` | `length` | Tag label size. | `Size derived.` | | `--cui-tag-label-color` | `color` | Group-label foreground. | `CanvasText` | | `--cui-tag-description-color` | `color` | Description foreground. | `Scheme-derived muted text.` |
    ### Attributes HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only. #### CTagGroup attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-selection-mode` | Group root | `"none" | "single" | "multiple"` | Reflects collection behavior. | | `data-actionable` | Group root | `present-or-absent` | Present when action callbacks are enabled. | | `data-removable` | Group root | `present-or-absent` | Present when removal is enabled. | | `data-disabled` | Group root | `present-or-absent` | Mirrors effective group disabledness. | | `data-variant` | Group root and Tag | `"soft" | "solid" | "outline"` | Reflects visual treatment. | | `data-size` | Group root and Tag | `"sm" | "md" | "lg"` | Reflects geometry. |
    #### CTag attributes
    | Attribute | Element | Type | Meaning | |---|---|---|---| | `data-value` | Tag root | `string` | Exposes canonical identity. | | `data-selected` | Tag root | `present-or-absent` | Mirrors effective selection. | | `data-disabled` | Tag root | `present-or-absent` | Mirrors effective item disabledness. | | `data-removable` | Tag root | `present-or-absent` | Present when the remove affordance exists. | | `aria-selected` | Selectable Tag row | `boolean` | Exposes selection to assistive technology. | | `aria-disabled` | Interactive Tag row | `boolean` | Exposes effective disabledness. |
    ### Selectors Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing. #### CTagGroup selectors
    | Selector | Element | Purpose | |---|---|---| | `[data-citry-ui-part="tag-group"]` | Group root | Stable group and attrs destination. | | `[data-citry-ui-part="group-label"]` | Visible group label | Names the collection. | | `[data-citry-ui-part="list"]` | List or grid | Stable direct collection surface. | | `[data-citry-ui-part="description"]` | Optional description | Describes the collection. | | `[data-citry-ui-part="tag"]` | Tag root | Stable Tag and attrs destination. | | `[data-citry-ui-part="indicator"]` | Selection indicator | Exposes selected state visually. | | `[data-citry-ui-part="start"]` | Decorative start wrapper | Positions composed decoration. | | `[data-citry-ui-part="tag-label"]` | Tag label | Supplies the accessible Tag name. | | `[data-citry-ui-part="remove"]` | Native Button | Requests removal. |
    ### 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]` | | `CTagSelectionMode` | `Literal["none", "single", "multiple"]` | | `CTagVariant` | `Literal["soft", "solid", "outline"]` | | `CTagSize` | `Literal["sm", "md", "lg"]` | | `CTagValue` | `str | None | Sequence[str]` |
    #### `CTagValueChangeDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str | list[str] | None` | - | Requested selection. | | `previousValue` | `str | list[str] | None` | - | Selection before activation. | | `tagValue` | `str` | - | Activated Tag identity. | | `source` | `"activation"` | - | Change origin. | | `controlled` | `bool` | - | Whether client value controls selection. | | `nativeEvent` | `Event` | - | Triggering native event. |
    #### `CTagActionDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `value` | `str` | - | Activated Tag identity. | | `source` | `"activation"` | - | Action origin. | | `nativeEvent` | `Event` | - | Triggering native event. |
    #### `CTagRemoveDetail`
    | Field | Type | Default | Meaning | |---|---|---|---| | `values` | `list[str]` | - | Requested removal identities. | | `tagValue` | `str` | - | Tag that received the removal action. | | `source` | `"remove-button" | "delete-key"` | - | Removal origin. | | `nativeEvent` | `Event` | - | Triggering native event. |
    #### `CTagGroupDefaultSlotData` Empty dataclass: `{}`. #### `CTagGroupLabelSlotData` Empty dataclass: `{}`. #### `CTagGroupDescriptionSlotData` Empty dataclass: `{}`. #### `CTagDefaultSlotData` Empty dataclass: `{}`. #### `CTagStartSlotData` Empty dataclass: `{}`. ### 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. #### CTagGroup translation keys
    | Key | Purpose | Variables | Override | Browser updates | |---|---|---|---|---| | `citry-ui-tag-remove` | Supplies hidden accessible text for every remove control. | `None` | `remove_label` input | $c-tr updates text content. |
    --- # Timeline Source: https://citry.dev/ui-library/components/timeline/ # Timeline Use `CTimeline` and `CTimelineItem` for ordered histories, activity feeds, roadmaps, and status sequences. Timeline is presentational: links, actions, loading, and date formatting remain owned by your application. ## Timeline at a glance ### Timeline at a glance [Open the rendered preview](/ui-library/components/timeline/_previews/at-a-glance/) ````citry import citry_ui from citry import Component, citry citry.register_library(citry_ui) class TimelineAtAGlance(Component): template = """ Order confirmed
    Payment received
    In transit
    Departed the regional hub
    Delivered
    """ preview = TimelineAtAGlance() preview # noqa: B018 ```` ## Present an activity feed Place semantic `