Skip to content
AI Interfaces block

Recommendation with Confidence Meter

A suggestion scored with a native meter rather than a styled bar, banded in words as well as numbers, and showing what the model rejected — the fastest way a human catches a bad recommendation.

258 lineslucide-reactAdded 13 Aug 2026Updated 10 Sept 2026
  • recommendation
  • confidence
  • meter
  • suggestion
  • trust

What's included

  • components/confidence-recommendation.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add confidence-recommendation

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Suggested action

Reorder 1,400 units of the winter blend now

Demand is tracking 22% above last October and your supplier holds a volume break at 1,200 units. Ordering today lands stock four days before the forecast stockout.

Moderate confidence71%
71%

Based on 3 seasons of sales history. The forecast has not seen a promotion like the one running this month, which is why this is not higher.

Considered and rejected

  • Wait for the October close — Safer, but the reorder lead time is 3 weeks — you would stock out first
  • Order half now, half in November — Loses the volume break; unit cost rises about 9%

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/confidence-recommendation.tsx
/**
 * <ConfidenceRecommendation> — a suggestion the agent is offering, with an
 * honest confidence reading and the alternatives it rejected.
 *
 * Confidence meters are usually theatre. This one is built so it can carry
 * real information:
 *
 *  - The number is expressed as a `<meter>`, the element that exists for
 *    "a measurement within a known range". It exposes value, min and max to
 *    assistive tech natively, and `low`/`high`/`optimum` let the browser
 *    class the reading — which is genuinely the semantic here, unlike
 *    `<progress>`, which means "how far through a task".
 *  - The percentage is never the only signal. A band label ("moderate") and
 *    a plain-English caveat sit beside it, because 71% means nothing without
 *    knowing 71% of what.
 *  - The rejected alternatives are shown, not hidden behind a toggle. What
 *    the agent *didn't* pick, and why, is the fastest way for a human to
 *    catch a bad recommendation, and burying it is how automation bias sets
 *    in.
 *  - Accept is not styled as the only path. Dismiss and Edit are the same
 *    visual weight, so the card reads as a proposal rather than a prompt to
 *    click the blue thing.
 */

'use client'

import * as React from 'react'
import { Check, ChevronRight, Info, Lightbulb, PenLine, X } from 'lucide-react'

export interface RecommendationAlternative {
  id: string
  label: string
  why: string
}

export interface ConfidenceRecommendationProps {
  eyebrow?: string
  headline?: string
  body?: string
  /** 0–100. */
  confidence?: number
  /** What the score is based on, in plain language. */
  basis?: string
  alternatives?: RecommendationAlternative[]
  className?: string
}

const DEFAULT_ALTERNATIVES: RecommendationAlternative[] = [
  {
    id: 'wait',
    label: 'Wait for the October close',
    why: 'Safer, but the reorder lead time is 3 weeks — you would stock out first',
  },
  {
    id: 'half',
    label: 'Order half now, half in November',
    why: 'Loses the volume break; unit cost rises about 9%',
  },
]

/**
 * `<meter>` restyled without losing its semantics.
 *
 * Left alone a meter renders in the platform's own olive-and-yellow, which
 * is jarring in any themed UI and is why most teams give up and use a div
 * with no semantics at all. Keeping the element is worth this much CSS: the
 * browser still classifies the value against `low`/`high`/`optimum` and
 * decides *which* pseudo-element applies, so the colour goes on meaning what
 * it meant. Only the paint changes.
 *
 * Why a `<style>` block rather than Tailwind utilities — this cost an hour,
 * so it is written down:
 *
 *  - Chromium gates `::-webkit-meter-*` on the *prefixed*
 *    `-webkit-appearance: none`. Tailwind's `appearance-none` emits only the
 *    unprefixed property, so every rule is dropped silently.
 *  - Worse, an arbitrary variant like `[&::-webkit-meter-bar]:bg-muted`
 *    cannot work at all. Tailwind nests it as `&::-webkit-meter-bar`, which
 *    desugars to `:is(.cls)::-webkit-meter-bar`, and Chromium rejects a UA
 *    shadow pseudo-element sitting behind `:is()`. The selector has to be
 *    flat, which no utility class can produce.
 *
 * Firefox exposes one `::-moz-meter-bar` and colours it by the same
 * classification, so it needs no per-band rule.
 */
