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
| SwiftUI | CrownUI |
|---|---|
Text, Button, Toggle, TextField | text(..), button(..), toggle(on), text_field(value, placeholder) |
VStack, HStack, ZStack, Spacer, Divider | vstack(..), hstack(..), zstack(..), spacer(), divider() |
ScrollView, LazyVStack, LazyVGrid | scroll_view(..), lazy_vstack(..) or lazy_list(..), lazy_grid(..) |
Image, Canvas | image(width, height, rgba), svg(..), icon(..), canvas(..) |
if in a body, ForEach, .id(..) | show(cond, || then, || otherwise), for_each(items, key, row), keyed(..) |
@State | cx.signal(value) |
withAnimation | cx.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.
| Tailwind | CrownUI |
|---|---|
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() |
requestAnimationFrame | cx.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_framesteps 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>).
| DOM | CrownUI |
|---|---|
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 drag | cx.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 seeking | the kit waveform(..) option .on_seeking(|cx, position| ..) |
navigator.clipboard.write and read | cx.set_clipboard(vec![(mime, bytes)]) and cx.read_clipboard(&[mime], |cx, data| ..) |
navigator.clipboard.writeText and readText | cx.copy_to_clipboard(text) and cx.paste_from_clipboard() |
element.scrollTo and scrollIntoView | ScrollHandle with scroll_to_offset, scroll_to_index and ensure_visible |
beforeunload | cx.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 happens | CrownUI |
|---|---|
| The compositor asks a launcher to start an app | cx.on_launch_app(|cx, app_id| ..), after cx.activate_app_icon(app_id) |
| The set of apps with windows changes | cx.on_app_windows(|cx, windows| ..) |
| The compositor starts or stops drawing a launcher icon during a window animation | cx.on_app_icon_lifted(|cx, app_id, lifted| ..) |
Accessibility attribute to modifier
These live on the Semantic trait. See Semantic.
| ARIA or HTML | CrownUI |
|---|---|
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 Library | CrownUI |
|---|---|
getByRole('button', { name: 'Save' }) | role(Role::Button).name("Save") |
getByText('Saved') | text("Saved"), which matches a substring of the name or value |
queryAllByRole | app.count(role(Role::Button)) |
user.click user.dblClick user.hover | app.click(..) app.double_click(..) app.hover(..) |
element.focus() | app.focus(..) |
user.type user.keyboard | app.type_text("milk") app.press_key("Ctrl+S") |
fireEvent.scroll, a drag | app.scroll(query, dy) app.drag(query, x, y) |
Web media to audio
These need the audio cargo feature. See Audio.
| Web | CrownUI |
|---|---|
getUserMedia({ audio }) | audio::Microphone::open(MicrophoneOptions::default()), or kit::use_microphone(cx, options) inside a component |
MediaTrackConstraints.deviceId | MicrophoneOptions::source(SourceId) |
noiseSuppression | MicrophoneOptions::enhancement(Enhancement::NoiseSuppression) |
enumerateDevices() and devicechange | audio::sources(), a watch::Receiver<Vec<SourceInfo>> |
AudioWorklet frames | MicrophoneStream::take_frames(), a ring of AudioFrame |
AnalyserNode levels | MicrophoneStream::level(), a watch::Receiver<Level> with peak and rms |
track.readyState | MicrophoneStream::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 typef32, so a bound value like.p(bind(..))works the same way. hiddenfollows SwiftUI, not Tailwind..hidden(true)keeps the space, like Tailwind'sinvisible. Use.display_none()for Tailwind'shidden.- 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-700becomes.hover(|style| style.bg(..)). That costs a few characters, and in return the compiler checks every property inside the variant.