Skip to content

Well

A 70% Well Gray sub-region inside a card, used instead of a nested card.

Status
Beta
Level
Atom
Category
Layout
Adoption
Not used yet
import { Well } from "@oration/canon/components/well";
packages/canon/src/components/well.tsx

Interruptions

How many words a supplier has to say before the agent stops talking.

Stop after

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

Inside a card, a sub-region is a Well Gray tint at 70% with a 10px radius, never a second bordered or shadowed card.

One level deep

A well sits directly in a card. Don't put a well inside a well; split the content with a hairline or spacing instead.

Anatomy#

Bank details

Chase, ending 4417

Verified Sep 12

  1. Card. The surface the well sits in: rounded-xl bg-card shadow-border with 16px padding. Not part of the component.
  2. Well. A <div> with bg-muted/70 and 10px corners. No border, no shadow.
  3. Padding. 12px by default. Use 16px (p-4) for fields, px-3 py-2.5 for a single line.
  4. 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.

One line: px-3 py-2
Default: p-3, for previews, excerpts and short lists.
Fields: 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#

States
StateTreatment
RestThe only state. The well is a static region with no hover, focus or pressed styles.
Dark themeWell Gray swaps with the theme, so the 70% tint stays a step off the card in both.
RevealedWhen a well holds dependent fields, show it only while the controlling setting is on. It appears in place, no animation needed.

Behavior#

  • Well is a styled <div>. Every prop spreads onto it, and className is merged after rounded-[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-0 so long values truncate instead of stretching the grid.

Do and don't#

Bank details

Chase, ending 4417

Verified Sep 12

Do. Split the inside of a card with a 70% tint well at a 10px radius.

Bank details

Chase, ending 4417

Verified Sep 12

Don't. Put a bordered or shadowed card inside a card. Two edges stack, and the inner box reads as a separate object.

Payment terms

TermsNet 30
Early-pay discount2% in 10 days
Do. Keep wells one level deep, directly inside the card.

Payment terms

TermsNet 30
Early-pay discount2% in 10 days
Don't. Nest a well inside a well. The tints add up to a darker gray that reads as disabled.

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> with aria-labelledby inside 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#

Design tokens
TokenUsed for
--muted at 70%Fill (Well Gray tint)
rounded-[10px]Standard corners, equal to --radius-lg
p-312px padding

API reference#

Well

A 70% Well Gray sub-region inside a card.

Other props spread onto <div>.

Props of Well
PropTypeDefaultDescription
classNamestringNo defaultMerged after rounded-[10px] bg-muted/70 p-3. Use it for padding and layout.
childrenReact.ReactNodeNo defaultThe 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.