Reference

Naming

Tailwind, DOM, SwiftUI and Testing Library names mapped to their CrownUI methods and listeners.

CrownUI borrows its names from vocabularies you already know. Views are SwiftUI nouns, style methods are Tailwind classes, event listeners are DOM events, and test actions are Testing Library verbs. If you know the web name, this page gives you the CrownUI name.

button("Save")
    .px(16.0)
    .py(8.0)
    .rounded(8.0)
    .bg(rgb(0x2563eb))
    .hover(|style| style.opacity(0.9))
    .on_click(|cx, event| save(cx, event.modifiers.shift_key))

That snippet reads as px-4 py-2 rounded-lg bg-blue-600 hover:opacity-90 plus onclick. Tailwind's spacing scale is gone. Every style method takes a literal value in logical pixels, so .p(16.0) is p-4.

Views

SwiftUICrownUI
Text, Button, Toggle, TextFieldtext(..), button(..), toggle(on), text_field(value, placeholder)
VStack, HStack, ZStack, Spacer, Dividervstack(..), hstack(..), zstack(..), spacer(), divider()
ScrollView, LazyVStack, LazyVGridscroll_view(..), lazy_vstack(..) or lazy_list(..), lazy_grid(..)
Image, Canvasimage(width, height, rgba), svg(..), icon(..), canvas(..)
if in a body, ForEach, .id(..)show(cond, || then, || otherwise), for_each(items, key, row), keyed(..)
@Statecx.signal(value)
withAnimationcx.with_animation(curve, |cx| ..)

Tailwind class to style method

Every method lives on the Styled trait, so it works on every element. Values are logical pixels unless the row says otherwise.

TailwindCrownUI
p-4 px-4 py-2 pt-* pr-* pb-* pl-*.p(16.0) .px(16.0) .py(8.0) .pt() .pr() .pb() .pl()
m-* mx-* my-* mt-* mr-* mb-* ml-*.m() .mx() .my() .mt() .mr() .mb() .ml()
w-* h-* size-*.w() .h() .size()
min-w-* max-w-* min-h-* max-h-* aspect-*.min_w() .max_w() .min_h() .max_h() .aspect()
w-full h-full w-1/2 h-1/3.w_full() .h_full() .w_frac(0.5) .h_frac(0.33)
flex-row flex-col flex-row-reverse flex-col-reverse.flex_row() .flex_col() .flex_row_reverse() .flex_col_reverse()
flex-wrap flex-1.flex_wrap() .flex_1()
grow shrink-0 basis-*.grow(1.0) .shrink(0.0) .basis()
gap-* gap-x-* gap-y-*.gap() .gap_x() .gap_y()
items-start items-center items-end items-stretch items-baseline.items_start() .items_center() .items_end() .items_stretch() .items_baseline()
self-start self-center self-end self-stretch.self_start() .self_center() .self_end() .self_stretch()
justify-start justify-center justify-end.justify_start() .justify_center() .justify_end()
justify-between justify-around justify-evenly.justify_between() .justify_around() .justify_evenly()
grid grid-cols-3 col-span-* row-span-*.grid() .grid_cols(3) .col_span(2) .row_span(2)
grid-cols-[200px_1fr].grid_template_cols(vec![..]) with GridTrack values
absolute relative.absolute() .relative()
inset-* top-* right-* bottom-* left-* z-*.inset() .top() .right() .bottom() .left() .z(10)
overflow-hidden overflow-scroll.overflow_hidden() .overflow_scroll()
bg-*.bg(color)
text-* for color and for size.text_color(color) .text_size(14.0)
font-medium font-semibold font-bold font-[650].font_medium() .font_semibold() .font_bold() .font_weight(650)
font-sans and other families, italic.font_family("Inter") .italic()
leading-* tracking-*.leading(1.5) as a multiple of the font size, .tracking(0.5) for letter spacing, which the Impeller renderer ignores today
truncate line-clamp-2.truncate() .line_clamp(2)
text-left text-center text-right text-justify.text_left() .text_center() .text_right() .text_justify()
rounded-*.rounded(8.0)
rounded-t-* rounded-r-* rounded-b-* rounded-l-*.rounded_t() .rounded_r() .rounded_b() .rounded_l()
rounded-tl-* rounded-tr-* rounded-br-* rounded-bl-*.rounded_tl() .rounded_tr() .rounded_br() .rounded_bl()
border border-* (color).border(1.0) .border_color(color)
ring-* outline-*.ring(2.0, color) .outline(1.0, color)
shadow-sm shadow-md shadow-lg shadow-xl shadow-none.shadow(Shadow::sm()) up to Shadow::xl(), .shadow_none()
opacity-50 backdrop-blur-xl.opacity(0.5) .backdrop_blur(24.0)
blur-* on text only.text_blur(4.0)
translate-x-* translate-y-*.translate_x() .translate_y(), or .translate(Vector::new(x, y))
scale-* scale-x-* scale-y-* rotate-* origin-*.scale() .scale_x() .scale_y() .rotate(degrees) .origin(x, y)
pointer-events-none pointer-events-auto.pointer_events_none() .pointer_events_auto()
invisible.hidden(true), which keeps the element's space
hidden (CSS display: none).display_none()
cursor-pointer and other cursors.cursor(CursorIcon::Pointer)
transition duration-200 ease-in-out.transition(Spring::snappy()) or .transition(Curve::ease_in_out(200))
hover: active: focus: focus-visible:.hover(|s| ..) .active(|s| ..) .focus(|s| ..) .focus_visible(|s| ..)
disabled: dark:.when_disabled(|s| ..) .dark(|s| ..)
CSS @starting-style, exit animations.enter(|s| ..) .exit(|s| ..)
CSS top layer (popovers, dialogs).top_layer()
CrownOS frosted glass.material(Material::thin()) or Material::thick()
requestAnimationFramecx.request_animation_frame(|cx, now| ..)

