Fine-tune card
NewA small inspector in which an agent adjusts a design’s layout, size, radius, opacity and type, and the reader can take over.
fine-tune-card/fine-tune-card.tsxLayout
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- An agent adjusting a design’s properties where the reader can watch each value change and take over at any moment.
- A handful of numbers and choices on one selected thing: layout, size, corner radius, opacity, a type.
- Precise tweaks by feel: dragging a field’s name scrubs its number, arrow keys step it, typing sets it.
- Showing at a glance what was changed: edited fields keep a blue tint and ring, and the header turns to Edited.
When not to use
- A full inspector with dozens of properties in groups. Use a panel with collapsible sections.
- Values without a sensible range or step, such as names or free text. Use a form.
- An agent that must ask before it changes anything. Use an approval card that waits for an answer.
- Settings that apply to the whole app rather than one selected object. Use a settings page.
You can add this component with the shadcn CLI or by hand:
Into components/fine-tune-card of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/fine-tune-card.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
import { FineTuneCard, type FineTuneValues } from './fine-tune-card';export function CardInspector({ onEdit }: { onEdit: (values: FineTuneValues) => void }) { return ( <FineTuneCard title="Hero banner" defaultValues={{ layout: 'row', width: 640, height: 240, radius: 16, opacity: 100 }} typeOptions={['Promo', 'Editorial', 'Product']} onChange={onEdit} autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
An inspector card for one design. While the agent works, its Adjust label shimmers; it picks a layout, counts each number to a new value and chooses a type from a menu, and the header turns to a green Edited. Every control works by hand too: drag a field’s name to scrub it, use the arrow keys or type the number, and each field you change keeps a blue tint so the edits read at a glance.
- Layout is a radio group drawn as a segmented control: one raised thumb, a third of the well wide, slides under the choice by a transform (300 ms on a soft ease-out) while the icons cross from grey to blue in 200 ms. The arrow keys move the choice.
- Each number is a field whose name is its handle, a slider in its own right. Dragging the name sideways scrubs the value at half a unit per pixel, with the pointer captured so the drag can leave the card; the arrows step by one, Shift by ten, Home and End go to the ends.
- The number itself is a text field that keeps only digits while typing, and commits on Enter or on leaving, clamped to the field’s range; Escape puts the value back.
- A field once changed keeps an accent tint and a 1 px accent ring, crossing in 200 ms, so the card shows what has been edited. The first change turns the header from the shimmering Adjust to a green Edited that pops in (250 ms, from 95 %).
- The shimmer is a gradient, accent to a lighter accent and back, twice the width of the words and clipped to them, running right to left every 1.4 s.
- Type is a listbox that opens upwards from its button, popping from its bottom right corner in 200 ms while the chevron turns over. A soft highlight glides to the row under the pointer or the focus by its top and height (220 ms), and fades when the pointer leaves. It closes on a pick, Escape or a press outside.
- While on screen and untouched, a demo plays the agent: it picks the grid layout, counts width, height, radius and opacity to new values one after another on an ease-in-out curve, the name of the field it writes lit in blue, opens the menu, walks the highlight down and picks a type, then marks the card Edited, waits three seconds and starts over. Its edits never reach onChange; any key or press hands the card to the reader.
- Every colour is a light-dark() token on the root, so the card follows the page’s color-scheme or the colorScheme prop. Shadows are written once with all their layers, those a scheme does not use being transparent: long soft lifts by day, short dark ones at night.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | 'Flavor card' | The name of what is being inspected, in the header. |
defaultValues | Partial<FineTuneValues> | { layout: 'row', width: 324, height: 96, radius: 28, opacity: 100, type: null } | Where the card starts, and where the demo returns to: layout (row, column or grid), width, height, radius, opacity and type. |
defaultState | FineTuneState | — | Opens the card mid-way: edited (the header reads the edited label), touched (the fields drawn as changed), busy (the field the agent is writing), open (the Type menu) and highlight (its lit row). Setting it turns the demo off. |
onChange | (values: FineTuneValues) => void | — | Called with every value after each change the reader makes. |
typeOptions | string[] | ['Seasonal', 'Classic', 'Limited'] | The choices in the Type menu. |
typePlaceholder | string | 'Select type' | The Type button’s words before a choice. |
demoValues | Partial<FineTuneValues> | { layout: 'grid', width: 360, height: 120, radius: 16, opacity: 90, type: 'Limited' } | What the demo agent sets. |
agentLabel | string | 'Adjust' | The header’s shimmering words while the agent works. |
editedLabel | string | 'Edited' | The header’s words once something has changed. |
autoplay | boolean | true | While on screen and untouched, a demo agent adjusts the card by itself and starts over. |
colorScheme | 'light' | 'dark' | 'system' | — | Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent,} from 'react';import { useAutoplay } from './use-autoplay';import styles from './fine-tune-card.module.css';/* * Fine-tune card: a small inspector in which an agent adjusts a design's properties (its layout, * size, corner radius, opacity and type), and the reader can take over at any time. * * - The header says what is going on: "Adjust", its words shimmering, while the agent works; a * green "Edited" that pops in once something has changed. * - Layout is a three-way segmented control; the raised thumb slides under the choice by a * transform (300 ms, a soft ease-out) while the icons cross their colours./* Fine-tune card (fine-tune-card.tsx): a soft grey stage with a 240 px inspector card in its middle, or at night a graphite stage with the card a step lighter. Colours are the tokens below, light-dark(light, dark): the component takes the color-scheme it inherits from the page (light unless the page says otherwise), or the one its colorScheme prop sets (data-color-scheme). The shadows are one list for both schemes, each layer coloured for one of them and clear in the other. Motion is transforms, opacity and colours: the layout thumb slides, the fields cross their tint, the menu pops up from its corner and its highlight glides by top and height. */.root { /* The stage, the card, the fields and the select's well. */ --canvas: oklch(96.1% 0.002 247.84); --surface: oklch(100% 0 0); --field: oklch(96.1% 0.001 286.375); --inset: oklch(97.9% 0.002 247.839); /* Words: strong, the handles under the pointer, and quiet. */ --ink: oklch(24.7% 0.006 258.361); --ink-2: oklch(50.6% 0.01 264.477); --ink-3: oklch(69.5% 0.009 264.505); --line: oklch(94.6% 0.003 264.542); /* The agent's blue: icons, rings, the tint of a changed field, its shimmer's light. */ --accent: oklch(62.6% 0.205 254.947);'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver( ([entry]) => { onScreen = entry.isIntersecting;- The card is a section named by its heading. Layout is a radiogroup of three radio buttons named Row, Column and Grid, with one tab stop and the arrow keys to move.
- Each field’s name is a slider (aria-valuenow, min and max; Opacity also says its percent) that the arrows, Shift, Home and End move; its number is a text field named “W value” and so on, with the up and down arrows stepping too.
- Type is a button with aria-haspopup="listbox" and aria-expanded; its listbox of options with aria-selected opens on a press or the arrow keys, takes the focus to the chosen row, moves with the arrows, Home and End, and closes on Escape, giving the focus back.
- The first change the reader makes is announced politely (“Flavor card: edited”); the demo’s edits are not. Focus rings are visible on every control, the select ringing in the accent.
- Under reduced motion the shimmer stands still, the thumb, tints, menu and highlight change at once, and the demo never plays.