a value that falls back to a default: set here, inherited, or locked here
npx shadcn add @ag/settingAlready installed? Preview an update
npx shadcn add @ag/setting --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-12
- Changed: its words (the hover tip and what a screen reader hears) say what each state does over time, never "inherited", which read as a value saved here once: "follows lark: changes when lark does", "set here; lark says 16", and for a value set here that equals the one above, "set here, the same as lark: stays if lark changes", the one case that looks like following and is not. The × reads "follow lark again"; a clear is said as "workers: follows lark again, 16".
- Fixed: Delete or Backspace on the × (or an ⌥-click) clears and hands the focus back to the control, as a press on the × did; it fell to the page when the × went.
also called inherited value, override
shelf controls
comes with setting-context
uses button, key-router, tooltip
used by components
the demo above, verbatim
"use client"
import "@/registry/base-nova/ui/edge-fade"
import * as React from "react"
import { Sample } from "@/app/samples"
import { Button } from "@/registry/base-nova/ui/button"
import { Checkbox } from "@/registry/base-nova/ui/checkbox"
import { Input } from "@/registry/base-nova/ui/input"
import { MaterialProvider } from "@/registry/base-nova/ui/material"
import { NumberField } from "@/registry/base-nova/ui/number"
import {
NO_SCROLLBAR,
ScrollThumbs,
} from "@/registry/base-nova/ui/scroll-thumb"
import { Select } from "@/registry/base-nova/ui/select"
import { type Fallback, Setting } from "@/registry/base-nova/ui/setting"
import { Slider } from "@/registry/base-nova/ui/slider"
import { Switch } from "@/registry/base-nova/ui/switch"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/registry/base-nova/ui/toggle-group"
// #region layers
/** Three layers: the fleet holds every value, a model falls back to the fleet, a
* machine to its model. */
const LAYERS: readonly {
id: Layer
kind: "fleet" | "model" | "machine"
parent?: Layer
}[] = [
{ id: "fleet", kind: "fleet" },
{ id: "lark", kind: "model", parent: "fleet" },
{ id: "lark-01", kind: "machine", parent: "lark" },
{ id: "lark-02", kind: "machine", parent: "lark" },
{ id: "wren", kind: "model", parent: "fleet" },
{ id: "wren-01", kind: "machine", parent: "wren" },
]
type Layer = "fleet" | "lark" | "lark-01" | "lark-02" | "wren" | "wren-01"
interface Values {
alerts: boolean
backup: boolean
channel: string
level: string
workers: number
zone: string
fan: number
port: number
}
type Key = keyof Values
/** What each layer sets itself; anything absent is inherited. */
type Store = { [K in Key]: Partial<Record<Layer, Values[K]>> }
const START: Store = {
alerts: { fleet: true, wren: false, "lark-02": false },
backup: { fleet: true, "wren-01": false },
channel: { fleet: "stable", lark: "beta", "lark-01": "stable" },
level: { fleet: "normal", wren: "verbose" },
workers: { fleet: 8, lark: 16, "lark-02": 24 },
zone: { fleet: "eu-west", "wren-01": "eu-north" },
fan: { fleet: 0.4, wren: 0.7 },
port: { fleet: 8080, "lark-01": 8081 },
}
/** The value that applies on a layer, and the layer that set it. */
function resolve<K extends Key>(
store: Store,
key: K,
id: Layer,
): Fallback<Values[K]> {
const own = store[key][id]
if (own !== undefined) return { value: own, from: id }
const parent = LAYERS.find((l) => l.id === id)?.parent
if (!parent) throw new Error(`${key}: ${id} holds no value of its own`)
return resolve(store, key, parent)
}
const LEVELS = [
{ value: "quiet", label: "quiet" },
{ value: "normal", label: "normal" },
{ value: "verbose", label: "verbose" },
]
/** Each setting: its name, the control that sets it, how its value reads, and
* where it is locked. */
const ROWS: {
[K in Key]: {
name: string
format?: (value: Values[K]) => string
lockedOn?: "machine"
control: (s: {
value: Values[K]
pin: (next: Values[K]) => void
}) => React.ReactNode
}
} = {
alerts: {
name: "alerts",
control: (s) => (
<Switch
size="sm"
aria-label="alerts"
checked={s.value}
onCheckedChange={s.pin}
/>
),
},
backup: {
name: "nightly backup",
control: (s) => (
<Checkbox
aria-label="nightly backup"
checked={s.value}
onCheckedChange={s.pin}
/>
),
},
channel: {
name: "channel",
control: (s) => (
<ToggleGroup
size="sm"
aria-label="channel"
value={s.value}
onValueChange={s.pin}
>
<ToggleGroupItem value="stable">stable</ToggleGroupItem>
<ToggleGroupItem value="beta">beta</ToggleGroupItem>
</ToggleGroup>
),
},
level: {
name: "log level",
control: (s) => (
<Select
size="sm"
aria-label="log level"
options={LEVELS}
value={s.value}
onValueChange={(v) => v && s.pin(v)}
className="min-w-28"
/>
),
},
workers: {
name: "workers",
control: (s) => (
<NumberField
size="sm"
label="workers"
value={s.value}
onValueChange={s.pin}
min={1}
max={64}
whole
bar={false}
chevrons={false}
className="w-20"
/>
),
},
zone: {
name: "zone",
format: (v) => `“${v}”`,
control: (s) => (
<Input
size="sm"
aria-label="zone"
value={s.value}
onChange={(e) => s.pin(e.target.value)}
className="w-24"
/>
),
},
fan: {
name: "fan",
format: (v) => `${Math.round(v * 100)}%`,
control: (s) => (
<Slider
label="fan"
value={s.value}
onValueChange={s.pin}
format={(v) => `${Math.round(v * 100)}%`}
className="w-24"
/>
),
},
port: {
name: "port",
lockedOn: "machine",
control: (s) => (
<NumberField
size="sm"
label="port"
value={s.value}
onValueChange={s.pin}
min={1}
max={65535}
whole
bar={false}
chevrons={false}
format={String}
className="w-20"
/>
),
},
}
const KEYS = Object.keys(ROWS) as Key[]
/** A value as the captions say it: on and off for a switch, else as written. */
const words = (v: unknown) =>
typeof v === "boolean" ? (v ? "on" : "off") : String(v)
/** One cell: the layer's own value, or what applies from above, and where from. */
function Cell<K extends Key>({
k,
layer,
store,
set,
}: {
k: K
layer: (typeof LAYERS)[number]
store: Store
set: (k: K, id: Layer, v: Values[K] | undefined) => void
}) {
const row = ROWS[k]
const own = store[k][layer.id]
const above = layer.parent ? resolve(store, k, layer.parent) : undefined
return (
<div className="flex flex-col items-start gap-1">
<Setting
label={`${row.name} on ${layer.id}`}
value={own}
fallback={above}
onValueChange={(v) => set(k, layer.id, v)}
locked={row.lockedOn === layer.kind ? "provisioning" : undefined}
format={row.format}
>
{row.control}
</Setting>
{/* What the cell does when the layer above changes: follows it, or keeps its
own value; set here, what the layer above says (the same, which looks
like following and is not, or its own). Two lines whatever they say, and
never wider than the control (`w-0 min-w-full`): a changing caption moved
the grid. */}
<span className="flex w-0 min-w-full flex-col font-mono text-[10px] text-current/45">
<span>{own === undefined ? `follows ${above?.from}` : "set here"}</span>
<span className="min-h-[1lh] truncate">
{own === undefined || !above
? ""
: Object.is(own, above.value)
? `same as ${above.from}`
: `${above.from} says ${(row.format ?? words)(above.value)}`}
</span>
</span>
</div>
)
}
/** Every setting on every layer. Change the fleet or a model and what inherits
* follows; the port is set by provisioning on every machine. */
function ConfigGrid() {
const [store, setStore] = React.useState(START)
const scroller = React.useRef<HTMLDivElement>(null)
const set = <K extends Key>(k: K, id: Layer, v: Values[K] | undefined) =>
setStore((s) => ({ ...s, [k]: { ...s[k], [id]: v } }))
return (
<div className="space-y-3">
<div
ref={scroller}
className={`ag-edge-fade relative pb-2 ${NO_SCROLLBAR}`}
>
<ScrollThumbs scroller={scroller} />
<div className="grid w-max grid-cols-[6rem_repeat(6,max-content)] items-start gap-x-5 gap-y-3">
<span />
{LAYERS.map((l) => (
<span key={l.id} className="flex flex-col font-mono">
<span className="text-xs">{l.id}</span>
<span className="text-[10px] text-current/45">
{l.kind === "machine" ? `machine of ${l.parent}` : l.kind}
</span>
</span>
))}
{KEYS.map((k) => (
<React.Fragment key={k}>
<span className="pt-1 font-mono text-[11px] text-current/60">
{ROWS[k].name}
</span>
{LAYERS.map((l) => (
<Cell key={l.id} k={k} layer={l} store={store} set={set} />
))}
</React.Fragment>
))}
</div>
</div>
<Button variant="ghost" size="sm" onClick={() => setStore(START)}>
start over
</Button>
</div>
)
}
// #endregion
// #region machine
/** One machine's settings, in glass over a picture: the same rule, the glass's own
* ghost (thinner, its rim softer) where a value is inherited. */
function Machine() {
const [alerts, setAlerts] = React.useState<boolean | undefined>(undefined)
const [workers, setWorkers] = React.useState<number | undefined>(24)
const [level, setLevel] = React.useState<string | undefined>(undefined)
return (
<div className="flex flex-col items-start gap-3">
<Setting
label="alerts"
value={alerts}
fallback={{ value: true, from: "lark" }}
onValueChange={setAlerts}
>
{(s) => (
<Switch checked={s.value} onCheckedChange={s.pin}>
alerts
</Switch>
)}
</Setting>
<Setting
label="workers"
value={workers}
fallback={{ value: 16, from: "lark" }}
onValueChange={setWorkers}
>
{(s) => (
<NumberField
label="workers"
handle="workers"
value={s.value}
onValueChange={s.pin}
min={1}
max={64}
whole
bar={false}
chevrons={false}
/>
)}
</Setting>
<Setting
label="log level"
value={level}
fallback={{ value: "verbose", from: "wren" }}
onValueChange={setLevel}
>
{(s) => (
<Select
aria-label="log level"
options={LEVELS}
value={s.value}
onValueChange={(v) => v && s.pin(v)}
/>
)}
</Setting>
<Setting
label="port"
value={undefined}
fallback={{ value: 8080, from: "the fleet" }}
onValueChange={() => {}}
locked="provisioning"
>
{(s) => (
<NumberField
label="port"
handle="port"
value={s.value}
onValueChange={s.pin}
format={String}
bar={false}
chevrons={false}
/>
)}
</Setting>
</div>
)
}
// #endregion
export default function SettingDemo() {
const [threads, setThreads] = React.useState<number | undefined>(undefined)
return (
<div className="space-y-8">
<Sample
name="layers"
title="fleet, model, machine"
description="each cell is a setting that follows the layer above (a ghost: it changes when that does) or holds its own value (× to follow again; also ⌥-click, or Delete on the control; an emptied number or text field). a value set here equal to the one above still stays when that one changes, and says so. hover or focus a cell to read the same."
with="layers"
>
<ConfigGrid />
</Sample>
<Sample name="glass" title="in glass" with="machine">
<MaterialProvider material="glass" scheme="dark">
<div className="rounded-xl bg-[url(/lightbox/shore.jpg)] bg-cover bg-center p-6 text-white">
<Machine />
</div>
</MaterialProvider>
</Sample>
<Sample name="one" glance title="one setting">
<Setting
label="threads"
value={threads}
fallback={{ value: 8, from: "the model" }}
onValueChange={setThreads}
>
{(s) => (
<NumberField
label="threads"
handle="threads"
value={s.value}
onValueChange={s.pin}
min={1}
max={64}
whole
bar={false}
chevrons={false}
/>
)}
</Setting>
</Sample>
</div>
)
}the source, every file an install writes
"use client"
import { Lock, X } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/base-nova/ui/button"
import { ownedHere } from "@/registry/base-nova/ui/key-router"
import {
type Fallback,
SettingContext,
type SettingContextValue,
} from "@/registry/base-nova/ui/setting-context"
import { Tip } from "@/registry/base-nova/ui/tooltip"
export type { Fallback } from "@/registry/base-nova/ui/setting-context"
// A SETTING THAT FALLS BACK TO A DEFAULT: a machine's value over its model's over the
// fleet's, a project's over the workspace's. It is in one of three states:
//
// following `value` is undefined; `fallback.value` applies and changes when it
// does, said to come from `fallback.from` ("inherited" in the code,
// never in the words a person reads: it read as a value saved here)
// set here `value` holds it, and keeps it whatever the fallback does: a value
// set here equal to the fallback's is a real choice (it stays when
// the fallback moves) and is said as one ("the same as lark")
// locked here `locked` names who sets it; it is a readout (read-only, never
// disabled: it keeps its place in the Tab order and is read out)
//
// One rule for every value control. The control inside reads the setting
// (`useSetting()`, setting-context) and draws the value that applies, as a GHOST
// while it is inherited (a dashed edge, a hollow mark, the same colour at less
// strength; in glass the material's own ghost). Where the value comes from is named
// on hover and focus, and to a screen reader as the control's description.
//
// The slot after the control holds × while the value is set here (shown under the
// hand and on focus, reachable by Tab; focus comes back to the control after it),
// a padlock while it is locked, nothing while it is inherited. Clearing is also
// ⌥-click on the control, and Delete or Backspace on a control that does not edit
// text with them (`ownedHere` decides, as for every key in the registry); a number
// or a text field clears on an empty commit. A clear is said in a polite live
// region.
//
// <Setting label="workers" value={own} fallback={{ value: 16, from: "lark" }}
// onValueChange={setOwn}>
// {(s) => <NumberField label="workers" value={s.value} onValueChange={s.pin} />}
// </Setting>
export interface SettingProps<T> {
/** Its name, said when it is cleared ("workers: follows lark again, 16"). */
label: string
/** Set here; undefined, it is inherited. */
value: T | undefined
/** What applies when nothing is set here. Absent: the root of the chain, which
* holds every value itself. */
fallback?: Fallback<T>
/** A value sets it here; undefined clears it back to the fallback. */
onValueChange: (next: T | undefined) => void
/** Who sets it, when it cannot be changed here ("provisioning"). */
locked?: string
/** A value as a person reads it. Default: on and off for a yes or no, else as
* written. */
format?: (value: T) => string
className?: string
/** The control, given the value that applies and how to set it. */
children: (setting: { value: T; pin: (next: T) => void }) => React.ReactNode
}
const asWords = (v: unknown) =>
typeof v === "boolean" ? (v ? "on" : "off") : String(v)
/** The value that applies: set here, else the fallback's. A value set nowhere is a
* chain with no root, which screams. */
function applies<T>(
label: string,
value: T | undefined,
fallback: Fallback<T> | undefined,
): T {
if (value !== undefined) return value
if (fallback === undefined)
throw new Error(
`setting "${label}": nothing set here and nothing above it to inherit`,
)
return fallback.value
}
/** The control a clear hands focus back to: the one in its Tab stop (a choice's
* pressed option), else its first button or field. */
function focusIn(el: HTMLElement | null) {
const target =
el?.querySelector<HTMLElement>('[tabindex="0"]') ??
el?.querySelector<HTMLElement>(
'button, input:not([type=hidden]):not([aria-hidden="true"]), [role=slider]',
)
target?.focus()
}
export function Setting<T>({
label,
value,
fallback,
onValueChange,
locked,
format = asWords,
className,
children,
}: SettingProps<T>) {
const id = React.useId()
const says = `${id}-says`
const control = React.useRef<HTMLSpanElement>(null)
const [said, setSaid] = React.useState("")
const inherited = value === undefined
const effective = applies(label, value, fallback)
const readOnly = locked !== undefined
const clearable = !inherited && !readOnly && fallback !== undefined
// The × hides once it has cleared, so the focus it held goes back to the control
// after the render that shows the fallback: a field focused before it would take
// up the value it is leaving, and set it here again when it lost the focus.
const refocus = React.useRef(false)
React.useEffect(() => {
if (!refocus.current || !inherited) return
refocus.current = false
focusIn(control.current)
})
const clear = React.useCallback(() => {
if (!fallback) return
onValueChange(undefined)
setSaid(
`${label}: follows ${fallback.from} again, ${format(fallback.value)}`,
)
}, [fallback, onValueChange, label, format])
/** A clear asked for from `from`: one from outside the control (the × itself, by a
* press, a Delete or an ⌥-click) takes the × away with the focus it may hold, so the
* focus goes back to the control. */
const clearFrom = (from: EventTarget) => {
refocus.current = !control.current?.contains(from as Node)
clear()
}
const pin = (next: T) => {
if (readOnly) return
onValueChange(next)
if (inherited) setSaid(`${label}: set here, ${format(next)}`)
}
// Said as what each state does over time, never "inherited", which reads as a value
// saved here once (Adrian, 2026-10-12). Following, it changes when the fallback
// does; set here, it stays, and a value set here that equals the fallback's says
// so, since it looks the same as following and is not.
const words = readOnly
? `locked here, set by ${locked}`
: !fallback
? undefined
: inherited
? `follows ${fallback.from}: changes when ${fallback.from} does`
: Object.is(value, fallback.value)
? `set here, the same as ${fallback.from}: stays if ${fallback.from} changes`
: `set here; ${fallback.from} says ${format(fallback.value)}`
const described = words ? says : undefined
const context = React.useMemo<SettingContextValue>(
() => ({
inherited,
readOnly,
says: described,
clear: clearable ? clear : null,
fallback: fallback ?? null,
}),
[inherited, readOnly, described, clearable, clear, fallback],
)
const root = (
// One group: the control, its slot and the words about it.
<fieldset
data-slot="setting"
data-inherited={inherited ? "" : undefined}
data-locked={readOnly ? "" : undefined}
className={cn(
"group/setting inline-flex min-w-0 items-center gap-1",
className,
)}
onKeyDown={(e) => {
if (!clearable || (e.key !== "Delete" && e.key !== "Backspace")) return
if (ownedHere(e.nativeEvent, e.currentTarget)) return
e.preventDefault()
clearFrom(e.target)
}}
// ⌥-click clears what is set here, taken before the control sees the press,
// so a switch does not also flip and a select does not also open.
onPointerDownCapture={(e) => {
if (!clearable || !e.altKey) return
e.preventDefault()
e.stopPropagation()
}}
onClickCapture={(e) => {
if (!clearable || !e.altKey) return
e.preventDefault()
e.stopPropagation()
clearFrom(e.target)
}}
>
<span ref={control} data-slot="setting-control" className="contents">
<SettingContext.Provider value={context}>
{children({ value: effective, pin })}
</SettingContext.Provider>
</span>
{/* The slot: kept whatever it holds, so setting or clearing moves nothing. */}
<span
data-slot="setting-slot"
className="grid size-5 shrink-0 place-items-center"
>
{readOnly ? (
<Lock aria-hidden className="size-3 text-current/50" />
) : clearable ? (
<Button
variant="ghost"
size="icon-sm"
data-slot="setting-clear"
aria-label={`follow ${fallback.from} again, ${format(fallback.value)}`}
onClick={(e) => clearFrom(e.currentTarget)}
className="size-5 opacity-0 group-focus-within/setting:opacity-100 group-hover/setting:opacity-100 focus-visible:opacity-100 [&_svg]:size-3"
>
<X aria-hidden />
</Button>
) : null}
</span>
{words && (
<span id={says} className="sr-only">
{words}
</span>
)}
<span aria-live="polite" className="sr-only">
{said}
</span>
</fieldset>
)
// Where the value comes from, named under the hand and on focus. The root holds
// every value itself and has nothing to name.
return words ? (
<Tip content={words} delay={300}>
{root}
</Tip>
) : (
root
)
}