Skip to content
txbrendan/ui

Consensus Meter

beta

How a group of raters lean, one segment per rating, and where their low, average, and high targets sit against the current value.

Preview

data

Strong buy7 analysts
  • 0Bearish
  • 1Neutral
  • 6Bullish
  1. $375.00Low
  2. $398.57Average
  3. $282.32Current
  4. $460.00High

Average $398.57, +41.2% from current $282.32. Low $375.00, +32.8%. High $460.00, +62.9%.

split only

Hold9 ratings
  • 3Bearish
  • 4Neutral
  • 2Bullish

many

Buy90 ratings
  • 12Bearish
  • 30Neutral
  • 48Bullish
  1. $80.00Low
  2. $104.50Average
  3. $96.20Current
  4. $140.00High

Average $104.50, +8.6% from current $96.20. Low $80.00, -16.8%. High $140.00, +45.5%.

down

Sell13 ratings
  • 9Bearish
  • 3Neutral
  • 1Bullish
  1. $18.00Low
  2. $22.40Average
  3. $31.80Current
  4. $27.00High

Average $22.40, -29.6% from current $31.80. Low $18.00, -43.4%. High $27.00, -15.1%.

one

1 analyst
  • 0Bearish
  • 0Neutral
  • 1Bullish

loading

Loading

empty

No ratings yet. They show here once an analyst covers it.

No ratings yet. They show here once an analyst covers it.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/consensus-meter

Use it for

  • Showing how analysts or reviewers lean, and where their price or score targets sit against today's value.
  • Pass counts for the negative, neutral, and positive sides. Pass targets to add the track of low, average, and high.
  • Pass verdict for the overall call. It turns green or red when most ratings lean one way.
  • Change counts or targets, as from tabs, and the counts roll, the segments grow, shrink, and change color in place, the marks glide, and each label glides to its new column.
  • Pass noun, like { one: "analyst", other: "analysts" }, for the count.

Avoid it for

  • A rating out of five stars or a single score. Use Progress or a plain number.
  • More than three sides. The split has negative, neutral, and positive only.
  • Values over time. Use LineChart.

Props

  • counts

    How many ratings fall on each side. Up to 40 draw one segment each. More draw one segment per side, sized by its share. Counts below 0 count as 0, and parts round to whole ratings.

    { negative: number; neutral: number; positive: number }

  • targets

    The targets and the current value. The track always fits the current value, even outside the targets. A low above the high is read the other way round.

    { low: number; average: number; high: number; current: number }

  • verdict

    The overall call, shown in a badge. Green when 60% or more lean positive, red when 60% or more lean negative. Its words morph when they change, and long words are cut short.

    ReactNode

  • aria-label

    Names the meter. TypeScript requires aria-label or aria-labelledby.

    string

  • valueFormat

    Formats the targets.

    Intl.NumberFormatOptions · default US dollars, with cents on every target when any has them, and short form like $2.35M from $100,000 up

  • locale

    The locale for numbers. Fixed, so server and browser print the same text.

    string · default "en-US"

  • labels

    The words for each side and each target.

    { negative?, neutral?, positive?, low?, average?, current?, high?: string } · default Bearish, Neutral, Bullish, Low, Average, Current, High

  • noun

    What one rating is called, for the count.

    { one: string; other: string } · default { one: "rating", other: "ratings" }

  • loading

    Shows placeholders at the same size.

    boolean · default false

  • emptyText

    Shown when every count is 0.

    ReactNode · default "No ratings yet"

  • className

    Extra classes. Later classes win.

    string

Accessibility

  • Renders a <figure> named by aria-label or aria-labelledby. TypeScript requires one.
  • Every count and every target is printed as text, so nothing lives only in color. The segments and the track are hidden from screen readers, and a hidden line reads each target and how far it sits from the current value, which sighted people see on hover or tap.
  • Each side's color sits beside its word: Bearish, Neutral, Bullish. The targets differ by shape: plain dots for the ends, rings for the average and the current value. The average and the line to it take the color of the lean, up or down. With forced colors, the bar, track, and marks take system colors, so they stay visible.
  • Hovering a side lifts its segments and fades the others. Hovering a target grows its mark, fades the others, and swaps its word for how far it sits from the current value. On touch, a tap does the same, and a tap anywhere but on a part lets go. Each side has a strip 26px tall over the thin bar, and each mark a 24px target.
  • Once on screen, the bar fills from the start and each target glides out from the current value. When the data changes, counts roll, segments resize, and marks and labels glide. With motion off, it all shows at once. Right to left, the labels and the track both run from the right.
  • Loading and the empty text are read out through a status line that stays on the page.

Examples

Ratings and targets

<ConsensusMeter
  aria-label="Analyst consensus"
  counts={{ negative: 0, neutral: 1, positive: 6 }}
  targets={{ low: 375, average: 398.57, high: 460, current: 282.32 }}
  verdict="Strong buy"
  noun={{ one: "analyst", other: "analysts" }}
/>

The split alone

<ConsensusMeter aria-label="Reviews" counts={{ negative: 3, neutral: 4, positive: 2 }} verdict="Mixed" />

While it loads

<ConsensusMeter aria-label="Analyst consensus" counts={data?.counts ?? { negative: 0, neutral: 0, positive: 0 }} targets={data?.targets} loading={isLoading} />