QuoteField

Enter a quote in the instrument's notation, step it by the convention's increment, and keep the numeric value in parent state.

import { useState } from "react"
import { NULL_TOKEN, formatQuote, type InstrumentConvention } from "@/lib/format"
import { QuoteField } from "@/components/ui/quote-field"

// Three instruments, three bases. The field learns everything from the convention.
const NOTE: InstrumentConvention = { price: { kind: "fraction", denominator: 32, half: "+" }, tick: 1 / 64 }
const BILL: InstrumentConvention = { price: { kind: "decimal", decimals: 3 }, tick: 0.0005, quoteBasis: "discount" }
const CREDIT: InstrumentConvention = { price: { kind: "decimal", decimals: 3 }, tick: 0.001, quoteBasis: "spread" }

function Quote({ title, convention, stepFrom, label }: { title: string; convention: InstrumentConvention; stepFrom: number; label?: string }) {
  const [value, setValue] = useState<number | null>(null)
  return (
    <div className="flex w-52 max-w-full flex-col gap-1">
      <span className="text-muted-foreground">{title}</span>
      <QuoteField convention={convention} value={value} onValueChange={setValue} stepFrom={stepFrom} label={label} />
      <span className="lining-nums tabular-nums text-muted-foreground">{value === null ? NULL_TOKEN : `${value} prints ${formatQuote(value, convention)}`}</span>
    </div>
  )
}

export default function QuoteFieldDemo() {
  return (
    <div className="w-fit max-w-full font-(family-name:--tradecn-font-mono) text-xs">
      <div className="flex flex-wrap justify-center gap-6">
        <Quote title="a note, in 32nds" convention={NOTE} stepFrom={99.5} />
        <Quote title="a bill, on discount" convention={BILL} stepFrom={4.25} />
        <Quote title="credit, on spread" convention={CREDIT} stepFrom={112.5} label="Spread (bp)" />
      </div>
    </div>
  )
}

Installation

pnpm dlx shadcn@latest add tradecn/ui/quote-field#v2.0.0
npx shadcn@latest add tradecn/ui/quote-field#v2.0.0
yarn dlx shadcn@latest add tradecn/ui/quote-field#v2.0.0
bunx --bun shadcn@latest add tradecn/ui/quote-field#v2.0.0

Usage

import { useState } from "react"
import { QuoteField } from "@/components/ui/quote-field"
import type { InstrumentConvention } from "@/lib/format"
const BILL: InstrumentConvention = { price: { kind: "decimal", decimals: 3 }, tick: 0.0005, quoteBasis: "discount" }

function BillQuote() {
  const [discount, setDiscount] = useState<number | null>(null)

  return <QuoteField convention={BILL} value={discount} onValueChange={setDiscount} stepFrom={4.25} />
}

The bill starts blank. Its first upward step uses 4.25 as the reference and produces 4.251; discount steps by 0.001, independently of the price tick. Replace the sample reference with your current market value.

The preview compares a note on price, a bill on discount, and credit on spread. Type 99.75 into the note and leave the field to see 99-24. Arrows move one step, Shift+arrows move ten, and the readout shows the value held by the parent.

API Reference

QuoteFieldProps exposes these inputs. It does not forward arbitrary input or wrapper props.

Prop Type Default Purpose
convention InstrumentConvention Required Quote basis, notation, and step
value number | null Required Parent-owned quote; null for blank or invalid text
onValueChange (value: number | null) => void Required Receives parsed edits and stepped values
stepFrom number | null null Starting value for a step when value is null
label string Basis label below Visible label and the {label} in the step buttons' names
error string Unset Parent validation message; takes precedence over the field's own error. Shown in an alert with the id ${id}-error, which the input's aria-describedby names, as the field's own message for unreadable text is
announceError boolean true false shows error without an alert and without tying it to the input, when you say it yourself: for an error whose words change on their own, such as a distance from a moving market, which would otherwise be read again at every change. The field's own message for unreadable text stays an alert and the input's description
invalidText string DEFAULT_INVALID_QUOTE Text shown after invalid input loses focus
placeholder string formatQuote(0, convention) Empty-input hint, such as 0-00 or 0.000
id string Generated with useId Input ID associated with the label
disabled boolean false Disables the input and both step buttons
shiftMultiplier number 10 Multiplier for Shift+Up and Shift+Down
side "bid" | "ask" Unset Root data-side for styling
inputRef RefObject<HTMLInputElement | null> Unset Ref to the input
labels Partial<QuoteFieldLabels> DEFAULT_QUOTE_FIELD_LABELS The step buttons' names, stepUp and stepDown, each with a {label} placeholder; a word given as undefined keeps its default
className string Unset Classes on the outer wrapper

