UI Components Easy
Input Group
Input fields with addon elements — icon prefix, button suffix, or text prepend like "$" — all with seamless shared borders.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #0a0a0a;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 480px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.demo-stack {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.input-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.75rem;
margin-bottom: 0.25rem;
}
.input-label:first-child {
margin-top: 0;
}
/* ── Input Group ── */
.input-group {
display: flex;
align-items: stretch;
border-radius: 0.625rem;
border: 1px solid #2a2a2a;
background: #141414;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
}
.input-group:focus-within {
border-color: #38bdf8;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.15);
}
.input-group--disabled {
opacity: 0.5;
pointer-events: none;
}
/* ── Input field ── */
.input-field {
flex: 1;
min-width: 0;
padding: 0.625rem 0.75rem;
background: transparent;
border: none;
color: #f2f6ff;
font-size: 0.875rem;
font-family: inherit;
outline: none;
}
.input-field::placeholder {
color: #4a4a4a;
}
.input-field::-webkit-inner-spin-button,
.input-field::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* ── Addon ── */
.input-addon {
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.75rem;
color: #4a4a4a;
font-size: 0.8125rem;
white-space: nowrap;
flex-shrink: 0;
border-right: 1px solid #2a2a2a;
}
.input-addon:last-child {
border-right: none;
border-left: 1px solid #2a2a2a;
}
.input-addon--text {
background: rgba(255, 255, 255, 0.02);
color: #64748b;
font-size: 0.8125rem;
font-weight: 500;
padding: 0 0.75rem;
user-select: none;
}
.input-addon--btn {
background: rgba(56, 189, 248, 0.1);
color: #38bdf8;
font-size: 0.8125rem;
font-weight: 600;
font-family: inherit;
padding: 0 1rem;
cursor: pointer;
border: none;
border-left: 1px solid #2a2a2a;
border-right: none;
transition: background 0.15s;
}
.input-addon--btn:hover {
background: rgba(56, 189, 248, 0.2);
}
.input-addon--btn:active {
background: rgba(56, 189, 248, 0.25);
}
.input-addon svg {
display: block;
}(function () {
document.querySelectorAll("[data-copy]").forEach(function (btn) {
btn.addEventListener("click", function () {
var group = btn.closest(".input-group");
if (!group) return;
var input = group.querySelector(".input-field");
if (!input) return;
navigator.clipboard.writeText(input.value).then(function () {
var original = btn.textContent;
btn.textContent = "Copied!";
setTimeout(function () {
btn.textContent = original;
}, 1500);
});
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Input Group</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Input Group</h1>
<p class="demo-sub">Input with addon elements: icons, text, and buttons.</p>
<div class="demo-stack">
<!-- Search with icon -->
<label class="input-label">Search</label>
<div class="input-group">
<span class="input-addon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5"/><path d="M11 11l3.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<input type="text" class="input-field" placeholder="Search resources..." />
</div>
<!-- URL with text prepend -->
<label class="input-label">Website</label>
<div class="input-group">
<span class="input-addon input-addon--text">https://</span>
<input type="text" class="input-field" placeholder="example.com" />
</div>
<!-- Price with currency and button -->
<label class="input-label">Price</label>
<div class="input-group">
<span class="input-addon input-addon--text">$</span>
<input type="number" class="input-field" placeholder="0.00" />
<button class="input-addon input-addon--btn" type="button">Apply</button>
</div>
<!-- Email with icon and domain -->
<label class="input-label">Email</label>
<div class="input-group">
<span class="input-addon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="3" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M1 5l7 4 7-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<input type="text" class="input-field" placeholder="username" />
<span class="input-addon input-addon--text">@company.com</span>
</div>
<!-- Copy with button -->
<label class="input-label">API Key</label>
<div class="input-group">
<span class="input-addon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2H4a2 2 0 00-2 2v8a2 2 0 002 2h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><rect x="6" y="1" width="8" height="10" rx="2" stroke="currentColor" stroke-width="1.5"/></svg>
</span>
<input type="text" class="input-field" value="sk_live_a1b2c3d4e5f6" readonly />
<button class="input-addon input-addon--btn" type="button" data-copy>Copy</button>
</div>
<!-- Disabled state -->
<label class="input-label">Disabled</label>
<div class="input-group input-group--disabled">
<span class="input-addon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M5 7V5a3 3 0 016 0v2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<input type="text" class="input-field" placeholder="Not editable" disabled />
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
interface InputGroupProps {
prepend?: React.ReactNode;
append?: React.ReactNode;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
label?: string;
disabled?: boolean;
}
export function InputGroup({
prepend,
append,
inputProps = {},
label,
disabled = false,
}: InputGroupProps) {
const [focused, setFocused] = useState(false);
const borderColor = focused ? "#38bdf8" : "#2a2a2a";
const shadow = focused ? "0 0 0 3px rgba(56,189,248,0.15)" : "none";
return (
<div style={{ opacity: disabled ? 0.5 : 1 }}>
{label && (
<label
style={{
display: "block",
fontSize: "0.75rem",
fontWeight: 600,
color: "#94a3b8",
textTransform: "uppercase",
letterSpacing: "0.05em",
marginBottom: "0.375rem",
}}
>
{label}
</label>
)}
<div
style={{
display: "flex",
alignItems: "stretch",
borderRadius: "0.625rem",
border: `1px solid ${borderColor}`,
background: "#141414",
transition: "border-color 0.15s, box-shadow 0.15s",
boxShadow: shadow,
overflow: "hidden",
}}
>
{prepend && (
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "0 0.75rem",
color: "#4a4a4a",
fontSize: "0.8125rem",
whiteSpace: "nowrap",
flexShrink: 0,
borderRight: "1px solid #2a2a2a",
background: "rgba(255,255,255,0.02)",
}}
>
{prepend}
</span>
)}
<input
{...inputProps}
disabled={disabled}
onFocus={(e) => {
setFocused(true);
inputProps.onFocus?.(e);
}}
onBlur={(e) => {
setFocused(false);
inputProps.onBlur?.(e);
}}
style={{
flex: 1,
minWidth: 0,
padding: "0.625rem 0.75rem",
background: "transparent",
border: "none",
color: "#f2f6ff",
fontSize: "0.875rem",
fontFamily: "inherit",
outline: "none",
...(inputProps.style ?? {}),
}}
/>
{append && (
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "0 0.75rem",
color: "#4a4a4a",
fontSize: "0.8125rem",
whiteSpace: "nowrap",
flexShrink: 0,
borderLeft: "1px solid #2a2a2a",
background: "rgba(255,255,255,0.02)",
}}
>
{append}
</span>
)}
</div>
</div>
);
}
/* Demo */
export default function InputGroupDemo() {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText("sk_live_a1b2c3d4e5f6").then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};
const SearchIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
<path d="M11 11l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
const MailIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="1" y="3" width="14" height="10" rx="2" stroke="currentColor" strokeWidth="1.5" />
<path
d="M1 5l7 4 7-4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
return (
<div
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#0a0a0a",
fontFamily: "Inter, system-ui, sans-serif",
color: "#f2f6ff",
padding: "2rem",
}}
>
<div
style={{
width: "100%",
maxWidth: 480,
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
<h1 style={{ fontSize: "1.5rem", fontWeight: 800, marginBottom: "0.375rem" }}>
Input Group
</h1>
<p style={{ color: "#475569", fontSize: "0.875rem", marginBottom: "1rem" }}>
Input with addon elements: icons, text, and buttons.
</p>
<InputGroup
label="Search"
prepend={<SearchIcon />}
inputProps={{ placeholder: "Search resources..." }}
/>
<InputGroup
label="Website"
prepend={<span style={{ color: "#64748b", fontWeight: 500 }}>https://</span>}
inputProps={{ placeholder: "example.com" }}
/>
<InputGroup
label="Price"
prepend={<span style={{ color: "#64748b", fontWeight: 500 }}>$</span>}
append={
<button
type="button"
style={{
background: "rgba(56,189,248,0.1)",
color: "#38bdf8",
border: "none",
padding: "0 0.75rem",
fontWeight: 600,
fontSize: "0.8125rem",
cursor: "pointer",
fontFamily: "inherit",
}}
>
Apply
</button>
}
inputProps={{ placeholder: "0.00", type: "number" }}
/>
<InputGroup
label="Email"
prepend={<MailIcon />}
append={<span style={{ color: "#64748b", fontWeight: 500 }}>@company.com</span>}
inputProps={{ placeholder: "username" }}
/>
<InputGroup
label="API Key"
append={
<button
type="button"
onClick={handleCopy}
style={{
background: "rgba(56,189,248,0.1)",
color: "#38bdf8",
border: "none",
padding: "0 0.75rem",
fontWeight: 600,
fontSize: "0.8125rem",
cursor: "pointer",
fontFamily: "inherit",
}}
>
{copied ? "Copied!" : "Copy"}
</button>
}
inputProps={{ value: "sk_live_a1b2c3d4e5f6", readOnly: true }}
/>
<InputGroup
label="Disabled"
prepend={<span style={{ color: "#64748b" }}>@</span>}
inputProps={{ placeholder: "Not editable" }}
disabled
/>
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const copied = ref(false);
const focusStates = ref([false, false, false, false, false, false]);
function handleCopy() {
navigator.clipboard.writeText("sk_live_a1b2c3d4e5f6").then(() => {
copied.value = true;
setTimeout(() => (copied.value = false), 1500);
});
}
function setFocus(index, val) {
focusStates.value[index] = val;
}
function borderColor(focused) {
return focused ? "#38bdf8" : "#2a2a2a";
}
function shadow(focused) {
return focused ? "0 0 0 3px rgba(56,189,248,0.15)" : "none";
}
</script>
<template>
<div class="input-group-demo">
<div class="inner">
<h1 class="title">Input Group</h1>
<p class="desc">Input with addon elements: icons, text, and buttons.</p>
<!-- Search -->
<div>
<label class="label">Search</label>
<div class="group" :style="{ borderColor: borderColor(focusStates[0]), boxShadow: shadow(focusStates[0]) }">
<span class="prepend">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</span>
<input class="input" placeholder="Search resources..." @focus="setFocus(0, true)" @blur="setFocus(0, false)" />
</div>
</div>
<!-- Website -->
<div>
<label class="label">Website</label>
<div class="group" :style="{ borderColor: borderColor(focusStates[1]), boxShadow: shadow(focusStates[1]) }">
<span class="prepend"><span class="text-addon">https://</span></span>
<input class="input" placeholder="example.com" @focus="setFocus(1, true)" @blur="setFocus(1, false)" />
</div>
</div>
<!-- Price -->
<div>
<label class="label">Price</label>
<div class="group" :style="{ borderColor: borderColor(focusStates[2]), boxShadow: shadow(focusStates[2]) }">
<span class="prepend"><span class="text-addon">$</span></span>
<input class="input" placeholder="0.00" type="number" @focus="setFocus(2, true)" @blur="setFocus(2, false)" />
<span class="append">
<button type="button" class="addon-btn">Apply</button>
</span>
</div>
</div>
<!-- Email -->
<div>
<label class="label">Email</label>
<div class="group" :style="{ borderColor: borderColor(focusStates[3]), boxShadow: shadow(focusStates[3]) }">
<span class="prepend">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="1" y="3" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.5" />
<path d="M1 5l7 4 7-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
<input class="input" placeholder="username" @focus="setFocus(3, true)" @blur="setFocus(3, false)" />
<span class="append"><span class="text-addon">@company.com</span></span>
</div>
</div>
<!-- API Key -->
<div>
<label class="label">API Key</label>
<div class="group" :style="{ borderColor: borderColor(focusStates[4]), boxShadow: shadow(focusStates[4]) }">
<input class="input" value="sk_live_a1b2c3d4e5f6" readonly @focus="setFocus(4, true)" @blur="setFocus(4, false)" />
<span class="append">
<button type="button" class="addon-btn" @click="handleCopy">
{{ copied ? 'Copied!' : 'Copy' }}
</button>
</span>
</div>
</div>
<!-- Disabled -->
<div style="opacity: 0.5;">
<label class="label">Disabled</label>
<div class="group" style="border-color: #2a2a2a;">
<span class="prepend"><span class="text-addon">@</span></span>
<input class="input" placeholder="Not editable" disabled />
</div>
</div>
</div>
</div>
</template>
<style scoped>
.input-group-demo {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
font-family: Inter, system-ui, sans-serif;
color: #f2f6ff;
padding: 2rem;
}
.inner {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.title {
font-size: 1.5rem;
font-weight: 800;
margin: 0 0 0.375rem;
}
.desc {
color: #475569;
font-size: 0.875rem;
margin: 0 0 1rem;
}
.label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.375rem;
}
.group {
display: flex;
align-items: stretch;
border-radius: 0.625rem;
border: 1px solid;
background: #141414;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
}
.prepend {
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.75rem;
color: #4a4a4a;
font-size: 0.8125rem;
white-space: nowrap;
flex-shrink: 0;
border-right: 1px solid #2a2a2a;
background: rgba(255,255,255,0.02);
}
.append {
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.75rem;
color: #4a4a4a;
font-size: 0.8125rem;
white-space: nowrap;
flex-shrink: 0;
border-left: 1px solid #2a2a2a;
background: rgba(255,255,255,0.02);
}
.input {
flex: 1;
min-width: 0;
padding: 0.625rem 0.75rem;
background: transparent;
border: none;
color: #f2f6ff;
font-size: 0.875rem;
font-family: inherit;
outline: none;
}
.text-addon {
color: #64748b;
font-weight: 500;
}
.addon-btn {
background: rgba(56,189,248,0.1);
color: #38bdf8;
border: none;
padding: 0 0.75rem;
font-weight: 600;
font-size: 0.8125rem;
cursor: pointer;
font-family: inherit;
}
</style><script>
let copied = false;
function handleCopy() {
navigator.clipboard.writeText("sk_live_a1b2c3d4e5f6").then(() => {
copied = true;
setTimeout(() => (copied = false), 1500);
});
}
let focusStates = [false, false, false, false, false, false];
function setFocus(index, val) {
focusStates[index] = val;
focusStates = focusStates;
}
function borderColor(focused) {
return focused ? "#38bdf8" : "#2a2a2a";
}
function shadow(focused) {
return focused ? "0 0 0 3px rgba(56,189,248,0.15)" : "none";
}
</script>
<div class="input-group-demo">
<div class="inner">
<h1 class="title">Input Group</h1>
<p class="desc">Input with addon elements: icons, text, and buttons.</p>
<!-- Search -->
<div>
<label class="label">Search</label>
<div class="group" style="border-color: {borderColor(focusStates[0])}; box-shadow: {shadow(focusStates[0])};">
<span class="prepend">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</span>
<input class="input" placeholder="Search resources..." on:focus={() => setFocus(0, true)} on:blur={() => setFocus(0, false)} />
</div>
</div>
<!-- Website -->
<div>
<label class="label">Website</label>
<div class="group" style="border-color: {borderColor(focusStates[1])}; box-shadow: {shadow(focusStates[1])};">
<span class="prepend"><span class="text-addon">https://</span></span>
<input class="input" placeholder="example.com" on:focus={() => setFocus(1, true)} on:blur={() => setFocus(1, false)} />
</div>
</div>
<!-- Price -->
<div>
<label class="label">Price</label>
<div class="group" style="border-color: {borderColor(focusStates[2])}; box-shadow: {shadow(focusStates[2])};">
<span class="prepend"><span class="text-addon">$</span></span>
<input class="input" placeholder="0.00" type="number" on:focus={() => setFocus(2, true)} on:blur={() => setFocus(2, false)} />
<span class="append">
<button type="button" class="addon-btn">Apply</button>
</span>
</div>
</div>
<!-- Email -->
<div>
<label class="label">Email</label>
<div class="group" style="border-color: {borderColor(focusStates[3])}; box-shadow: {shadow(focusStates[3])};">
<span class="prepend">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="1" y="3" width="14" height="10" rx="2" stroke="currentColor" stroke-width="1.5" />
<path d="M1 5l7 4 7-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
<input class="input" placeholder="username" on:focus={() => setFocus(3, true)} on:blur={() => setFocus(3, false)} />
<span class="append"><span class="text-addon">@company.com</span></span>
</div>
</div>
<!-- API Key -->
<div>
<label class="label">API Key</label>
<div class="group" style="border-color: {borderColor(focusStates[4])}; box-shadow: {shadow(focusStates[4])};">
<input class="input" value="sk_live_a1b2c3d4e5f6" readonly on:focus={() => setFocus(4, true)} on:blur={() => setFocus(4, false)} />
<span class="append">
<button type="button" class="addon-btn" on:click={handleCopy}>
{copied ? 'Copied!' : 'Copy'}
</button>
</span>
</div>
</div>
<!-- Disabled -->
<div style="opacity: 0.5;">
<label class="label">Disabled</label>
<div class="group" style="border-color: #2a2a2a;">
<span class="prepend"><span class="text-addon">@</span></span>
<input class="input" placeholder="Not editable" disabled />
</div>
</div>
</div>
</div>
<style>
.input-group-demo {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
font-family: Inter, system-ui, sans-serif;
color: #f2f6ff;
padding: 2rem;
}
.inner {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.title {
font-size: 1.5rem;
font-weight: 800;
margin: 0 0 0.375rem;
}
.desc {
color: #475569;
font-size: 0.875rem;
margin: 0 0 1rem;
}
.label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.375rem;
}
.group {
display: flex;
align-items: stretch;
border-radius: 0.625rem;
border: 1px solid;
background: #141414;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
}
.prepend {
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.75rem;
color: #4a4a4a;
font-size: 0.8125rem;
white-space: nowrap;
flex-shrink: 0;
border-right: 1px solid #2a2a2a;
background: rgba(255,255,255,0.02);
}
.append {
display: flex;
align-items: center;
justify-content: center;
padding: 0 0.75rem;
color: #4a4a4a;
font-size: 0.8125rem;
white-space: nowrap;
flex-shrink: 0;
border-left: 1px solid #2a2a2a;
background: rgba(255,255,255,0.02);
}
.input {
flex: 1;
min-width: 0;
padding: 0.625rem 0.75rem;
background: transparent;
border: none;
color: #f2f6ff;
font-size: 0.875rem;
font-family: inherit;
outline: none;
}
.text-addon {
color: #64748b;
font-weight: 500;
}
.addon-btn {
background: rgba(56,189,248,0.1);
color: #38bdf8;
border: none;
padding: 0 0.75rem;
font-weight: 600;
font-size: 0.8125rem;
cursor: pointer;
font-family: inherit;
}
</style>Input Group
Input fields with addon elements such as icon prefixes, button suffixes, and text prepends, all seamlessly connected with shared borders.
Features
- Prepend and append addon slots
- Icon, text, and button addons
- Seamless border connection between elements
- Focus ring spans the full group
- Dark theme styling