bottega

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):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/outline-expand-input

Usage

Usagetsx
<OutlineExpandInput />

Props

Standard props — see source.


Source

outline-expand-input.tsxtsx
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