> ## Documentation Index
> Fetch the complete documentation index at: https://developers.reintersect.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Viewer inputs, votes and tallies

> Choices each member makes on their own, stored by Reintersect, with live counts and voter lists your app never has to compute.

> Reintersect apps are in developer preview. Names and limits can still change before general
> release.

A viewer input is a choice each member makes for themselves: a vote, an RSVP, a go or no-go call.
Reintersect stores each member's pick and keeps the counts live for every viewer. Your render
function never sees individual picks, so one shared tree serves everyone.

## Declare an input

Declare inputs on the block with `choice()`, then render them with a `ChoiceGroup`:

```tsx theme={null}
import { choice, defineBlock } from "@reintersect/sdk/app";
import { Block, ChoiceGroup, Header } from "@reintersect/sdk/components";
import { z } from "zod";

export const GoNoGo = defineBlock({
  type: "go-no-go",
  title: "Go / no-go",
  insertable: true,
  props: z.object({ title: z.string() }),
  state: z.object({ closed: z.boolean() }),
  initialState: { closed: false },
  inputs: { verdict: choice(["go", "nogo"]) },
}).render((props, { inputs }) => (
  <Block summary={`Go / no-go: ${props.title}`}>
    <Header>{props.title}</Header>
    <ChoiceGroup
      input={inputs.verdict}
      label="Your call"
      appearance="segmented"
      labels={{ go: "Go", nogo: { label: "No go", description: "Say why in the thread." } }}
    />
  </Block>
));
```

`choice(values, options)` takes 1 to 20 values (each 1 to 64 characters from `A-Z`, `a-z`, `0-9`, `_`,
`.` and `-`) and these options:

| Option | Default | Description |
| - | - | - |
| `multiple` | `false` | Members may pick several values. |
| `anonymous` | `false` | Counts only. Reintersect never records who picked what anywhere a client or your app can read it. |

`labels` must name every declared value; a missing one is a type error. Labels are up to 60
characters and unique within the group. Each input may be rendered once: input and field names are
unique across the whole block, including across two forms.

`appearance` is a preference. Reintersect uses a segmented control for 2 to 4 short choices and
chips for up to 8, and falls back to a list when the choices don't fit.

## What members see

With the default `scope="viewer"` and `results="host"`:

* A member's pick shows immediately. Reintersect saves it in the background with no pending state.
* Every viewer sees live counts and fill bars, and, unless the input is anonymous, the faces of the
  members behind each choice.
* With `clearable`, a member can take back a single pick.
* When the block becomes [final](/blocks/audience-and-lifecycle#final-blocks), picks stop and the last
  counts stay. There's no `disabled` prop, so closing a vote means returning `final` from an action,
  as `close` does below, or no longer rendering the group.

Read-only views (share pages and connected workspaces) show the counts without voters. Agent text
lists each choice with its current count.

## Read tallies in an action

The render function doesn't receive tallies, because one tree serves every viewer and counts change
without a re-render. Actions read them. Declare the inputs an action needs with `inputs`:

```ts theme={null}
import { choice, defineAction } from "@reintersect/sdk/app";
import { z } from "zod";

const verdict = choice(["go", "nogo"]);

export const close = defineAction(
  "close",
  {
    state: z.object({ closed: z.boolean() }),
    inputs: { verdict },
    destructive: true,
    description: "Close the vote and announce the result",
  },
  ({ tallies }) => {
    const { go, nogo } = tallies.verdict;

    return {
      state: { closed: true },
      final: { summary: go > nogo ? `Go: ${go} to ${nogo}` : `No go: ${nogo} to ${go}` },
    };
  },
);
```

`tallies.verdict` is typed by the declared values, so `tallies.verdict.maybe` is a type error. An
action that tallies an input its block doesn't declare fails to attach.

## Results modes

| `results` | Who shows the results |
| - | - |
| `"host"` (default) | Reintersect: counts, fill bars, voters and the winner once the block is final. |
| `"integration"` | Your app. Give the group an `onChange` action; it runs after each pick with the new `tallies`, and the block re-renders from the state it returns. Without `onChange`, Reintersect shows its own counts. |
| `"none"` | Nobody. Use it for private picks such as "Remind me". |

## Shared choices

`scope="shared"` turns the group into one control for the whole block instead of one per member. The
pick isn't stored per member: it goes to the group's `onChange` action as
`input: { [inputName]: values }`, and your action decides what to store in state. A shared group
outside a form must have `onChange`, because the action is the only place a shared value is kept;
`reintersect check` and Reintersect reject one without it.

```tsx theme={null}
<ChoiceGroup
  input={inputs.priority}
  label="Priority"
  scope="shared"
  results="none"
  labels={{ low: "Low", high: "High" }}
  onChange={actions.setPriority}
/>
```

A `ChoiceGroup` inside a [`Form`](/blocks/sections-and-forms#forms) is a form field instead: its
picks are submitted with the rest of the form's input.

## Audience and anonymity

Tallies follow [audience](/blocks/audience-and-lifecycle#audience). Counts for a `ChoiceGroup` inside
an audience-limited part of a block are visible only to members who pass that audience. Anonymous
inputs never record voters, so facepiles and `voters` stay empty.

## Test it

The [testing kit](/testing) records votes and reads tallies the same way Reintersect does:

```ts theme={null}
import { createTestHost, testViewer } from "@reintersect/sdk/testing";

const host = createTestHost(app);
const block = await host.insert(GoNoGo, { title: "Launch v2.4" });

await host.as(testViewer("Ada")).vote(block, "verdict", "go");
await host.as(testViewer("Grace")).vote(block, "verdict", "nogo");

expect(await host.tallies(block)).toEqual({ verdict: { go: 1, nogo: 1 } });
expect(await host.voters(block, "verdict", "go")).toEqual(["member_ada"]);
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.