A few rows need a note.

  • .pointer_events_none() and .pointer_events_auto() on a layer surface's root also decide the surface's input region. The boxes that opt in receive input. Everything else passes through to the window below.
  • .material(..) asks the compositor to draw the glass. It takes a bound value, so you can fade it.
  • cx.request_animation_frame steps motion on the UI thread. Use it only for motion that .transition(..) cannot express, such as a physics simulation or a value that drives layout.
  • Variants take paint, composite and text properties. A layout property inside a variant is ignored with CUI0201. See Errors.

DOM event to listener

Every listener lives on the Listeners trait. A handler receives (&mut Cx, &mut Event<T>).

DOMCrownUI
click dblclick contextmenu.on_click .on_double_click .on_context_menu with event.modifiers.ctrl_key, .shift_key and the rest
pointerdown pointerup pointermove.on_pointer_down .on_pointer_up .on_pointer_move
pointerenter pointerleave pointercancel.on_pointer_enter .on_pointer_leave .on_pointer_cancel
wheel scroll.on_wheel .on_scroll
keydown keyup.on_key_down .on_key_up
input change submit.on_input .on_change .on_submit, with the text in event.value
compositionupdate beforeinput.on_text_input
focus blur.on_focus .on_blur, with event.visible for focus that arrived by keyboard
dragstart drag dragend for a pointer gesture.on_drag_start .on_drag .on_drag_end
HTML drag and drop dragenter dragover.on_drag_over, then event.accept(mime) or event.accept_with(mime, DndAction::Move)
HTML drag and drop dragleave drop.on_drag_leave .on_drop, with event.data, event.mime_type and event.action
Starting an HTML dragcx.start_drag(DragSource { .. }, |cx, outcome| ..), ending in DragOutcome
accesskey, a keyboard shortcut.shortcut(keybind), which clicks the element while it is mounted
Touch and touchpad gestures (no DOM name).on_long_press .on_pinch
Media seekingthe kit waveform(..) option .on_seeking(|cx, position| ..)
navigator.clipboard.write and readcx.set_clipboard(vec![(mime, bytes)]) and cx.read_clipboard(&[mime], |cx, data| ..)
navigator.clipboard.writeText and readTextcx.copy_to_clipboard(text) and cx.paste_from_clipboard()
element.scrollTo and scrollIntoViewScrollHandle with scroll_to_offset, scroll_to_index and ensure_visible
beforeunloadcx.on_close_requested(|cx, event| event.prevent_default()) keeps the window open
window.close()cx.close_window()

