דילוג לתוכן
uiify

Input Group

שדה קלט מובנה עם תווית ואפשרות לקידומת, לסיומת או לפעולה קשורה.

דפדפנים
Chrome 123+Firefox 121+Safari 17.5+
שכבה
0
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

$USD

סכום בדולרים אמריקאיים.

https://

יש להזין שם מתחם תקין.

https://
https://
https://
https://

Enter a domain. Add https:// when using the value.

import { Input, InputGroup, InputGroupAddon } from "@gunkin/uiify";

export default function Example() {
  return (
    <div className="example-field">
      <label htmlFor="input-group-website">Website</label>
      <InputGroup className="">
        <InputGroupAddon className="" align="start">
          https://
        </InputGroupAddon>
        <Input
          className=""
          id="input-group-website"
          name="website"
          type="text"
          inputMode="url"
          autoComplete="url"
          spellCheck={false}
          defaultValue="uiify.dev"
          aria-describedby="input-group-website-help"
        />
      </InputGroup>
      <p className="example-description" id="input-group-website-help">
        Enter a domain. Add https:// when using the value.
      </p>
    </div>
  );
}
/* Button — pill-shaped and monochrome: a filled foreground action by default, secondary for
   the tonal one, outline for the bordered one, ghost for a surface that only appears on
   hover, link for inline text actions.

   Filled variants are lit from above by two layers that sit between the fill and the label:
   - `::before`, the wash: a blurred copy of the shape raised by half its height, so only its
     soft lower edge glows inside the upper half. `filter` runs before `clip-path`, which lets
     one box blur itself and still be cut back to the button outline.
   - `::after`, the rim: a half-pixel ring whose colour fades out towards the bottom. A
     box-shadow cannot take a gradient and `border-image` ignores the radius, so the ring is
     a gradient box with its content box masked out. */

