Bevel
Browse the docs

Text field

A text input cut into the panel as a slot, with a printed label above and description and error lines below. Focus lights the slot's edge amber, the colour of a caret; an invalid field edges in red and lights a lamp by its label. Inputs, textareas, search boxes with an icon and a key hint, amounts with a unit.

Workspace settings

Fields validate as you leave them and again on save; the server's answer comes back on the field it's about. Save as is: the URL is taken.

bevel.app/

Lowercase letters, numbers and dashes. Try saving as is.

Runs, spend and failures, every Monday.

Installation

Add with the CLI

  1. Once per project, add the Bevel registry to components.json.

    components.json
    {
      "registries": {
        "@bevel": "https://bevel-ui.vercel.app/r/{name}.json"
      }
    }
  2. Add Text field. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.

    Terminal
    npx shadcn@latest add @bevel/text-field
Install manually
  1. Install the dependencies.

    Terminal
    pnpm add @base-ui/react cn
  2. Add the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.

    styles/bevel.css
    /*
     * Bevel foundation. Every component builds on these tokens and utilities;
     * component stylesheets only add their own palettes and shapes.
     *
     * Layers:
     *   1. Light     one source, above and slightly in front of the screen.
     *   2. Depth     named elevations built from the light: recessed, flush,
     *                raised, pressed, object.
     *   3. Material  a surface recipe (leather, plastic, metal) that reads
     *                --bevel-material and --bevel-material-edge.
     *   4. Motion    two curves, a spring, and durations named by purpose.
     *   5. Shape     radii, stitching, and the type scale for amounts.
     */
    
    :root {
      /* 1. Light */
      --bevel-highlight: oklch(1 0 0 / 0.22);
      --bevel-highlight-strong: oklch(1 0 0 / 0.35);
      --bevel-shade: oklch(0 0 0 / 0.32);
      --bevel-shade-strong: oklch(0 0 0 / 0.5);
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
      /* Stamped into a surface: shaded on the upper inner edge, lit below. */
      --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12);
    
      /* 3. Material defaults; components and variants override these. */
      --bevel-material: oklch(0.47 0.085 52);
      --bevel-material-edge: oklch(0.3 0.06 44);
      --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */
      --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
      --bevel-grain-size: 180px;
    
      /* 2. Depth */
      --bevel-depth-recessed:
        inset 0 1px 2px var(--bevel-shade-strong),
        0 1px 0 var(--bevel-highlight);
      --bevel-depth-flush:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade);
      --bevel-depth-raised:
        inset 0 1px 0 var(--bevel-highlight),
        0 1px 0 var(--bevel-material-edge),
        0 2px 0 var(--bevel-material-edge),
        0 3px 5px var(--bevel-shade);
      --bevel-depth-pressed:
        inset 0 1px 2px var(--bevel-shade),
        0 1px 0 var(--bevel-highlight);
      /* A whole object resting on the page. */
      --bevel-depth-object:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade),
        0 0 0 1px var(--bevel-material-edge),
        0 1px 1px var(--bevel-cast),
        0 6px 12px -2px var(--bevel-cast),
        0 18px 32px -8px var(--bevel-cast);
      /* The shadow under a loose object, for filter: drop-shadow(). */
      --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast);
      /* A thin object (a card) lying on another surface. */
      --bevel-depth-sheet:
        inset 0 1px 0 var(--bevel-highlight-strong),
        inset 0 0 0 1px oklch(0 0 0 / 0.12),
        0 -1px 2px oklch(0 0 0 / 0.18);
    
      /* 4. Motion */
      --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
      /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */
      --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1);
      --bevel-duration-press: 100ms; /* a control giving under a finger */
      --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */
      --bevel-duration-move: 300ms; /* objects moving: cards, drawers */
      --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */
      --bevel-stagger: 30ms;
      --bevel-lift: 3px; /* how far an object rises on hover */
      --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */
      /* Unchosen items in a group sit back in shadow. */
      --bevel-dim: 0.82;
    
      /* 5. Shape */
      --bevel-radius-xs: 0.25rem;
      --bevel-radius-sm: 0.5rem;
      --bevel-radius-md: 0.625rem;
      --bevel-radius-lg: 1rem;
      --bevel-radius-xl: 1.5rem;
      --bevel-stitch-width: 1.5px;
      --bevel-stitch-inset: 0.375rem;
      /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */
      --bevel-amount: 2.25rem;
      --bevel-amount-minor: 1.25rem;
      --bevel-label: 0.75rem;
      --bevel-fine: 0.6875rem;
      --bevel-tracking-label: 0.08em; /* stamped labels */
      --bevel-tracking-print: 0.06em; /* printed marks on cards */
    
      /* Paper and ink */
      --bevel-paper: oklch(0.975 0.008 85);
      --bevel-paper-foreground: oklch(0.28 0.02 260);
      --bevel-paper-muted: oklch(0.48 0.015 260);
      --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45);
      --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45);
      --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E");
      /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */
      --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E");
      --bevel-ink-red: oklch(0.52 0.19 27);
      --bevel-ink-blue: oklch(0.44 0.14 262);
      --bevel-ink-green: oklch(0.5 0.12 155);
      --bevel-ink-black: oklch(0.3 0.01 260);
      --bevel-ink-amber: oklch(0.55 0.13 65);
    
      /* Brass and ivory: instrument hardware and dial faces */
      --bevel-brass: oklch(0.76 0.11 82);
      --bevel-brass-edge: oklch(0.52 0.08 70);
      --bevel-ivory: oklch(0.95 0.025 90);
      --bevel-ivory-foreground: oklch(0.25 0.015 60);
      --bevel-needle: oklch(0.55 0.2 27);
    
      /* Desk stationery: legal pads, pens, pencils, exhibit stickers */
      --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */
      --bevel-pad-foreground: oklch(0.27 0.03 255);
      --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45);
      --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55);
      --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */
      --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */
      --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */
      --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */
      --bevel-sticker-foreground: oklch(0.26 0.03 70);
      /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */
      --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif;
    }
    
    .dark {
      --bevel-highlight: oklch(1 0 0 / 0.14);
      /* Paper under dim light, not inverted: it's still paper. */
      --bevel-paper: oklch(0.88 0.008 85);
      --bevel-ivory: oklch(0.86 0.022 90);
      --bevel-pad: oklch(0.86 0.07 98);
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /* Depth */
    @utility bevel-recessed {
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    @utility bevel-flush {
      box-shadow: var(--bevel-depth-flush);
    }
    @utility bevel-raised {
      background-color: color-mix(in oklch, var(--bevel-material), white 7%);
      background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%);
      box-shadow: var(--bevel-depth-raised);
    }
    @utility bevel-pressed {
      background-color: color-mix(in oklch, var(--bevel-material), black 4%);
      background-image: none;
      box-shadow: var(--bevel-depth-pressed);
    }
    @utility bevel-object {
      box-shadow: var(--bevel-depth-object);
    }
    
    /* Materials */
    @utility bevel-leather {
      background-color: var(--bevel-material);
      background-image:
        var(--bevel-grain),
        linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade));
      background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%;
      background-blend-mode: soft-light, normal;
    }
    @utility bevel-plastic {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%),
        linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45));
      box-shadow: var(--bevel-depth-sheet);
    }
    @utility bevel-metal {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%),
        repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px);
      box-shadow:
        inset 0 0 0 1px oklch(0 0 0 / 0.25),
        inset 0 1px 0 var(--bevel-highlight-strong);
    }
    
    /* Details */
    @utility bevel-deboss {
      text-shadow: var(--bevel-deboss);
    }
    @utility bevel-stitch {
      border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8));
      border-style: dashed;
      border-width: var(--bevel-stitch-width);
      filter: drop-shadow(0 1px 0 var(--bevel-shade));
    }
    /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */
    @utility bevel-fill {
      background-color: var(--bevel-fill);
      background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18));
      box-shadow: 0 0 6px -1px var(--bevel-fill);
    }
    /* A small indicator light. Colour from --bevel-led. */
    @utility bevel-led {
      background-color: var(--bevel-led);
      background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%);
      box-shadow:
        0 0 6px var(--bevel-led),
        inset 0 -1px 1px oklch(0 0 0 / 0.35),
        0 1px 0 var(--bevel-highlight);
    }
    
    /* Paper: a sheet resting on the surface, with fibre in it. */
    @utility bevel-paper {
      background-color: var(--bevel-paper);
      background-image: var(--bevel-fibre);
      background-size: 240px 240px;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
    /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */
    @utility bevel-ink {
      color: var(--bevel-ink, var(--bevel-ink-red));
      border-color: currentColor;
      mix-blend-mode: multiply;
      mask-image: var(--bevel-ink-texture);
      mask-size: 160px 160px;
    }
    /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */
    @utility bevel-glass {
      pointer-events: none;
      background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%);
    }
    
    /* Material families: one per screen. Set on the screen's container. */
    [data-bevel-family="leather"] {
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
    }
    [data-bevel-family="paper"] {
      --bevel-cast: oklch(0.3 0.01 250 / 0.16);
    }
    .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) {
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /*
     * Ruled paper: rules every --bevel-rule-height and a margin line at
     * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling
     * scrolls with the content.
     */
    @utility bevel-ruled {
      background-color: var(--bevel-sheet, var(--bevel-paper));
      background-image:
        linear-gradient(
          to right,
          transparent calc(var(--bevel-rule-margin) - 1px),
          var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px),
          transparent calc(var(--bevel-rule-margin) + 1px)
        ),
        repeating-linear-gradient(
          to bottom,
          transparent 0 calc(var(--bevel-rule-height) - 1px),
          var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height)
        ),
        var(--bevel-fibre);
      background-size: 100% 100%, 100% 100%, 240px 240px;
      background-attachment: local;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
  3. Copy text-field.tsx into components/ui.

    components/text-field.tsx
    "use client"
    
    import * as React from "react"
    import { Field } from "@base-ui/react/field"
    import { Input } from "@base-ui/react/input"
    import { cn } from "cn"
    
    type TextFieldProps = Field.Root.Props & {
      /** Row height: sm for dense forms and toolbars, md for everything else. */
      size?: "sm" | "md"
    }
    
    /**
     * A text field: a printed label, a slot cut into the panel, and the lines
     * under it. Focus lights the slot's edge; an invalid field lights a red lamp
     * by its label. Wraps Base UI Field, so label, description and error are
     * wired to the control for you.
     */
    function TextField({ className, size = "md", ...props }: TextFieldProps) {
      return (
        <Field.Root
          data-slot="text-field"
          data-size={size}
          className={cn("flex min-w-0 flex-col gap-1.5", className)}
          {...props}
        />
      )
    }
    
    /** The printed label. Carries the error lamp, lit while the field is invalid. */
    function TextFieldLabel({ className, children, ...props }: Field.Label.Props) {
      return (
        <Field.Label
          data-slot="text-field-label"
          className={cn(
            "flex w-fit items-center gap-1.5 text-(length:--bevel-label) font-semibold tracking-(--bevel-tracking-label) text-(--text-field-print) uppercase",
            className
          )}
          {...props}
        >
          {children}
          <span
            aria-hidden
            data-slot="text-field-lamp"
            className="bevel-led size-1.5 shrink-0 rounded-full opacity-0 transition-opacity duration-(--bevel-duration-small) ease-(--bevel-ease-out) in-data-invalid:opacity-100 motion-reduce:transition-none"
          />
        </Field.Label>
      )
    }
    
    // flex-1 only inside a slot: on a bare input in the field's column, its 0% basis beats the height.
    const control =
      "min-w-0 in-data-[slot=text-field-slot]:flex-1 text-sm text-foreground focus-visible:outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:text-muted-foreground"
    
    /**
     * The slot, when the field has adornments: an icon, a unit, a key hint.
     * Put TextFieldAdornment, TextFieldKbd and the control inside it; pressing
     * anywhere in the slot focuses the control.
     */
    function TextFieldSlot({ className, onPointerDown, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="text-field-slot"
          onPointerDown={(event) => {
            onPointerDown?.(event)
            const target = event.target as HTMLElement
            if (event.defaultPrevented || target.closest("input, textarea, button, a")) return
            const field = event.currentTarget.querySelector<HTMLElement>("input, textarea")
            if (!field || (field as HTMLInputElement).disabled) return
            event.preventDefault()
            field.focus()
          }}
          className={cn(
            "text-field-well flex min-h-(--text-field-height) w-full cursor-text items-center gap-2 rounded-(--bevel-radius-sm) px-3 in-data-disabled:cursor-not-allowed",
            className
          )}
          {...props}
        />
      )
    }
    
    /** The input. Alone it is its own slot; inside TextFieldSlot it fills the slot. */
    function TextFieldInput({ className, ...props }: Input.Props) {
      return (
        <Input
          data-slot="text-field-input"
          className={cn(
            control,
            "text-field-well h-(--text-field-height) rounded-(--bevel-radius-sm) px-3",
            "in-data-[slot=text-field-slot]:h-auto in-data-[slot=text-field-slot]:self-stretch in-data-[slot=text-field-slot]:rounded-none in-data-[slot=text-field-slot]:px-0",
            className
          )}
          {...props}
        />
      )
    }
    
    /** A multi-line input in the same slot. Grows with its rows; resize vertically. */
    function TextFieldTextarea({ className, ...props }: Omit<Field.Control.Props, "render"> & React.ComponentProps<"textarea">) {
      return (
        <Field.Control
          data-slot="text-field-textarea"
          render={<textarea />}
          className={cn(
            control,
            "text-field-well min-h-(--text-field-textarea-height) resize-y rounded-(--bevel-radius-sm) px-3 py-2 leading-relaxed",
            "in-data-[slot=text-field-slot]:rounded-none in-data-[slot=text-field-slot]:px-0",
            className
          )}
          {...(props as Field.Control.Props)}
        />
      )
    }
    
    /** Something printed in the slot beside the text: an icon, a unit, a prefix. Decorative unless you label it. */
    function TextFieldAdornment({ className, ...props }: React.ComponentProps<"span">) {
      return (
        <span
          data-slot="text-field-adornment"
          className={cn(
            "flex shrink-0 items-center text-sm text-(--text-field-print) select-none [&_svg]:size-4 [&_svg]:shrink-0",
            className
          )}
          {...props}
        />
      )
    }
    
    /** A small keycap in the slot: the shortcut that focuses the field. */
    function TextFieldKbd({ className, ...props }: React.ComponentProps<"kbd">) {
      return (
        <kbd
          data-slot="text-field-kbd"
          className={cn(
            "bevel-raised inline-flex h-5 shrink-0 items-center rounded-(--bevel-radius-xs) px-1.5 font-mono text-(length:--bevel-fine) leading-none text-(--text-field-key-print) select-none",
            className
          )}
          {...props}
        />
      )
    }
    
    /** A line under the slot saying what goes in it. Linked to the control with aria-describedby. */
    function TextFieldDescription({ className, ...props }: Field.Description.Props) {
      return (
        <Field.Description
          data-slot="text-field-description"
          className={cn("text-xs text-muted-foreground", className)}
          {...props}
        />
      )
    }
    
    /**
     * What's wrong, shown while the field is invalid and announced as it
     * appears. Pass match to show it for one validity state (valueMissing,
     * typeMismatch…), or match={true} to show it whenever it's rendered.
     */
    function TextFieldError({ className, ...props }: Field.Error.Props) {
      return (
        <Field.Error
          data-slot="text-field-error"
          role="alert"
          className={cn("text-xs font-medium text-(--text-field-error-print)", className)}
          {...props}
        />
      )
    }
    
    export {
      TextField,
      TextFieldLabel,
      TextFieldSlot,
      TextFieldInput,
      TextFieldTextarea,
      TextFieldAdornment,
      TextFieldKbd,
      TextFieldDescription,
      TextFieldError,
    }
    export type { TextFieldProps }
  4. Copy text-field.css into your styles.

    styles/text-field.css
    /*
     * Text field: a slot cut into the panel. The well is a shade darker than the
     * surface and shadowed along its upper inner edge by the one light. Focus
     * lights a thin amber edge, the colour of a caret; an invalid field edges in
     * red and lights the lamp by its label. Builds on bevel.css (recessed, led).
     */
    
    :root {
      --text-field-well: oklch(0.955 0.004 90);
      --text-field-shade: oklch(0 0 0 / 0.2); /* the inner shadow under the slot's top edge */
      --text-field-glow: oklch(0.76 0.15 68); /* amber caret */
      --text-field-glow-soft: oklch(0.76 0.15 68 / 0.35);
      --text-field-error: oklch(0.6 0.21 27);
      --text-field-error-soft: oklch(0.6 0.21 27 / 0.3);
      --text-field-error-print: oklch(0.5 0.19 27);
      --text-field-print: oklch(0.45 0.01 260); /* the printed label */
      --text-field-key: oklch(0.97 0.004 90);
      --text-field-key-edge: oklch(0.8 0.006 90);
      --text-field-key-print: oklch(0.4 0.01 260);
      --text-field-height: 2.25rem;
      --text-field-textarea-height: 5.5rem;
    }
    
    .dark {
      --text-field-well: oklch(0.17 0.005 260);
      --text-field-shade: oklch(0 0 0 / 0.7);
      --text-field-error-print: oklch(0.72 0.17 25);
      --text-field-print: oklch(0.72 0.01 260);
      --text-field-key: oklch(0.32 0.006 260);
      --text-field-key-edge: oklch(0.2 0.006 260);
      --text-field-key-print: oklch(0.86 0.005 260);
    }
    
    [data-slot="text-field"][data-size="sm"] {
      --text-field-height: 2rem;
      --text-field-textarea-height: 4.5rem;
    }
    
    /* The well: recessed, in the field's own colour and a softer shade. */
    @utility text-field-well {
      --bevel-material-recess: var(--text-field-well);
      --bevel-shade-strong: var(--text-field-shade);
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    
    /* A control inside a slot is part of the slot's well, not a well of its own. */
    [data-slot="text-field-slot"] :is([data-slot="text-field-input"], [data-slot="text-field-textarea"]) {
      background-color: transparent;
      box-shadow: none;
    }
    [data-slot="text-field-slot"]:has([data-slot="text-field-textarea"]) {
      align-items: flex-start;
    }
    
    /* Focus lights the slot's edge. */
    [data-slot="text-field-slot"]:focus-within,
    :is([data-slot="text-field-input"], [data-slot="text-field-textarea"]):not([data-slot="text-field-slot"] *):focus-visible {
      box-shadow:
        var(--bevel-depth-recessed),
        0 0 0 1px var(--text-field-glow),
        0 0 0 3px var(--text-field-glow-soft);
    }
    
    /* Invalid: a red edge, brighter while focused. */
    [data-slot="text-field"][data-invalid] :is([data-slot="text-field-slot"], [data-slot="text-field-input"], [data-slot="text-field-textarea"]):not([data-slot="text-field-slot"] *) {
      box-shadow:
        var(--bevel-depth-recessed),
        0 0 0 1px var(--text-field-error);
    }
    [data-slot="text-field"][data-invalid] :is([data-slot="text-field-slot"]:focus-within, :is([data-slot="text-field-input"], [data-slot="text-field-textarea"]):not([data-slot="text-field-slot"] *):focus-visible) {
      box-shadow:
        var(--bevel-depth-recessed),
        0 0 0 1px var(--text-field-error),
        0 0 0 3px var(--text-field-error-soft);
    }
    
    [data-slot="text-field-lamp"] {
      --bevel-led: var(--text-field-error);
    }
    [data-slot="text-field-kbd"] {
      --bevel-material: var(--text-field-key);
      --bevel-material-edge: var(--text-field-key-edge);
    }
    
    @media (forced-colors: active) {
      [data-slot="text-field-slot"],
      :is([data-slot="text-field-input"], [data-slot="text-field-textarea"]):not([data-slot="text-field-slot"] *) {
        border: 1px solid CanvasText;
      }
      [data-slot="text-field-slot"]:focus-within,
      :is([data-slot="text-field-input"], [data-slot="text-field-textarea"]):not([data-slot="text-field-slot"] *):focus-visible {
        outline: 2px solid Highlight;
      }
    }
  5. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./text-field.css";

Anatomy

Each part is its own component. Leave out the ones you don't need.

Anatomy
<TextField name="email">
  <TextFieldLabel />
  <TextFieldSlot>
    <TextFieldAdornment />
    <TextFieldInput />
    <TextFieldKbd />
  </TextFieldSlot>
  <TextFieldDescription />
  <TextFieldError />
</TextField>

Parts

PartWhat it does
TextFieldField.Rootsize sm or md. Everything Base UI Field takes: name, disabled, invalid, validate, validationMode. Wires the label, description and error to the control.
TextFieldLabelField.LabelThe printed label. Its lamp lights red while the field is invalid.
TextFieldSlotdivThe slot, when there are adornments. Pressing anywhere in it focuses the control. Optional: a bare TextFieldInput is its own slot.
TextFieldInputInputThe input. value, defaultValue, onValueChange, and every input attribute.
TextFieldTextareaField.Control (textarea)Several lines, resizable vertically.
TextFieldAdornmentspanAn icon, unit or prefix printed in the slot.
TextFieldKbdkbdA small keycap: the shortcut that focuses the field.
TextFieldDescriptionField.DescriptionWhat goes in it. Describes the control.
TextFieldErrorField.Error (alert)Shown while invalid and announced as it appears. match a validity state (valueMissing, typeMismatch…) or leave it off to show validate's message or the form's.