Well
A 70% Well Gray sub-region inside a card, used instead of a nested card.
Interruptions
How many words a supplier has to say before the agent stops talking.
While the agent is reading a remittance
- “Sorry, which invoice?” (3 words)Agent stops
- “Wait” (1 word)Keeps talking
- “Can you send that remittance again” (6 words)Agent stops
import { SegmentedControl } from "@oration/canon/components/segmented-control";import { StatusLabel } from "@oration/canon/components/status-dot";import { Well } from "@oration/canon/components/well";import * as React from "react";export function Hero() { const [threshold, setThreshold] = React.useState<"1" | "2" | "3">("2"); const words = Number(threshold); const phrases = [ "Sorry, which invoice?", "Wait", "Can you send that remittance again", ]; return ( <div className="flex w-full max-w-md flex-col gap-4 rounded-xl bg-card p-4 shadow-border"> <div className="flex flex-col gap-1"> <p className="text-sm font-medium text-foreground"> Interruptions </p> <p className="text-13 text-muted-foreground"> How many words a supplier has to say before the agent stops talking. </p> </div> <div className="flex items-center justify-between gap-3"> <span className="text-13 text-foreground">Stop after</span> <SegmentedControl label="Stop after" value={threshold} onValueChange={setThreshold} options={[ { value: "1", label: "1 word" }, { value: "2", label: "2 words" }, { value: "3", label: "3 words" }, ]} /> </div> <Well> <p className="mb-2 text-13 font-medium text-foreground"> While the agent is reading a remittance </p> <ul className="flex flex-col gap-1.5"> {phrases.map((phrase) => { const count = phrase.split(" ").length; const stops = count >= words; return ( <li key={phrase} className="flex items-center justify-between gap-4 text-13" > <span className="text-foreground"> “{phrase}”{" "} <span className="text-muted-foreground tabular-nums"> ({count}{" "} {count === 1 ? "word" : "words"}) </span> </span> <StatusLabel tone={stops ? "warning" : "neutral"} > {stops ? "Agent stops" : "Keeps talking"} </StatusLabel> </li> ); })} </ul> </Well> </div> );}Usage#
Well is a tint well: a sub-region inside a card, filled with Well Gray at 70%, with 10px corners and 12px of padding. It groups a preview, a few figures or a set of dependent fields without drawing a second card. The mistake it exists to prevent is the nested card, a bordered or shadowed box inside another, which stacks edges and flattens the hierarchy. A well has no border, no shadow and no interaction of its own.
When to use
- For a live preview under a setting: sample phrases an agent would stop for, a remittance email as the supplier will see it.
- For a few figures inside a sheet or record panel: open, overdue and paid this month.
- For dependent fields that appear when a setting is on, such as the approver and threshold under Require a second approval.
- For a quoted excerpt inside a card: a transcript line, a supplier's email, a note from the last call.
When not to use
- For a surface on the page plane. A well only exists inside a card. Use Card
- For a message that tells the person something went wrong or needs attention. Use Alert
- For code or JSON, which has its own mono surface and copy action. Use Code block
- For a selectable option. A well isn't interactive. Use Choice card
- For a row in a list with an icon, title and action. Use Item
The Tint Well Rule
One level deep
Anatomy#
Bank details
Chase, ending 4417
Verified Sep 12
- Card. The surface the well sits in:
rounded-xl bg-card shadow-borderwith 16px padding. Not part of the component. - Well. A
<div>withbg-muted/70and 10px corners. No border, no shadow. - Padding. 12px by default. Use 16px (
p-4) for fields,px-3 py-2.5for a single line. - Content. Whatever the region holds, in the card's type: 13px for dense content, Slate Meta for secondary lines.
Examples#
Preview in a settings card
A well holds what the setting produces, here the remittance email a supplier receives, labelled as a preview.
Remittance email
Sent to each supplier when a payment goes out.
As Northwind Freight will see it
Payment of $48,210.50 sent
Cedarline paid INV-20931 by ACH on Friday, Oct 2. Funds usually arrive in one to two business days.
import { Well } from "@oration/canon/components/well";export function InASettingsCard() { return ( <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 shadow-border"> <div className="flex flex-col gap-1"> <p className="text-sm font-medium text-foreground"> Remittance email </p> <p className="text-13 text-muted-foreground"> Sent to each supplier when a payment goes out. </p> </div> <Well className="flex flex-col gap-1 text-13"> <p className="text-xs text-muted-foreground"> As Northwind Freight will see it </p> <p className="font-medium text-foreground"> Payment of $48,210.50 sent </p> <p className="text-muted-foreground"> Cedarline paid INV-20931 by ACH on Friday, Oct 2. Funds usually arrive in one to two business days. </p> </Well> </div> );}Figures
A row of wells for a few figures inside a record panel: a 12px label, a tabular value and a detail line. min-w-0 lets long values truncate.
Northwind Freight
- Open
- $184,250
- 48 invoices
- Overdue
- $12,780
- 3 invoices
- Paid this month
- $642,300
- 164 invoices
import { Well } from "@oration/canon/components/well";export function Figures() { const figures = [ { label: "Open", value: "$184,250", detail: "48 invoices" }, { label: "Overdue", value: "$12,780", detail: "3 invoices" }, { label: "Paid this month", value: "$642,300", detail: "164 invoices" }, ]; return ( <div className="flex w-full max-w-lg flex-col gap-3 rounded-xl bg-card p-4 shadow-border"> <p className="text-sm font-medium text-foreground"> Northwind Freight </p> <dl className="grid grid-cols-1 gap-2 sm:grid-cols-3"> {figures.map((figure) => ( <Well key={figure.label} className="flex min-w-0 flex-col gap-0.5 px-3 py-2.5" > <dt className="text-xs text-muted-foreground"> {figure.label} </dt> <dd className="truncate text-base font-semibold text-foreground tabular-nums"> {figure.value} </dd> <dd className="text-xs text-muted-foreground tabular-nums"> {figure.detail} </dd> </Well> ))} </dl> </div> );}Dependent fields
Fields that only apply while a setting is on sit in a well under it, indented to the setting's label, with 16px padding.
import { Checkbox } from "@oration/canon/components/checkbox";import { Input } from "@oration/canon/components/input";import { Label } from "@oration/canon/components/label";import { toast } from "@oration/canon/components/toast";import { Well } from "@oration/canon/components/well";import * as React from "react";export function DependentFields() { const [on, setOn] = React.useState(true); const approverId = React.useId(); const thresholdId = React.useId(); return ( <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 shadow-border"> <label className="flex items-start gap-3"> <Checkbox checked={on} onCheckedChange={(checked) => { setOn(checked); toast.add({ title: checked ? "Second approval required" : "Second approval turned off", }); }} className="mt-0.5" /> <span className="flex flex-col gap-0.5"> <span className="text-sm font-medium text-foreground"> Require a second approval </span> <span className="text-13 text-muted-foreground"> For invoices over a threshold, before they join a payment run. </span> </span> </label> {on ? ( <Well className="ml-7 flex flex-col gap-4 p-4"> <div className="flex flex-col gap-2"> <Label htmlFor={thresholdId}>Threshold</Label> <Input id={thresholdId} defaultValue="$25,000" inputMode="decimal" /> </div> <div className="flex flex-col gap-2"> <Label htmlFor={approverId}>Second approver</Label> <Input id={approverId} defaultValue="Maya Okafor" /> </div> </Well> ) : null} </div> );}Quoted excerpt
A line from a call or an email, with who said it underneath in Slate Meta.
Last call
Sep 25, 4:03 PM
“We switched banks in August. The remittance for INV-20877 went to the old account, can you resend it to the new one?”
Tomás Ferreira, Halcyon Logistics
import { Well } from "@oration/canon/components/well";export function Excerpt() { return ( <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 shadow-border"> <div className="flex items-baseline justify-between gap-3"> <p className="text-sm font-medium text-foreground">Last call</p> <p className="text-xs text-muted-foreground tabular-nums"> Sep 25, 4:03 PM </p> </div> <Well className="text-13"> <blockquote className="text-foreground"> “We switched banks in August. The remittance for INV-20877 went to the old account, can you resend it to the new one?” </blockquote> <p className="mt-2 text-xs text-muted-foreground"> Tomás Ferreira, Halcyon Logistics </p> </Well> </div> );}Padding
12px by default. Tighten to px-3 py-2 for a single line, open to p-4 when the well holds fields.
px-3 py-2p-3, for previews, excerpts and short lists.p-4, so inputs have room around their focus ring.import { Well } from "@oration/canon/components/well";export function Padding() { return ( <div className="flex w-full max-w-md flex-col gap-3 rounded-xl bg-card p-4 shadow-border"> <Well className="px-3 py-2 text-13 text-foreground"> One line: <code className="font-mono text-xs">px-3 py-2</code> </Well> <Well className="text-13 text-foreground"> Default: <code className="font-mono text-xs">p-3</code>, for previews, excerpts and short lists. </Well> <Well className="p-4 text-13 text-foreground"> Fields: <code className="font-mono text-xs">p-4</code>, so inputs have room around their focus ring. </Well> </div> );}States#
| State | Treatment |
|---|---|
| Rest | The only state. The well is a static region with no hover, focus or pressed styles. |
| Dark theme | Well Gray swaps with the theme, so the 70% tint stays a step off the card in both. |
| Revealed | When a well holds dependent fields, show it only while the controlling setting is on. It appears in place, no animation needed. |
Behavior#
Wellis a styled<div>. Every prop spreads onto it, andclassNameis merged afterrounded-[10px] bg-muted/70 p-3, so padding and layout can be overridden.- It sets no text color or size. Content inherits from the card.
- Controls inside a well keep their own surfaces: inputs stay white with their hairline, so they stand out against the tint.
- It works on any card width. In a grid of figures, give each well
min-w-0so long values truncate instead of stretching the grid.
Do and don't#
Bank details
Chase, ending 4417
Verified Sep 12
Bank details
Chase, ending 4417
Verified Sep 12
Payment terms
Payment terms
Content#
- Label a preview so it's clear it is one: Preview, As Northwind Freight will see it.
- Figures in a well follow the stat pattern: a 12px Slate label, a tabular value, an optional detail line.
- Quoted text keeps its source nearby: who said it and when, in Slate Meta under the quote.
Accessibility#
- The well is a
<div>with no role; it groups visually only. If the region needs a name for screen readers, use a<section>witharia-labelledbyinside it, or put a heading in it. - The tint is decoration. Don't rely on it alone to show that fields are dependent; the setting that reveals them says so.
- Slate Meta and Graphite Ink both keep their contrast on the 70% tint in both themes.
- Revealed fields appear after the setting that controls them in reading order, so focus moves into them naturally.
Design tokens#
| Token | Used for |
|---|---|
--muted at 70% | Fill (Well Gray tint) |
rounded-[10px] | Standard corners, equal to --radius-lg |
p-3 | 12px padding |
API reference#
Well
A 70% Well Gray sub-region inside a card.
Other props spread onto <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged after rounded-[10px] bg-muted/70 p-3. Use it for padding and layout. |
children | React.ReactNode | No default | The region's content. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
Only the agent configuration screens import Well. About 140 files in apps/web write the bg-muted/70 recipe by hand, with a mix of corners and padding.
Two local components are also called Well: a figure well in contact-center/charts.tsx (label, value, detail) and a 12px text well in the procedure editor nodes. They look close to this one but don't share it.
Corners are written as rounded-[10px] rather than rounded-lg, so they won't follow a change to --radius.