const METER_CSS = `
  .hl-meter { -webkit-appearance: none; appearance: none; background: none; }
  .hl-meter::-webkit-meter-bar {
    background: var(--muted); border: none; border-radius: 9999px;
  }
  .hl-meter::-webkit-meter-optimum-value {
    background: var(--color-emerald-500, #10b981); border-radius: 9999px;
  }
  .hl-meter::-webkit-meter-suboptimum-value {
    background: var(--color-amber-500, #f59e0b); border-radius: 9999px;
  }
  .hl-meter::-webkit-meter-even-less-good-value {
    background: var(--color-rose-500, #f43f5e); border-radius: 9999px;
  }
  .hl-meter::-moz-meter-bar { border-radius: 9999px; }
`

/** Bands, so the number is never the only thing said about it. */
function band(value: number) {
  if (value >= 85) return { label: 'High confidence', tone: 'text-emerald-600 dark:text-emerald-400' }
  if (value >= 60) return { label: 'Moderate confidence', tone: 'text-amber-600 dark:text-amber-400' }
  return { label: 'Low confidence', tone: 'text-rose-600 dark:text-rose-400' }
}

export function ConfidenceRecommendation({
  eyebrow = 'Suggested action',
  headline = 'Reorder 1,400 units of the winter blend now',
  body = 'Demand is tracking 22% above last October and your supplier holds a volume break at 1,200 units. Ordering today lands stock four days before the forecast stockout.',
  confidence = 71,
  basis = 'Based on 3 seasons of sales history. The forecast has not seen a promotion like the one running this month, which is why this is not higher.',
  alternatives = DEFAULT_ALTERNATIVES,
  className = '',
}: ConfidenceRecommendationProps) {
  const [decision, setDecision] = React.useState<'accepted' | 'dismissed' | null>(null)
  const { label, tone } = band(confidence)

  return (
    <section
      className={`mx-auto w-full max-w-xl overflow-hidden rounded-2xl border border-border/60 bg-card ${className}`}
    >
      <style>{METER_CSS}</style>

      <div className="space-y-4 px-5 py-4">
        <div className="flex items-start gap-3">
          <span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
            <Lightbulb aria-hidden className="h-4 w-4" />
          </span>

          <div className="min-w-0">
            <p className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
              {eyebrow}
            </p>
            {headline ? (
              <h3 data-stress-ignore className="mt-0.5 text-base font-semibold leading-snug">{headline}</h3>
            ) : null}
          </div>
        </div>

        <p className="text-sm leading-relaxed text-muted-foreground">{body}</p>

        {/* -- Confidence ------------------------------------------------ */}
        <div className="rounded-xl border border-border/60 bg-muted/40 px-4 py-3">
          <div className="flex items-baseline justify-between gap-3">
            <span className={`text-sm font-semibold ${tone}`}>{label}</span>
            <span className="font-mono text-sm tabular-nums text-muted-foreground">
              {confidence}%
            </span>
          </div>

          {/*
            `<meter>`, not a styled div. The value, bounds and the fact that
            this is a measurement are all exposed without a line of ARIA —
            and the bands below let a browser render it as good/average/poor
            on its own.
          */}
          <meter
            value={confidence}
            min={0}
            max={100}
            low={60}
            high={85}
            optimum={100}
            aria-label="Model confidence"
            className="hl-meter mt-2 h-1.5 w-full"
          >
            {confidence}%
          </meter>

          <p className="mt-2.5 flex gap-2 text-xs leading-relaxed text-muted-foreground">
            <Info aria-hidden className="mt-0.5 h-3.5 w-3.5 shrink-0" />
            {basis}
          </p>
        </div>

        {/* -- What it rejected ----------------------------------------- */}
        {alternatives.length > 0 ? (
          <div>
            <h4
              data-stress-ignore
              className="mb-2 text-xs font-bold uppercase tracking-wider text-muted-foreground"
            >
              Considered and rejected
            </h4>
            <ul className="space-y-1.5">
              {alternatives.map((alt) => (
                <li key={alt.id} className="flex gap-2 text-xs leading-relaxed">
                  <ChevronRight
                    aria-hidden
                    className="mt-0.5 h-3 w-3 shrink-0 text-muted-foreground rtl:rotate-180"
                  />
                  <span>
                    <span className="font-medium">{alt.label}</span>
                    <span className="text-muted-foreground"> — {alt.why}</span>
                  </span>
                </li>
              ))}
            </ul>
          </div>
        ) : null}
      </div>

      {/* -- Actions, all three at equal weight ------------------------- */}
      <div className="flex flex-wrap items-center gap-2 border-t border-border/60 bg-muted/30 px-5 py-3">
        {decision ? (
          <p role="status" className="flex items-center gap-2 text-sm">
            {decision === 'accepted' ? (
              <>
                <Check aria-hidden className="h-4 w-4 text-emerald-500" />
                Order queued for your approval in Purchasing.
              </>
            ) : (
              <>
                <X aria-hidden className="h-4 w-4 text-muted-foreground" />
                <span className="text-muted-foreground">
                  Dismissed. I will not raise this again this week.
                </span>
              </>
            )}
          </p>
        ) : (
          <>
            <button
              type="button"
              onClick={() => setDecision('dismissed')}
              className="inline-flex items-center gap-1.5 rounded-xl border border-border/60 px-3.5 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              <X aria-hidden className="h-4 w-4" />
              Dismiss
            </button>

            <button
              type="button"
              className="inline-flex items-center gap-1.5 rounded-xl border border-border/60 px-3.5 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              <PenLine aria-hidden className="h-4 w-4" />
              Edit quantity
            </button>

            <button
              type="button"
              onClick={() => setDecision('accepted')}
              className="ms-auto inline-flex items-center gap-1.5 rounded-xl border border-primary/40 bg-primary/10 px-3.5 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              <Check aria-hidden className="h-4 w-4" />
              Accept
            </button>
          </>
        )}
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/confidence-recommendation.tsx and import it where you need the section:

import { ConfidenceRecommendation } from '@/components/confidence-recommendation'

Customize

3 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
eyebrowstring'Suggested action'
headlinestring'Reorder 1,400 units of the winter blend now'
bodystring—
confidence0–100.number71
basisWhat the score is based on, in plain language.string—
alternativesRecommendationAlternative[]DEFAULT_ALTERNATIVES
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

confidence-recommendation.html
<!--
  Recommendation with Confidence Meter — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="mx-auto w-full max-w-xl overflow-hidden rounded-2xl border border-border/60 bg-card ">
  <style>.hl-meter { -webkit-appearance: none; appearance: none; background: none; }
  .hl-meter::-webkit-meter-bar {
    background: var(--muted); border: none; border-radius: 9999px;
  }
  .hl-meter::-webkit-meter-optimum-value {
    background: var(--color-emerald-500, #10b981); border-radius: 9999px;
  }
  .hl-meter::-webkit-meter-suboptimum-value {
    background: var(--color-amber-500, #f59e0b); border-radius: 9999px;
  }
  .hl-meter::-webkit-meter-even-less-good-value {
    background: var(--color-rose-500, #f43f5e); border-radius: 9999px;
  }
  .hl-meter::-moz-meter-bar { border-radius: 9999px; }</style>
  <div class="space-y-4 px-5 py-4">
    <div class="flex items-start gap-3">
      <span class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb h-4 w-4" aria-hidden="true">
          <path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path>
          <path d="M9 18h6"></path>
          <path d="M10 22h4"></path>
        </svg>
      </span>
      <div class="min-w-0">
        <p class="text-xs font-bold uppercase tracking-wider text-muted-foreground">Suggested action</p>
        <h3 data-stress-ignore="true" class="mt-0.5 text-base font-semibold leading-snug">Reorder 1,400 units of the winter blend now</h3>
      </div>
    </div>
    <p class="text-sm leading-relaxed text-muted-foreground">Demand is tracking 22% above last October and your supplier holds a volume break at 1,200 units. Ordering today lands stock four days before the forecast stockout.</p>
    <div class="rounded-xl border border-border/60 bg-muted/40 px-4 py-3">
      <div class="flex items-baseline justify-between gap-3">
        <span class="text-sm font-semibold text-amber-600 dark:text-amber-400">Moderate confidence</span>
        <span class="font-mono text-sm tabular-nums text-muted-foreground">71%</span>
      </div>
      <meter value="71" min="0" max="100" low="60" high="85" optimum="100" aria-label="Model confidence" class="hl-meter mt-2 h-1.5 w-full">71%</meter>
      <p class="mt-2.5 flex gap-2 text-xs leading-relaxed text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>Based on 3 seasons of sales history. The forecast has not seen a promotion like the one running this month, which is why this is not higher.</p>
    </div>
    <div>
      <h4 data-stress-ignore="true" class="mb-2 text-xs font-bold uppercase tracking-wider text-muted-foreground">Considered and rejected</h4>
      <ul class="space-y-1.5">
        <li class="flex gap-2 text-xs leading-relaxed">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right mt-0.5 h-3 w-3 shrink-0 text-muted-foreground rtl:rotate-180" aria-hidden="true">
            <path d="m9 18 6-6-6-6"></path>
          </svg>
          <span>
            <span class="font-medium">Wait for the October close</span>
            <span class="text-muted-foreground">— Safer, but the reorder lead time is 3 weeks — you would stock out first</span>
          </span>
        </li>
        <li class="flex gap-2 text-xs leading-relaxed">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right mt-0.5 h-3 w-3 shrink-0 text-muted-foreground rtl:rotate-180" aria-hidden="true">
            <path d="m9 18 6-6-6-6"></path>
          </svg>
          <span>
            <span class="font-medium">Order half now, half in November</span>
            <span class="text-muted-foreground">— Loses the volume break; unit cost rises about 9%</span>
          </span>
        </li>
      </ul>
    </div>
  </div>
  <div class="flex flex-wrap items-center gap-2 border-t border-border/60 bg-muted/30 px-5 py-3">
    <button type="button" class="inline-flex items-center gap-1.5 rounded-xl border border-border/60 px-3.5 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x h-4 w-4" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>Dismiss</button>
    <button type="button" class="inline-flex items-center gap-1.5 rounded-xl border border-border/60 px-3.5 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pen-line h-4 w-4" aria-hidden="true"><path d="M12 20h9"></path><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"></path></svg>Edit quantity</button>
    <button type="button" class="ms-auto inline-flex items-center gap-1.5 rounded-xl border border-primary/40 bg-primary/10 px-3.5 py-2 text-sm font-semibold text-primary transition-colors hover:bg-primary/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check h-4 w-4" aria-hidden="true"><path d="M20 6 9 17l-5-5"></path></svg>Accept</button>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

7 more blocks in Human in the Loop

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Human in the Loop

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Human in the Loop blocks

View category
Open the full page for this block

Approval Request with Blast Radius

The agent stops and asks: the effect stated before the verb, choices as radios in a fieldset so picking is separate from committing, no pre-focused Approve, and the decision left on screen afterwards.

Human in the Loop247 lines1 dep
Open the full page for this block

Proposed Edits Diff Table

Machine-proposed row edits accepted one at a time, with before and after in real del/ins elements, a tri-state select-all that actually sets indeterminate, and a running count of what will be written.

Human in the Loop269 lines1 dep
Open the full page for this block

Agent Permission Scopes

Read and write scopes separated rather than bundled, writes off by default, a required expiry, and switches that are real checkboxes with role="switch" — so the grant is toggleable by keyboard and announced as on or off.

Human in the Loop297 lines1 dep
Open the full page for this block

Bulk Approval Queue

Forty decisions and an honest way to clear them: bulk approve that cannot reach the high-impact rows, a select-all that says how many it takes, and the auto-approve deadline on every line.

Human in the Loop307 lines1 dep
Open the full page for this block

Approval Policy List

Which agent actions run unattended and which need a signature, stated as a policy the operator can read before anything happens.

Human in the Loop156 linesNo deps
Open the full page for this block

Escalation Queue

What is waiting on a person, ordered by how long it has been waiting rather than when it arrived.

Human in the Loop148 linesNo deps
Open the full page for this block

Human Oversight Split

Where a person sits in an automated run, written as four guarantees rather than as a promise that a human is involved.

Human in the Loop156 linesNo deps