# Input URL Prefix Copy — React Input Component

> A URL field with a fixed prefix segment that strips pasted protocols, plus a copy button that morphs to a tick while a highlight sweeps the field.

- Category: [Inputs](https://pixelpapa.com/components/inputs.md)
- Tags: input, url, prefix, copy, clipboard
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/inputs/input-url-prefix-copy

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/input-url-prefix-copy.json"
```

The file is written to `components/pixelpapa/input-url-prefix-copy.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Check, Copy, Globe, Link2 } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useId, useState, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type UrlInputProps = {
  label: string;
  /** Fixed, non-editable start of the URL, e.g. "https://" or "halden.link/". */
  prefix?: string;
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
  /** Receives the full URL after it was written to the clipboard. */
  onCopy?: (url: string) => void;
  /** Turn input into a URL-safe slug (lowercase, dashes). */
  slug?: boolean;
  placeholder?: string;
  icon?: ReactNode;
  className?: string;
};

export function UrlInput({
  label,
  prefix = "https://",
  value,
  defaultValue = "",
  onChange,
  onCopy,
  slug = false,
  placeholder,
  icon,
  className,
}: UrlInputProps) {
  const [inner, setInner] = useState(defaultValue);
  const [copied, setCopied] = useState(0);
  const id = useId();
  const text = value ?? inner;
  const url = prefix + text;

  useEffect(() => {
    if (!copied) return;
    const t = setTimeout(() => setCopied(0), 1600);
    return () => clearTimeout(t);
  }, [copied]);

  function set(raw: string) {
    // Pasting a whole URL should not double the prefix.
    let next = raw.trim().replace(/^https?:\/\//i, "");
    if (next.startsWith(prefix)) next = next.slice(prefix.length);
    if (slug) next = next.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-/, "");
    if (value === undefined) setInner(next);
    onChange?.(next);
  }

  async function copy() {
    try {
      await navigator.clipboard?.writeText(url);
    } catch {
      // Clipboard can be blocked (iframes, permissions); the tick still confirms the intent.
    }
    setCopied((n) => n + 1);
    onCopy?.(url);
  }

  return (
    <div className={cn("w-full", className)}>
      <label htmlFor={id} className="text-sm font-medium text-foreground">
        {label}
      </label>
      <div className="relative mt-1.5 flex h-11 items-stretch overflow-hidden rounded-xl border border-input bg-background shadow-xs transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-4 focus-within:ring-ring/15">
        <AnimatePresence>
          {copied > 0 && (
            <motion.span
              key={copied}
              aria-hidden
              initial={{ x: "-100%", opacity: 0.9 }}
              animate={{ x: "100%", opacity: 0 }}
              exit={{ opacity: 0 }}
              transition={{ duration: 0.7, ease: "easeOut" }}
              className="pointer-events-none absolute inset-0 bg-linear-to-r from-transparent via-primary/25 to-transparent"
            />
          )}
        </AnimatePresence>
        <span className="flex items-center gap-2 border-r border-input bg-muted px-3 text-sm text-muted-foreground select-none">
          {icon && <span className="[&>svg]:size-4">{icon}</span>}
          {prefix}
        </span>
        <input
          id={id}
          value={text}
          placeholder={placeholder}
          inputMode="url"
          autoComplete="off"
          autoCapitalize="none"
          spellCheck={false}
          onChange={(e) => set(e.target.value)}
          className="relative min-w-0 flex-1 bg-transparent px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground/60"
        />
        <button
          type="button"
          onClick={copy}
          disabled={!text}
          aria-label={copied ? "Copied" : `Copy ${label.toLowerCase()}`}
          className="relative m-1 flex w-[5.25rem] shrink-0 items-center justify-center gap-1.5 overflow-hidden rounded-lg bg-secondary text-xs font-medium text-secondary-foreground transition-colors hover:bg-accent disabled:opacity-40"
        >
          <AnimatePresence mode="popLayout" initial={false}>
            <motion.span
              key={copied ? "done" : "copy"}
              initial={{ y: 14, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: -14, opacity: 0 }}
              transition={{ type: "spring", stiffness: 480, damping: 30 }}
              className="flex items-center gap-1.5"
            >
              {copied ? (
                <>
                  <Check className="size-3.5 text-emerald-500" strokeWidth={3} /> Copied
                </>
              ) : (
                <>
                  <Copy className="size-3.5" /> Copy
                </>
              )}
            </motion.span>
          </AnimatePresence>
        </button>
      </div>
      <span role="status" className="sr-only">
        {copied ? `${url} copied to clipboard` : ""}
      </span>
    </div>
  );
}

export default function InputUrlPrefixCopyDemo() {
  const [slug, setSlug] = useState("spring-launch-plan");

  return (
    <div className="w-full max-w-md space-y-5 rounded-2xl border border-border bg-card p-6 shadow-sm">
      <div>
        <h3 className="text-lg font-semibold tracking-tight text-card-foreground">Share this board</h3>
        <p className="mt-1 text-sm text-muted-foreground">Anyone with the link can view. Editing stays with your team.</p>
      </div>
      <UrlInput
        label="Public link"
        prefix="halden.link/"
        icon={<Link2 />}
        value={slug}
        onChange={setSlug}
        slug
        placeholder="your-board"
      />
      <UrlInput
        label="Company website"
        icon={<Globe />}
        defaultValue="halden.studio/work"
        placeholder="example.com"
      />
      <p className="text-xs text-muted-foreground">
        Paste a full address such as <span className="font-mono">https://halden.link/q2-roadmap</span> and the prefix is
        removed for you.
      </p>
    </div>
  );
}
```

## AI prompt

```text
Add the "Input URL Prefix Copy" component from PixelPapa UI to this project.

What it is: A URL field with a fixed prefix segment that strips pasted protocols, plus a copy button that morphs to a tick while a highlight sweeps the field.
Category: Inputs. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/input-url-prefix-copy.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/input-url-prefix-copy.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/input-url-prefix-copy" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More inputs

- [Input Credit Card](https://pixelpapa.com/components/inputs/input-credit-card.md) (Pro): A payment form with a live card preview: digits group in fours, a highlight glides to the part you are editing, and the card flips over for the security code.
- [Input Currency Format](https://pixelpapa.com/components/inputs/input-currency-format.md) (Pro): A large amount field that groups thousands as you type without losing the caret, pads cents on blur, and grows to hug its digits.
- [Input Floating Label](https://pixelpapa.com/components/inputs/input-floating-label.md): Text fields whose label rests as a placeholder, then springs up into a small caption on focus or when filled; inset and underline variants.
- [Input Inline Edit](https://pixelpapa.com/components/inputs/input-inline-edit.md): Text that turns into a field in place with no layout shift; Enter or blur saves with a brief tick, Escape puts the old value back.
- [Input Mention Autocomplete](https://pixelpapa.com/components/inputs/input-mention-autocomplete.md) (Pro): A comment box that opens a people list when you type @, filters as you go, inserts the handle with Enter or Tab and tints finished mentions inline.
- [Input Number Stepper](https://pixelpapa.com/components/inputs/input-number-stepper.md) (Pro): A spinbutton whose digits roll up or down like an odometer; hold a button to repeat with acceleration, or use the arrow keys.