@layer components {
  :where([data-uiify-button]) {
    --uiify-button-rim: rgb(255 255 255 / 20%);
    --uiify-button-wash: light-dark(rgb(255 255 255 / 15%), rgb(255 255 255 / 30%));

    position: relative;
    isolation: isolate;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    min-block-size: var(--control-md);

    /* Never narrower than tall: an icon size only has to drop the padding to be a circle. */
    min-inline-size: var(--control-md);
    padding-block: 0;
    padding-inline: 1.25rem;
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-full);
    background-color: var(--foreground);
    background-clip: padding-box;
    color: var(--background);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    line-height: 1.5rem;
    text-decoration: none;
    cursor: pointer;
    transition: var(--transition-colors);
  }

  :where([data-uiify-button])::before,
  :where([data-uiify-button])::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    pointer-events: none;
  }

  :where([data-uiify-button])::before {
    background-color: var(--uiify-button-wash);
    filter: blur(10px);
    translate: 0 -50%;
    clip-path: inset(50% 0 -50% 0 round var(--radius-full));
  }

  :where([data-uiify-button])::after {
    padding: 0.5px;
    background-image: linear-gradient(to bottom, var(--uiify-button-rim), transparent 75%);
    mask:
      linear-gradient(#fff 0 0) content-box exclude,
      linear-gradient(#fff 0 0);
  }

  /* Sizes: md is the default metrics above. */
  [data-uiify-button]:is([data-size="sm"], [data-size="icon-sm"]) {
    min-block-size: var(--control-sm);
    min-inline-size: var(--control-sm);
    padding-inline: 0.75rem;
    gap: 0.25rem;
    font-size: var(--text-sm);
    line-height: 1.25rem;
  }

  [data-uiify-button]:is([data-size="lg"], [data-size="icon-lg"]) {
    min-block-size: var(--control-lg);
    min-inline-size: var(--control-lg);
    padding-inline: 1.75rem;
    font-size: var(--text-lg);
    line-height: 1.75rem;
  }

  /* Icon sizes: the same three heights with no padding, for a single icon. */
  [data-uiify-button][data-size^="icon"] {
    padding-inline: 0;
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 90%, var(--background));
    }
  }

  [data-uiify-button]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 80%, var(--background));
  }

  [data-uiify-button]:disabled,
  [data-uiify-button][data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Secondary: tonal low-emphasis fill. */
  [data-uiify-button][data-variant="secondary"] {
    --uiify-button-rim: light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 10%));
    --uiify-button-wash: light-dark(rgb(255 255 255 / 20%), rgb(255 255 255 / 5%));

    background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
    color: var(--foreground);
  }

  /* Outline: no fill and no wash, a hairline border in the text colour. Its rim is a full
     border-width ring laid over that border, so the top edge reads slightly stronger and
     fades into the plain border below. */
  [data-uiify-button][data-variant="outline"] {
    --uiify-button-rim: color-mix(in oklch, var(--foreground) 25%, transparent);

    border-color: color-mix(in oklch, var(--foreground) 20%, transparent);
    background-color: transparent;
    color: var(--foreground);
  }

  [data-uiify-button][data-variant="outline"]::before {
    content: none;
  }

  [data-uiify-button][data-variant="outline"]::after {
    inset: calc(-1 * var(--border-width));
    padding: var(--border-width);
  }

  [data-uiify-button][data-variant="outline"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
  }

  /* Ghost: text only; the tonal surface appears on hover. Flat, so no light layers. */
  [data-uiify-button][data-variant="ghost"] {
    background-color: transparent;
    color: var(--foreground);
  }

  [data-uiify-button][data-variant="ghost"]::before,
  [data-uiify-button][data-variant="ghost"]::after {
    content: none;
  }

  [data-uiify-button][data-variant="ghost"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
  }

  [data-uiify-button][data-variant="secondary"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--foreground) 26%, transparent);
  }

  /* Destructive: irreversible or dangerous actions. */
  [data-uiify-button][data-variant="destructive"] {
    --uiify-button-wash: rgb(255 255 255 / 15%);

    background-color: var(--destructive);
    color: var(--destructive-foreground);
  }

  [data-uiify-button][data-variant="destructive"]:active:not(:disabled, [data-disabled]) {
    background-color: color-mix(in oklch, var(--destructive), black 20%);
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button][data-variant="outline"]:hover:not(:disabled, [data-disabled]),
    [data-uiify-button][data-variant="ghost"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
    }

    [data-uiify-button][data-variant="secondary"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--foreground) 20%, transparent);
    }

    [data-uiify-button][data-variant="destructive"]:hover:not(:disabled, [data-disabled]) {
      background-color: color-mix(in oklch, var(--destructive), black 12%);
    }
  }
}

@layer components {
  /* Selected: the chosen item of a set or a toggle that is on, filled with the brand colour.
     The state is read from whatever already carries it: the `selected` prop, a pressed toggle,
     the current page, or — for a label styled as a button — the checked native control right
     before it, so a toggle works with no JavaScript. */
  [data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]),
  :checked + [data-uiify-button] {
    --uiify-button-rim: rgb(255 255 255 / 20%);
    --uiify-button-wash: rgb(255 255 255 / 15%);

    border-color: transparent;
    background-color: var(--primary);
    color: var(--primary-foreground);
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]):hover:not(
        :disabled,
        [data-disabled]
      ),
    :checked:not(:disabled) + [data-uiify-button]:hover {
      background-color: color-mix(in oklch, var(--primary) 88%, var(--primary-foreground));
    }
  }

  /* A label styled as a button follows the native control it belongs to. */
  :disabled + [data-uiify-button] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  [data-uiify-button][hidden] {
    display: none;
  }
}

