Primitives

text_field

A single-line editable field bound to a String signal, with input, change and submit events.

FNtext_field
Usage
use crownui::prelude::*;
 
fn greeting() -> impl View {
    component(|cx| {
        let name = cx.signal(String::new());
        vstack((
            text_field(name, "Your name").label("Name"),
            text(bind(move |rt| {
                let name = name.get(rt);
                if name.is_empty() { "Hello!".to_string() } else { format!("Hello, {name}!") }
            })),
        ))
        .gap(8.0)
        .p(16.0)
    })
}
Signature
pub fn text_field(value: Signal<String>, placeholder: impl Into<String>) -> Element<TextFieldKind>

text_field is a single-line editable field bound to a Signal<String>. Typing and input method commits insert at the caret, the arrow keys move it, and Enter submits.

Both arguments are required. The field writes every edit into value, and anything that sets value from code updates the field.

Parameters

valueSignal<String>required

The text in the field. The field reads it to draw and writes it on every edit. It is also the field's value in the semantic tree.

placeholderimpl Into<String>required

Gray text shown while value is empty. Pass "" for no placeholder. The placeholder is fixed when the field mounts.

Returns

text_fieldElement<TextFieldKind>

The field element. Chain style, listener and semantic methods on it.

Events

The field fires three DOM-named events. Each handler receives &mut Event<InputEvent>, and event.value holds the full text.

ListenerFires when
.on_input(..)The text changed: a typed character, a paste, Backspace, Delete, Ctrl+X or an input method commit.
.on_change(..)The user commits the text: on Enter and when the field loses focus.
.on_submit(..)The user presses Enter.
let query = cx.signal(String::new());
let results = cx.signal(Vec::<String>::new());
text_field(query, "Search")
    .label("Search")
    .on_input(move |cx, event| {
        let needle = event.value.to_lowercase();
        results.set(cx, vec![format!("Results for {needle}")]);
    })
    .on_submit(move |cx, _| query.set(cx, String::new()))

Most code does not need on_input. Read the signal with bind or a memo, and the UI follows every keystroke.

Keyboard

KeyAction
Any character or SpaceInserts it at the caret.
Left and Right arrowsMove the caret by one character.
Home and EndMove the caret to the start or the end.
Backspace and DeleteRemove the character before or after the caret.
EnterFires change and submit.
Ctrl+C or Meta+CCopies the whole text.
Ctrl+X or Meta+XCopies the whole text and clears the field.
Ctrl+V or Meta+VPastes text from the clipboard at the caret.

Clicking the field focuses it. The caret is a 2 pixel blue bar that shows only while the field has focus. Input methods for languages such as Chinese and Japanese commit their text through the same path as typing.

Default style

PropertyDefaultOverride with
Directionrownot usually changed
Cross axis alignmentcenter.items_*()
Horizontal padding10.px(..)
Vertical padding6.py(..)
Corner radius8.rounded(..)
Backgroundgray at 12% alpha.bg(..)
CursorCursorIcon::Text.cursor(..)
Placeholder colorgray at 80% alphafixed
let email = cx.signal(String::new());
text_field(email, "you@example.com")
    .label("Email")
    .w_full()
    .px(12.0)
    .py(8.0)
    .rounded(10.0)
    .bg(WHITE)
    .border(1.0)
    .border_color(rgba(0x0f172a26))
    .focus(|style| style.ring(2.0, rgb(0x93c5fd)).border_color(rgb(0x3b82f6)))
    .transition(Spring::snappy())

Text properties set on the field, such as .text_size(..) and .text_color(..), reach the typed text.

Labels and testing

A field's placeholder is not its name. Give every field a .label(..) so screen readers, tests and agents find it.

let mut app = TestApp::new(component(|cx| {
    let draft = cx.signal(String::new());
    let saved = cx.signal(String::new());
    vstack((
        text_field(draft, "What needs doing?")
            .label("New todo")
            .on_submit(move |cx, event| saved.set(cx, event.value.clone())),
        text(bind(move |rt| format!("Saved: {}", saved.get(rt)))),
    ))
}));
app.click(role(Role::TextField).name("New todo"))
    .type_text("eggs")
    .press_key("Enter");
assert_eq!(app.text_of(role(Role::Text).text("Saved")), "Saved: eggs");

type_text needs a focused field, so click or focus it first.

Focusing a field when it appears

Call cx.request_focus(node) on the field's node. Build the field yourself in a small View to get the node.

use crownui::ext::NodeId;
use crownui::prelude::*;
 
struct Autofocus<V>(V);
 
impl<V: View> View for Autofocus<V> {
    fn build(self, cx: &mut Cx) -> NodeId {
        let node = self.0.build(cx);
        cx.request_focus(node);
        node
    }
}
 
let password = cx.signal(String::new());
Autofocus(text_field(password, "Password").label("Password"))

Secure fields

.secure() marks the field as secure in the semantic tree. Agents cannot read its value or type into it, and they get the error CUI0105 if they try. The primitive field still draws the text as typed. For a password field that hides the characters, use the kit's text_input(value).secret(). See kit inputs.

Limits

The primitive field is deliberately small.

  • It is one line. Enter submits instead of adding a line break.
  • There is no selection. Copy and cut act on the whole text.
  • It shows no preedit text while an input method is composing. Only the committed text appears.
  • The placeholder is fixed at mount.

The kit's text_input(value) covers more: selection, a .secret() mode, .read_only(), a clear button, a leading icon, pill shapes and theme colors. Reach for it in CrownOS apps.

Why text_field works this way

The field stores its text in the signal you pass, not in a private buffer. That makes the signal the single source of truth. Clearing a search box is query.set(cx, String::new()), and a field restored from settings is a signal created with the saved string. The cost is that every keystroke writes the signal and runs every binding that reads it. For a field that drives an expensive search, read the value in on_change or on_submit instead of binding to every keystroke.

Edits count characters, not bytes, so the caret moves over multibyte characters such as é correctly. The caret itself is a plain child box that show mounts while the field has focus, so the field needs no custom painting.