event.stop_propagation() and event.prevent_default() work as they do on the web. Events bubble from the target to the root.

CrownOS adds a few window events with no DOM equivalent. They serve launchers and docks.

What happensCrownUI
The compositor asks a launcher to start an appcx.on_launch_app(|cx, app_id| ..), after cx.activate_app_icon(app_id)
The set of apps with windows changescx.on_app_windows(|cx, windows| ..)
The compositor starts or stops drawing a launcher icon during a window animationcx.on_app_icon_lifted(|cx, app_id, lifted| ..)

Accessibility attribute to modifier

These live on the Semantic trait. See Semantic.

ARIA or HTMLCrownUI
aria-label.label(..)
role.role(Role::Button)
aria-description.description(..)
aria-valuetext, an input's value.value(..)
tabindex="0" and tabindex="-1".focusable(true) and .focusable(false)
disabled.disabled(true)
type="password".secure()

Testing Library query and action

crowntest, the MCP ui_act tool and the screen reader bridge share one set of names.

Testing LibraryCrownUI
getByRole('button', { name: 'Save' })role(Role::Button).name("Save")
getByText('Saved')text("Saved"), which matches a substring of the name or value
queryAllByRoleapp.count(role(Role::Button))
user.click user.dblClick user.hoverapp.click(..) app.double_click(..) app.hover(..)
element.focus()app.focus(..)
user.type user.keyboardapp.type_text("milk") app.press_key("Ctrl+S")
fireEvent.scroll, a dragapp.scroll(query, dy) app.drag(query, x, y)

Web media to audio

These need the audio cargo feature. See Audio.

WebCrownUI
getUserMedia({ audio })audio::Microphone::open(MicrophoneOptions::default()), or kit::use_microphone(cx, options) inside a component
MediaTrackConstraints.deviceIdMicrophoneOptions::source(SourceId)
noiseSuppressionMicrophoneOptions::enhancement(Enhancement::NoiseSuppression)
enumerateDevices() and devicechangeaudio::sources(), a watch::Receiver<Vec<SourceInfo>>
AudioWorklet framesMicrophoneStream::take_frames(), a ring of AudioFrame
AnalyserNode levelsMicrophoneStream::level(), a watch::Receiver<Level> with peak and rms
track.readyStateMicrophoneStream::state(), a watch::Receiver<StreamState>

Why borrowed names, and what they cost

A name you already know needs no lookup. Someone who writes Tailwind reads .items_center().justify_between() without opening a manual. Someone who writes React reads .on_click and event.stop_propagation() the same way. AI agents gain the same way. They learned Tailwind, the DOM and Testing Library from public code, so they guess CrownUI's names right without a manual.

The rule is one obvious name per thing. When a concept has a web name, CrownUI uses it, adapted to Rust: snake_case and no hyphens. When it has none, as with compositor glass or app icons, CrownUI picks a plain descriptive name and adds a row here.

The cost is that the borrowed names bring borrowed expectations, and a few of them do not hold.

  • Tailwind's spacing scale is gone. .p(16.0) is 16 logical pixels, not 16 steps. Literal values keep the code exact and make the type f32, so a bound value like .p(bind(..)) works the same way.
  • hidden follows SwiftUI, not Tailwind. .hidden(true) keeps the space, like Tailwind's invisible. Use .display_none() for Tailwind's hidden.
  • Some Tailwind utilities have no method yet. Add one with the recipe in AGENTS.md, then add its row to this page in the same change.
  • Variants are closures, not prefixes, so hover:bg-blue-700 becomes .hover(|style| style.bg(..)). That costs a few characters, and in return the compiler checks every property inside the variant.