primitives
Outline Expand Input
A labeled text input whose focus ring visually expands outward on focus — the outline-offset grows from 3 px to 5 px via a smooth CSS transition, giving the impression that the ring springs away from the field boundary. At rest the fill uses a muted token tint; on focus it brightens to the base background, drawing the eye to the active field. The design is pure CSS (no JS, no hooks) so the component ships as a server component. The ring color maps to --primary at rest and switches to --ring on keyboard :focus-visible, satisfying the contract's accessibility requirement. A disabled state dims opacity and neutralizes the outline to --border.
Ported from cohencoo (MIT)
Install
1. Register the namespace (once per project):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/outline-expand-inputUsage
<OutlineExpandInput />Props
Standard props — see source.
Source
import * as React from "react";
import styles from "./outline-expand-input.module.css";
export type OutlineExpandInputProps = {
/** Visible label text rendered above the input. */
label: string;
/** Current value for controlled usage. */
value?: string;
/** Change handler for controlled usage. */
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
/** Placeholder text shown when the field is empty. */
placeholder?: string;
/** Disables the input when true. */
disabled?: boolean;
/** Additional class names merged onto the root wrapper. */
className?: string;
/** Forwarded to the underlying <input> element. */
id?: string;
/** Input name attribute. */
name?: string;
} & Omit<
React.InputHTMLAttributes<HTMLInputElement>,
"value" | "onChange" | "disabled" | "placeholder" | "id" | "name"
>;
export const OutlineExpandInput = React.forwardRef<
HTMLInputElement,
OutlineExpandInputProps
>(function OutlineExpandInput(
{
label,
value,
onChange,
placeholder = "Write here...",
disabled = false,
className = "",
id,
name,
...rest
},
ref,
) {
// Stable id: use the consumer's id if provided, otherwise derive from label.
const inputId =
id ??
`outline-expand-input-${label.toLowerCase().replace(/\s+/g, "-")}`;
return (
<div className={[styles.wrapper, className].filter(Boolean).join(" ")}>
<label className={styles.label} htmlFor={inputId}>
{label}
</label>
<input
ref={ref}
id={inputId}
name={name}
type="text"
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
className={styles.input}
{...rest}
/>
</div>
);
});
OutlineExpandInput.displayName = "OutlineExpandInput";
Dependencies
- @bottega/tokens