CharacterCounter
A text box or textarea with a live 120 / 280 count that warns near the limit, as a hard stop or a soft one that blocks sending.
Demo
20 / 20 characters
Over the limit it will not send.
7 characters typed.
11 / 12 characters
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/character_counter.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/character_counter.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/character_counter.qwe
component CharacterCounterDemoLive() { state text = "Ship it" <div.demo-stack> <CharacterCounter name="post" label="Post" limit={40} rows={3} hard={false} value={text} field="character-counter-demo-soft" hint="Over the limit it will not send." on_input={(t) => { text = t }}/> <p.demo-note>{len(text)} characters typed.</p> </div>}view CharacterCounterDemo() = <div.demo-stack.demo-narrow> <CharacterCounter name="headline" label="Headline" limit={20} value="Quillo UI 2.0 is out" field="character-counter-demo-hard" placeholder="Short and sharp" required={true}/> <CharacterCounterDemoLive/> <CharacterCounter name="tag" label="Small" size="sm" limit={12} value="eleven char" field="character-counter-demo-small"/> </div>Source
ui/character_counter.qwe
// CharacterCounter: a text box or a textarea with "120 / 280" under it that warns// as the limit comes near and turns red at it. It is a real <input> or <textarea>// with `name`, so a plain <form> sends it; the count is told to screen readers as// it changes.//// <CharacterCounter name="headline" label="Headline" limit={60}/>// <CharacterCounter name="post" label="Post" limit={280} rows={4} hard={false} on_input={(t) => { text = t }}/>//// limit: the number of characters allowed. hard (true): the box takes no more// (maxlength); false lets more be typed, counts how many over in red and marks// the box invalid, so the form will not send it. rows: a textarea of that many// lines (none: a one-line box). warn_at (0.9): the share of the limit at which// the count turns amber. value, placeholder, hint, error, required, disabled,// field (the id, default "character-counter-" + name) and size ("sm" or "md") as// in TextField.// on_input(text): inside one of your components, called with the text as it is typed.import ui/theme.qwe use ui_clslet character_counter_css = css`.ui-character-counter { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-character-counter-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-character-counter-req { margin-left: 2px; color: var(--ui-danger); }.ui-character-counter-control { box-sizing: border-box; display: block; width: 100%; margin: 0; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); transition: border-color .15s, box-shadow .15s;}input.ui-character-counter-control { height: 38px; }textarea.ui-character-counter-control { padding: 8px 12px; resize: vertical; min-height: 38px; }.ui-character-counter-control::placeholder { color: var(--ui-muted); opacity: 1; }.ui-character-counter-control:hover { border-color: var(--ui-strong); }.ui-character-counter-control:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-character-counter-control[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-character-counter-control:disabled { opacity: .55; cursor: not-allowed; }.ui-character-counter-sm input.ui-character-counter-control { height: 30px; font-size: 13px; }.ui-character-counter-sm textarea.ui-character-counter-control { padding: 5px 10px; font-size: 13px; }.ui-character-counter-foot { display: flex; align-items: baseline; gap: 12px; }.ui-character-counter-foot > div { flex: 1; display: grid; gap: 4px; }.ui-character-counter-hint, .ui-character-counter-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-character-counter-error { color: var(--ui-danger); }.ui-character-counter-count { flex: none; margin-left: auto; font-size: 12px; color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-character-counter-near { color: var(--ui-warning); }.ui-character-counter-full, .ui-character-counter-over { color: var(--ui-danger); font-weight: 500; }.ui-character-counter-bar { height: 3px; border-radius: 2px; background: var(--ui-subtle); overflow: hidden; }.ui-character-counter-bar span { display: block; height: 100%; width: var(--p); background: var(--ui-accent); transition: width .15s, background .15s; }.ui-character-counter-near .ui-character-counter-bar span { background: var(--ui-warning); }.ui-character-counter-full .ui-character-counter-bar span, .ui-character-counter-over .ui-character-counter-bar span { background: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-character-counter-control, .ui-character-counter-bar span { transition: none; } }`component CharacterCounter(name: str? = null, label: str? = null, limit: int = 280, hard: bool = true, rows: int? = null, warn_at: num = 0.9, value: str = "", placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_input = null, class: str? = null) { style global character_counter_css state text = value let id = field ?? `character-counter-${name ?? re.replace(lower(label ?? "field"), "[^a-z0-9]+", "-")}` let used = len(text) let over = used > limit let phase = over ? "over" : used == limit ? "full" : used >= limit * warn_at ? "near" : "" let shown_error = error ?? (over ? `${used - limit} characters over the limit.` : null) let described = ui_cls([hint != null && `${id}-hint`, shown_error != null && `${id}-error`, `${id}-count`]) let common = { id, name, placeholder, required, disabled, maxlength: hard ? limit : null, "aria-invalid": shown_error != null ? "true" : null, "aria-describedby": described, } let typed = (e) => { text = e.value e.target.setCustomValidity(!hard && len(e.value) > limit ? "Too long" : "") on_input && on_input(e.value) } <div class={ui_cls(["ui-character-counter", size == "sm" && "ui-character-counter-sm", phase != "" && `ui-character-counter-${phase}`, class])}> {label != null && <label.ui-character-counter-label for={id}> {label}{required && <span.ui-character-counter-req aria-hidden="true">*</span>} </label>} {rows != null ? <textarea.ui-character-counter-control {...common} rows={rows} @input={typed}>{text}</textarea> : <input.ui-character-counter-control type="text" {...common} value={text} @input={typed}/>} <div.ui-character-counter-bar aria-hidden="true"><span style={`--p: ${min([100, round(used * 100 / limit)])}%`}></span></div> <div.ui-character-counter-foot> <div> {hint != null && <p.ui-character-counter-hint id={`${id}-hint`}>{hint}</p>} {shown_error != null && <p.ui-character-counter-error id={`${id}-error`} role="alert">{shown_error}</p>} </div> <span.ui-character-counter-count id={`${id}-count`} aria-live="polite" aria-atomic="true"> {used} / {limit}<span.ui-sr> characters</span> </span> </div> </div>}Related
InlineEditText that turns into a box to change it in place: Enter saves, Escape puts it back.5 views · 8 downloadsMarkdownEditorA Markdown textarea with a toolbar that wraps the selection (bold, italic, link, list) and a Preview tab rendered as the page will show it.5 views · 9 downloadsMaskedInputA box that shapes what is typed to a pattern (### ###-####, AA-00-AA) as it is typed, dropping what does not fit.6 views · 8 downloadsRichTextEditorA formatted-text editor (bold, italic, lists, links, headings) on contenteditable, its HTML kept in a hidden input a form sends.7 views · 5 downloadsTextAreaA box for several lines, with a live count against a limit and an option to grow with what is typed.5 views · 8 downloadsTextFieldA labelled input with help text, an error message and short text before or after it.5 views · 8 downloads
Thread
Thread 0 posts
On CharacterCounter: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.