Skip to content

Headless Components

A headless component gives a component the behavior of a common control, such as which keys edit text and where the cursor goes, or which keys scroll a box, and leaves drawing it to the component, as Headless UI and React Aria do for the web. Each one is a hook in counterweight.headless, and counterweight.widgets draws each one way, for when you don't need your own look:

Hook Widget Behavior
use_text_input text_input Edits one line of text
use_text_area text_area Edits text that wraps onto rows
use_scroll_area scroll_area Scrolls a box with the keys and the wheel

Styling the widgets

The text field widgets take a style, merged over their box's own style, for a size or a border, and styles, a TextFieldStyles that says how they draw their box while unfocused, focused, and unfocusable, their cursor, and their placeholder. Pass the fields you want to change, and share one TextFieldStyles between all the fields of a form:

AMBER = TextFieldStyles(
    box=border | border_heavy | border_color("slate", 500) | pad_x(1),
    focused_box=border_color("amber", 400),
    cursor=(text_color("slate", 900) | text_bg("amber", 300)).text_style,
    placeholder=CellStyle(italic=True) | text_color("slate", 400).text_style,
)


@component
def amber_sign_up() -> Div:
    name, set_name = use_state("")
    email, set_email = use_state("")
    return Div(
        style=col | width(30),
        on_input=tab_focus,
        children=(
            text_input(name, set_name, placeholder="Name", styles=AMBER, auto_focus=True),
            text_input(email, set_email, placeholder="Email", styles=AMBER),
        ),
    )

Two heavy-bordered fields, the focused one amber, with "Ada" typed into it

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Ada                        ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Email                      ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

The box's styles merge as a stylesheet's rules for an <input> apply: box, then the widget's style, then focused_box while it's focused, as a rule for input:focus overrides one for input, or unfocusable_box while it isn't focusable, as a rule for input:disabled does. So a style with a border color sets the color the box has while it isn't focused, and the focused box still shows which field has focus.

scroll_area takes a style and styles the same way, with a ScrollAreaStyles for its box while unfocused and focused.

API

counterweight.widgets.TextFieldStyles dataclass

TextFieldStyles(
    *,
    box: Style = border
    | border_lightrounded
    | border_color("slate", 600)
    | pad_x(1),
    focused_box: Style = border_color("indigo", 400),
    unfocusable_box: Style = text_color("slate", 600),
    cursor: CellStyle = text_style,
    placeholder: CellStyle = text_style,
)

How text_input and text_area draw their box, their cursor, and their placeholder.

A widget's box is styled by box, then the style passed to the widget, then focused_box while it's focused, or unfocusable_box while it isn't focusable, as a CSS rule for input:focus or input:disabled overrides one for input, so a style with another border color still shows which widget is focused. Pass the fields you want to change: TextFieldStyles(focused_box=border_color("amber", 400)).

box class-attribute instance-attribute

box: Style = (
    border
    | border_lightrounded
    | border_color("slate", 600)
    | pad_x(1)
)

The box around the text.

focused_box class-attribute instance-attribute

focused_box: Style = border_color('indigo', 400)

Merged over box and the widget's style while the widget is focused.

unfocusable_box class-attribute instance-attribute

unfocusable_box: Style = text_color('slate', 600)

Merged over box and the widget's style while the widget isn't focusable.

cursor class-attribute instance-attribute

cursor: CellStyle = (
    text_color("slate", 900) | text_bg("slate", 100)
).text_style

Merged over the text for the cell the cursor is on, while the widget is focused.

placeholder class-attribute instance-attribute

placeholder: CellStyle = text_color("slate", 500).text_style

Merged over the text for the placeholder, which the widget shows in place of an empty value.

box_style

box_style(
    style: Style, is_focused: bool, is_focusable: bool
) -> Style

The style of a widget's box, with style merged over box, under the styles for its state.