Forms

Poll

Vote for one option with a tap and see the results as bars with each one's share; the vote also goes in a hidden input.

#forms#button#social#feedback#interactive

Demo

Preview

Which language do you write most?

Pick one to vote

No vote yet.

Tabs or spaces?

  • Tabs30%
    12 votes
  • Spaces (your vote)70%
    28 votes
40 people answered

Best season for a release? (closed)

  • Spring0%
    0 votes
  • Summer0%
    0 votes
0 votes

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

Example

The code of the preview above.

demos/poll.qwe
let poll_demo_options = [    { label: "Quillo", value: "quillo", votes: 42 }, { label: "Rust", value: "rust", votes: 31 },    { label: "TypeScript", value: "ts", votes: 18 }, { label: "Python", value: "py", votes: 9 },]component PollDemoLive() {    state vote = null    <div.demo-stack>        <Poll id="poll-demo-live" question="Which language do you write most?" options={poll_demo_options} change={true}              on_vote={(v) => { vote = v }}/>        <p.demo-note>{vote == null ? "No vote yet." : `on_vote got ${vote}.`}</p>    </div>}view PollDemo() =    <div.demo-stack>        <PollDemoLive/>        <Poll id="poll-demo-voted" question="Tabs or spaces?" voted="spaces" total_text="{n} people answered"              options={[{ label: "Tabs", value: "tabs", votes: 12 }, { label: "Spaces", value: "spaces", votes: 28 }]}/>        <Poll id="poll-demo-closed" question="Best season for a release? (closed)" results={true} options={["Spring", "Summer"]}/>    </div>

Source

ui/poll.qwe
// Poll: a question with options to vote for, one tap each; once someone has// voted the options turn into bars with each one's share. The choice goes in a// hidden input named `name`, so a plain <form> can send it too.////   <Poll id="lang" question="Which language do you write most?" on_vote={(v) => { save_vote(v) }}//         options={[{ label: "Quillo", value: "quillo", votes: 42 }, { label: "Rust", value: "rust", votes: 31 }]}/>//   <Poll id="lang" question="..." options={opts} voted="rust"/>//   <Poll id="lang" question="..." options={opts} results={true}/>//// id: a prefix for its ids, unique on the page. question: the question asked.// options: maps of { label, value, votes? } (votes: the count so far, 0 when// left out), or strings. voted: the value this person chose already, which shows// the results with it marked. results: show the results without a vote (a closed// poll, or one that shows them first). name: the hidden input's name ("vote").// change (false): a vote can be changed afterwards. total_text: the line under// the bars ("{n} votes"; "{n}" is the count).// on_vote(value): inside one of your components, called with the value chosen// when someone votes (and again when they change it, with change={true}).import ui/theme.qwe use ui_clslet poll_css = css`.ui-poll { display: grid; gap: 12px; min-width: 0; max-width: 480px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-poll-question { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; }.ui-poll-options { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-poll-option {  display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; padding: 10px 14px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: inherit; text-align: left; cursor: pointer;  transition: border-color .15s, background .15s, box-shadow .15s;}.ui-poll-option:hover { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-poll-option:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-poll-result { position: relative; display: grid; gap: 4px; padding: 8px 12px; border-radius: var(--ui-radius-sm); overflow: hidden; background: var(--ui-subtle); }.ui-poll-result::before { content: ""; position: absolute; inset: 0; width: var(--p); background: var(--ui-accent-soft); transition: width .4s; }.ui-poll-mine::before { background: color-mix(in srgb, var(--ui-accent) 28%, var(--ui-accent-soft)); }.ui-poll-mine { box-shadow: inset 0 0 0 1px var(--ui-accent); }.ui-poll-line { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 10px; }.ui-poll-name { display: flex; align-items: center; gap: 8px; font-weight: 500; }.ui-poll-name svg { width: 14px; height: 14px; color: var(--ui-accent); }.ui-poll-pct { font-variant-numeric: tabular-nums; font-weight: 600; }.ui-poll-count { position: relative; font-size: 12px; color: var(--ui-muted); }.ui-poll-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--ui-muted); }.ui-poll-change { padding: 0; border: 0; background: none; color: var(--ui-accent); font: 500 13px var(--ui-font); cursor: pointer; border-radius: 3px; }.ui-poll-change:hover { text-decoration: underline; }.ui-poll-change:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-poll-option, .ui-poll-result::before { transition: none; } }`fn poll_option(o) = type_of(o) == "map" ? { label: o.label ?? str(o.value), value: str(o.value ?? o.label), votes: o.votes ?? 0 }                                        : { label: str(o), value: str(o), votes: 0 }component Poll(id: str, question: str, options: list = [], voted: str? = null, results: bool = false, name: str = "vote",               change: bool = false, total_text: str = "{n} votes", on_vote = null, class: str? = null) {    style global poll_css    let opts = options | map(poll_option)    state mine = voted    state changing = false    // The counts with this person's vote added, when it was cast here.    state added = null    let counts = opts | map((o) => o.votes + (o.value == added ? 1 : 0))    let total = sum(counts) ?? 0    let show = (mine != null || results) && !changing    let pct = (n: int) => total == 0 ? 0 : round(n * 100 / total)    let count_text = replace(total_text, "{n}", str(total))    <div class={ui_cls(["ui-poll", class])} role="group" aria-labelledby={`${id}-q`}>        <p.ui-poll-question id={`${id}-q`}>{question}</p>        {show            ? <ul.ui-poll-options aria-label="Results" aria-live="polite">                {for [i, o] in enumerate(opts) => <li key={o.value} class={ui_cls(["ui-poll-result", o.value == mine && "ui-poll-mine"])}                        style={`--p: ${pct(counts[i])}%`} aria-current={o.value == mine ? "true" : null}>                    <div.ui-poll-line>                        <span.ui-poll-name>                            {o.value == mine && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"                                                     stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>}                            {o.label}{o.value == mine && <span.ui-sr> (your vote)</span>}                        </span>                        <span.ui-poll-pct>{pct(counts[i])}%</span>                    </div>                    <span.ui-poll-count>{counts[i]} {counts[i] == 1 ? "vote" : "votes"}</span>                </li>}            </ul>            : <ul.ui-poll-options aria-label="Options">                {for o in opts => <li key={o.value}>                    <button.ui-poll-option type="button" @click={() => {                        if added != null && mine != null { added = o.value } else if mine == null { added = o.value }                        mine = o.value                        changing = false                        on_vote && on_vote(o.value)                    }}>                        <span>{o.label}</span>                        <span.ui-sr>Vote for {o.label}</span>                    </button>                </li>}            </ul>}        <input type="hidden" name={name} value={mine ?? ""}/>        <div.ui-poll-foot>            <span>{show ? count_text : mine != null ? "Pick another option" : "Pick one to vote"}</span>            {show && change && mine != null && <button.ui-poll-change type="button" @click={() => { changing = true }}>Change vote</button>}        </div>    </div>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.