The basis

convention.quoteBasis defaults to "price". Yield and discount values use percentage points; spreads use basis points, with no direction of their own — a CDS spread quotes bid below offer, cash credit the other way, and convention.quoteInverted declares which, for readers such as the RFQ ticket's crossed check. The root carries the basis as data-basis and the slot data-slot="tradecn-quote-field".

Basis Default label Default step Display precision
"price" Price convention.tick From convention.price
"yield" Yield 0.001 quoteDecimals, default 3
"discount" Discount 0.001 quoteDecimals, default 3
"spread" Spread 0.1 quoteDecimals, default 1

For non-price quotes, quoteStep overrides the default step. Price quotes ignore quoteStep and quoteDecimals. Set precision high enough to display the step; the bill above steps by 0.001 in discount, despite its price tick of 0.0005.

The field uses format's parseQuote, formatQuote, and stepQuote. Price input accepts the instrument's notation (99-16+) or a decimal. Decimal price conventions round to their decimal places, tick conventions snap to their price tick, and fraction conventions snap decimal input to their printable grid, so the value reported is the price the formatted text shows. Other bases accept decimals, snap to the quote step, and print without a unit suffix.

Parsing trims whitespace and accepts either minus sign; decimal input uses a point. A comma reads only as a thousands separator in the whole part, and, where the convention prints decimals, a number with one comma needs its point (4,253.000), since 4,253 could be a decimal comma: any other comma makes the text no price, so the field reports null rather than a bigger price. Deleting the first digit of 1,234.50 leaves ,234.50, which reads as no price until the comma goes too. Fraction prices use the mono font; other quotes use the numeric font. A new kind of instrument needs a new convention object, with no instrument-name lookup.

Typing and stepping

Each text edit calls onValueChange with the parsed number, or null for blank or invalid text. The input opens a phone's text keyboard, not its decimal pad, which has no minus or plus for a negative quote or 99-16+. Keep that value in parent state. Blur formats valid text (99.75 becomes 99-24 for 32nds) or marks invalid text with aria-invalid and a message below the field. Blur does not call onValueChange; it does not need to, since parsing already snapped the value to the printable grid — for a decimal convention that holds when its places can show the step.

The default message is DEFAULT_INVALID_QUOTE, Not a quote in this instrument's notation., the same for every label, so no label is lowercased or given an article. Typing clears the field's own error; blank text is not marked invalid. A supplied error remains until the parent clears it. error="" suppresses the field's message and invalid mark, as does invalidText="" when error is unset.

Control Behavior
Up / Down Add or subtract one quote step
Shift+Up / Shift+Down Add or subtract shiftMultiplier steps
Minus / plus buttons Subtract or add one step, including when Shift is held
Ctrl, Meta, or Alt + arrow Leave the event to listeners above the field

The buttons are named <label> down one step and <label> up one step by default, from labels. A step uses value ?? stepFrom, snaps to the nearest quote grid value, then moves by the requested steps. Choose a last price, mid, or available side for stepFrom; it applies whenever value is null, including invalid text. A non-finite value or stepFrom — a feed still warming up — counts as null on its own: a non-finite value leaves the field empty while a finite stepFrom still steps, and a non-finite stepFrom drops out while a finite value still shows and steps. Stepping waits only while neither supplies a finite start, and bare and Shift arrows still prevent their default behavior.

When value changes and differs from the parsed text, the field replaces the text and clears its own error. If the text already parses to the new value, it stays as typed until blur. Passing the same value again does not reset the text: setting an already-null value to null leaves invalid text in place. Changing only convention does not reformat the current text either; remount the field when you need to reset that local state.

There is no separate commit or cancel operation. Enter and Escape have no field-specific handler; a ticket can handle them above the field, just as its mod+up can reach the hotkey registry.

Two sides

Use two fields with the same convention and side="bid" or side="ask". The side only sets data-side; it does not change parsing or stepping. Pass a parent validation problem, such as a bid above the ask, through error.

What it does not do

The field checks notation only. Your ticket checks market prices, limits, and the other side before sending, and reports problems through error. The field registers no hotkeys, sends no orders or inquiries, and has no order state. ticket is built on it.

Built on shadcn's field and input-group.

This page is docs/quote-field.md at v2.0.0.