@layer components {
  /* Link: an inline text action with no surface, on a button or an anchor alike. */
  [data-uiify-button][data-variant="link"],
  [data-uiify-button][data-variant="link"]:active {
    padding-inline: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--primary);
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }
  [data-uiify-button][data-variant="link"] {
    min-inline-size: 0;
  }

  @media (any-hover: hover) and (any-pointer: fine) {
    [data-uiify-button][data-variant="link"]:hover {
      padding-inline: 0;
      border: 0;
      border-radius: 0;
      background: transparent;
      color: var(--primary);
      text-decoration: underline;
      text-underline-offset: 0.2em;
    }
  }

  [data-uiify-button][data-variant="link"]::before,
  [data-uiify-button][data-variant="link"]::after {
    content: none;
  }

  [data-uiify-button][data-variant="link"]:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 3px;
  }
}

/* Text fields — Input and Textarea share the same surface. Invalid state keys
   off the [data-invalid] attribute the components emit. Textarea autosize uses
   the native field-sizing (Chrome 123+) when the component sets data-autosize. */

@layer components {
  :where([data-uiify-input], [data-uiify-textarea]) {
    display: block;
    inline-size: 100%;
    max-inline-size: 24rem;
    padding-block: var(--space-sm);
    padding-inline: var(--space-md);
    border: var(--border-width) solid var(--input);
    border-radius: var(--radius-md);
    background-color: var(--card);
    color: var(--foreground);
    font-size: var(--uiify-control-text);
    line-height: var(--leading-normal);
    transition: var(--transition-colors);
  }

  :where([data-uiify-input], [data-uiify-textarea])::placeholder {
    color: var(--muted-foreground);
  }

  :where([data-uiify-input], [data-uiify-textarea]):focus-visible {
    border-color: var(--primary);
  }

  :where([data-uiify-input], [data-uiify-textarea]):disabled,
  :where([data-uiify-input], [data-uiify-textarea])[data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  :where([data-uiify-input], [data-uiify-textarea])[data-invalid],
  :where([data-uiify-input], [data-uiify-textarea]):user-invalid {
    border-color: var(--destructive);
  }

  :where([data-uiify-input], [data-uiify-textarea])[data-invalid]:focus-visible {
    outline-color: var(--destructive);
  }

  /* Input is a pill sized like Button: --control-md tall and base text. Textarea keeps its own
     rounded rectangle. */
  [data-uiify-input] {
    box-sizing: border-box;
    block-size: var(--uiify-control-size, var(--control-md));
    padding-block: 0;
    padding-inline: var(--space-lg);
    border-radius: var(--radius-full);
    font-size: var(--uiify-control-font, var(--text-base));
  }

  [data-uiify-textarea] {
    min-block-size: 4lh;
    resize: block;
  }

  [data-uiify-textarea][data-autosize] {
    field-sizing: content;
    resize: none;
  }

  [data-uiify-field] {
    display: grid;
    gap: var(--space-sm);
    inline-size: 100%;
    max-inline-size: 24rem;
  }

  [data-uiify-field] [data-part="label"] {
    color: var(--foreground);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    line-height: var(--leading-normal);
  }

  [data-uiify-field] [data-part="label"][data-required]::after {
    content: " *";
    color: var(--destructive);
  }

  [data-uiify-field] [data-part="description"] {
    margin: 0;
    color: var(--muted-foreground);
    font-size: var(--text-xs);
    line-height: var(--leading-normal);
  }

  [data-uiify-field] [data-part="error"] {
    margin: 0;
    color: var(--destructive);
    font-size: var(--text-xs);
    line-height: var(--leading-normal);
  }

  /* Shared presence fade when an error message is mounted. */
  @media (prefers-reduced-motion: no-preference) {
    [data-uiify-field] [data-part="error"] {
      transition: opacity var(--duration-fast) var(--ease-out);
    }

    @starting-style {
      [data-uiify-field] [data-part="error"] {
        opacity: 0;
      }
    }
  }
}

@layer components {
  [data-uiify-input-group][data-part="root"] {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    inline-size: 100%;
    max-inline-size: 24rem;
    min-inline-size: 0;
    box-sizing: border-box;
    min-block-size: var(--uiify-control-size, var(--control-md));
    padding-inline: var(--space-lg);
    border: var(--border-width) solid var(--input);
    border-radius: var(--radius-full);
    background-color: var(--card);
    color: var(--foreground);
    font-size: var(--uiify-control-font, var(--text-base));
    line-height: var(--leading-normal);
    transition: var(--transition-colors);
  }

  [data-uiify-input-group][data-part="root"] > :is(input, textarea, select) {
    flex: 1 1 0%;
    inline-size: 100%;
    min-inline-size: 0;
    max-inline-size: none;
    min-block-size: 2.625rem;
    padding-block: var(--space-sm);
    padding-inline: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    box-shadow: none;
    outline: none;
  }

  /* A single-line control fills the root exactly: the root's height minus its two borders. */
  [data-uiify-input-group][data-part="root"] > :is(input, select) {
    block-size: calc(var(--uiify-control-size, var(--control-md)) - 2 * var(--border-width));
    min-block-size: 0;
    padding-block: 0;
  }

  [data-uiify-input-group][data-part="root"] > :is(input, textarea, select):focus-visible {
    box-shadow: none;
    outline: none;
  }

  [data-uiify-input-group][data-part="root"]:focus-within {
    border-color: var(--primary);
    box-shadow: 0 0 0 var(--ring-width) color-mix(in oklch, var(--ring) 20%, transparent);
  }

  [data-uiify-input-group][data-part="root"] > [data-part="addon"] {
    display: flex;
    align-items: center;
    flex: 0 1 auto;
    min-inline-size: 0;
    max-inline-size: 50%;
    color: var(--muted-foreground);
    overflow-wrap: anywhere;
  }

  [data-uiify-input-group] > [data-part="addon"] > [data-uiify-button] {
    min-inline-size: 0;
    max-inline-size: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  /* Reorder passive affixes only. Actions keep their DOM and keyboard order. */
  [data-uiify-input-group]
    > [data-part="addon"][data-align="start"]:not(
      :has(:is(a[href], button, input, select, textarea, [tabindex]))
    ) {
    order: -1;
  }

  [data-uiify-input-group]
    > [data-part="addon"][data-align="end"]:not(
      :has(:is(a[href], button, input, select, textarea, [tabindex]))
    ) {
    order: 1;
  }

  [data-uiify-input-group][data-part="root"]:has(
      > :is(input, textarea, select):is(:disabled, [data-disabled])
    ) {
    cursor: not-allowed;
  }

  [data-uiify-input-group][data-part="root"]
    > :is(input, textarea, select):is(:disabled, [data-disabled]) {
    opacity: 0.5;
  }

  [data-uiify-input-group][data-part="root"]:has(
      > :is(input, textarea, select):is([data-invalid], [aria-invalid="true"], :user-invalid)
    ) {
    border-color: var(--destructive);
  }

  [data-uiify-input-group][data-part="root"]:has(
      > :is(input, textarea, select):is([data-invalid], [aria-invalid="true"], :user-invalid)
    ):focus-within {
    box-shadow: 0 0 0 var(--ring-width) color-mix(in oklch, var(--destructive) 20%, transparent);
  }
}
/* Example layout; component surfaces use the skins above. */
.example-form, .example-field {
  display: grid;
  box-sizing: border-box;
  inline-size: 100%;
  max-inline-size: 24rem;
  min-inline-size: 0;
  margin: 0;
}
:is(.example-form, .example-field) * { box-sizing: border-box; }
.example-form { gap: 1.5rem; }
.example-field { gap: 0.5rem; }
.example-field > label {
  color: var(--foreground);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.5;
}
.example-field > [data-uiify-input] { min-block-size: var(--control-md); }
.example-description, .example-error {
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.example-description { color: var(--muted-foreground); }
.example-error { color: var(--destructive); }
import { Input, InputGroup, InputGroupAddon } from "@gunkin/uiify";

export default function Example() {
  return (
    <div className="grid gap-2 [min-inline-size:0]">
      <label htmlFor="input-group-website">Website</label>
      <InputGroup
        className={
          "flex items-center gap-sm box-border [inline-size:100%] [max-inline-size:24rem] [min-inline-size:0] [min-b" +
          "lock-size:var(--control-md)] px-lg border-[length:var(--border-width)] border-solid border-input rounded-" +
          "full bg-card text-foreground text-base [line-height:var(--leading-normal)] [transition:var(--transition-c" +
          "olors)] focus-within:border-primary focus-within:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_" +
          "var(--ring)_20%,_transparent)] has-[>input:disabled]:cursor-not-allowed has-[>[data-disabled]]:cursor-not" +
          "-allowed has-[:is(input,textarea,select):is([data-invalid],[aria-invalid=true],:user-invalid)]:border-des" +
          "tructive has-[:is(input,textarea,select):is([data-invalid],[aria-invalid=true],:user-invalid)]:focus-with" +
          "in:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--destructive)_20%,_transparent)] [&>:is(i" +
          "nput,textarea,select):is(:disabled,[data-disabled])]:opacity-50 [&>[data-part=addon]>[data-uiify-button]]" +
          ":[min-inline-size:0] [&>[data-part=addon]>[data-uiify-button]]:[max-inline-size:100%] [&>[data-part=addon" +
          "]>[data-uiify-button]]:whitespace-normal [&>[data-part=addon]>[data-uiify-button]]:[overflow-wrap:anywher" +
          "e]"
        }
      >
        <InputGroupAddon
          className="
            flex items-center [flex:0_1_auto] [min-inline-size:0] [max-inline-size:50%] text-muted-foreground
            [overflow-wrap:anywhere]
            [&[data-align=start]:not(:has(:is(a[href],button,input,select,textarea,[tabindex])))]:[-order:1]
            [&[data-align=end]:not(:has(:is(a[href],button,input,select,textarea,[tabindex])))]:[order:1]
          "
          align="start"
        >
          https://
        </InputGroupAddon>
        <Input
          className="
            flex-1 [inline-size:100%] [min-inline-size:0] [max-inline-size:none] [min-block-size:2.625rem] py-sm px-0
            border-0 border-none rounded-none bg-transparent text-inherit [font:inherit] [box-shadow:none]
            [outline:none] placeholder:text-muted-foreground focus-visible:[box-shadow:none]
            focus-visible:[outline:none]
          "
          id="input-group-website"
          name="website"
          type="text"
          inputMode="url"
          autoComplete="url"
          spellCheck={false}
          defaultValue="uiify.dev"
          aria-describedby="input-group-website-help"
        />
      </InputGroup>
      <p className="m-0 text-sm text-muted-foreground" id="input-group-website-help">
        Enter a domain. Add https:// when using the value.
      </p>
    </div>
  );
}

API

InputGroup

מאפייןטיפוסברירת מחדלתיאור
size"sm" | "md" | "lg""md"גובה הפקד וגודל הטקסט, בהתאמה ל-Button (32, 40 או 48 px); נכתבים ל-data-size, עם md כשלא הוגדר ערך.
childrenReactNode—פקדים מובנים ותוספות אופציונליות בתחילה ובסוף.
className / style / ref / native div propsnative div props—המאפיינים, הסגנונות, האירועים וה-ref של המעטפת המובנית מועברים הלאה.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

InputGroupAddon

מאפייןטיפוסברירת מחדלתיאור
align*"start" | "end"—ממקם את התוספת בתחילה או בסוף הלוגיים של ציר השורה.
childrenReactNode—תוכן התוספת; אינו משמש כתווית לקלט.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

מקלדת

מקשפעולה

נגישות

מעטפת פריסה סביב ילדים מובנים שבאחריות המשתמש בספרייה. תנו תווית נפרדת לשדה הקלט; תוספות אינן תוויות, ופעולות נשארות קישורים או כפתורים מובנים. המעטפת אינה מוסיפה מיקוד, אימות או התנהגות בצד הלקוח.

נקודות חיבור לעיצוב

data-uiify-input-groupdata-part="root|addon"data-align
דף זה תורגם אוטומטית וטרם נבדק.