Forms

FontPicker

A drop-down of fonts, each option drawn in itself, with a preview line in the one chosen.

#forms#select#picker#typography#no-js

Demo

Preview

Inter

Each option is drawn in its font where the browser allows.

The quick brown fox jumps over the lazy dog

Headings in Georgia.

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/font_picker.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/font_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/font_picker.qwe
component FontPickerDemoLive() {    state font = "Georgia"    <div.demo-stack>        <FontPicker name="heading_font" label="Headings" value={font} field="font-picker-demo-live"                    preview="The quick brown fox jumps over the lazy dog" on_change={(f) => { font = f }}/>        <p.demo-note>Headings in {font}.</p>    </div>}view FontPickerDemo() =    <div.demo-stack.demo-narrow>        <FontPicker name="font" label="Body font" value="Inter" field="font-picker-demo-all" preview=""                    hint="Each option is drawn in its font where the browser allows." required={true}/>        <FontPickerDemoLive/>        <div.demo-row>            <FontPicker name="code_font" label="Code" size="sm" placeholder="Choose a font" field="font-picker-demo-small"                        fonts={[{ label: "JetBrains Mono", value: "jetbrains", stack: "'JetBrains Mono', monospace" }, "Menlo", "Courier New"]}/>            <FontPicker name="locked" label="Disabled" size="sm" value="Arial" fonts={["Arial"]} disabled={true}/>        </div>    </div>

Source

ui/font_picker.qwe
// FontPicker: a drop-down of fonts, each option shown in the font it names. It is// the browser's own <select>, so a plain <form> sends the chosen value as `name`// (and the system's own popup may show every option in one font: the preview// line under the select always shows the font chosen).////   <FontPicker name="font" label="Font" value="Georgia"/>//   <FontPicker name="heading_font" label="Headings" preview="The quick brown fox" on_change={(f) => { font = f }}//               fonts={[{ label: "Inter", value: "inter", stack: "Inter, sans-serif" }, "Georgia", "Courier New"]}/>//// fonts: strings (the font's name is the value and the stack), or maps of// { label, value, stack? } (stack: the CSS font-family it is shown in); the// built-in list is fourteen fonts most systems have (`font_picker_fonts`).// value: the one chosen to start with. preview: a line shown under the select in// the chosen font (none by default; "" shows the font's own name). placeholder: a// first line that cannot be chosen. hint, error, required, disabled, field (the// id, default "font-picker-" + name) and size ("sm" or "md") as in Select.// on_change(value): inside one of your components, given the value chosen. The// preview follows `value`, so a component that keeps it in state keeps it right.import ui/theme.qwe use ui_clslet font_picker_css = css`.ui-font-picker { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-font-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-font-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-font-picker-box { position: relative; display: block; }.ui-font-picker-box::after {  content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px;  border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-font-picker select {  appearance: none; display: block; width: 100%; height: 38px; padding: 0 36px 0 12px;  background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font);  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer;  text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-font-picker select:hover { border-color: var(--ui-strong); }.ui-font-picker select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-font-picker select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-font-picker option { color: var(--ui-fg); background: var(--ui-surface); font-size: 15px; }.ui-font-picker-preview {  margin: 0; padding: 10px 12px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm);  font-size: 20px; line-height: 1.3; overflow-wrap: anywhere;}.ui-font-picker-sm select { height: 30px; padding: 0 30px 0 10px; font-size: 13px; }.ui-font-picker-sm .ui-font-picker-box::after { right: 11px; width: 6px; height: 6px; margin-top: -5px; }.ui-font-picker-sm .ui-font-picker-preview { font-size: 16px; padding: 6px 10px; }.ui-font-picker-invalid select, .ui-font-picker-invalid select:hover { border-color: var(--ui-danger); }.ui-font-picker-hint, .ui-font-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-font-picker-error { color: var(--ui-danger); }.ui-font-picker-disabled { opacity: .55; }.ui-font-picker-disabled select { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-font-picker select { transition: none; } }`let font_picker_fonts = [    { label: "Arial", value: "Arial", stack: "Arial, Helvetica, sans-serif" },    { label: "Comic Sans MS", value: "Comic Sans MS", stack: "'Comic Sans MS', 'Comic Sans', cursive" },    { label: "Courier New", value: "Courier New", stack: "'Courier New', Courier, monospace" },    { label: "Garamond", value: "Garamond", stack: "Garamond, 'EB Garamond', serif" },    { label: "Georgia", value: "Georgia", stack: "Georgia, 'Times New Roman', serif" },    { label: "Helvetica", value: "Helvetica", stack: "Helvetica, Arial, sans-serif" },    { label: "Impact", value: "Impact", stack: "Impact, 'Arial Black', sans-serif" },    { label: "Inter", value: "Inter", stack: "Inter, system-ui, sans-serif" },    { label: "Menlo", value: "Menlo", stack: "Menlo, Consolas, monospace" },    { label: "Palatino", value: "Palatino", stack: "Palatino, 'Palatino Linotype', 'Book Antiqua', serif" },    { label: "System UI", value: "system-ui", stack: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" },    { label: "Times New Roman", value: "Times New Roman", stack: "'Times New Roman', Times, serif" },    { label: "Trebuchet MS", value: "Trebuchet MS", stack: "'Trebuchet MS', Tahoma, sans-serif" },    { label: "Verdana", value: "Verdana", stack: "Verdana, Geneva, sans-serif" },]// A font as { label, value, stack }, from a string or a map.fn font_picker_font(f) = type_of(f) == "map"    ? { label: f.label ?? str(f.value), value: str(f.value ?? f.label), stack: f.stack ?? str(f.value ?? f.label) }    : { label: str(f), value: str(f), stack: str(f) }// A font stack only made of what one can be: names, quotes, commas, dashes.fn font_picker_safe(stack: str) -> str = re.test(stack, "^[A-Za-z0-9 ,'_-]{1,120}$") ? stack : "inherit"view FontPicker(name: str? = null, label: str? = null, value: str? = null, fonts: list? = null, preview: str? = null,                placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false,                disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) {    style global font_picker_css    let id = field ?? `font-picker-${name ?? "font"}`    let items = (fonts ?? font_picker_fonts) | map(font_picker_font)    let chosen = value != null ? find(items, (f) => f.value == value) : null    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`, preview != null && `${id}-preview`])    <div class={ui_cls(["ui-font-picker", size == "sm" && "ui-font-picker-sm", error != null && "ui-font-picker-invalid", disabled && "ui-font-picker-disabled", class])}>        {label != null && <label.ui-font-picker-label for={id}>            {label}{required && <span.ui-font-picker-req aria-hidden="true">*</span>}        </label>}        <span.ui-font-picker-box>            <select id={id} name={name} required={required} disabled={disabled}                    style={chosen != null ? `font-family: ${font_picker_safe(chosen.stack)}` : null}                    aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}                    @change={(e) => { on_change && on_change(e.value) }}>                {placeholder != null && <option value="" disabled selected={chosen == null}>{placeholder}</option>}                {for f in items => <option value={f.value} selected={chosen != null && f.value == chosen.value}                                           style={`font-family: ${font_picker_safe(f.stack)}`}>{f.label}</option>}            </select>        </span>        {preview != null && <p.ui-font-picker-preview id={`${id}-preview`} aria-live="polite"                style={chosen != null ? `font-family: ${font_picker_safe(chosen.stack)}` : null}>            {preview != "" ? preview : chosen != null ? chosen.label : "No font chosen"}        </p>}        {hint != null && <p.ui-font-picker-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-font-picker-error id={`${id}-error`} role="alert">{error}</p>}    </div>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

On FontPicker: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.