Input Group
A labeled native input with an optional prefix, suffix, or related action.
- Navegadores
- Chrome 123+, Firefox 121+, Safari 17.5+
- Nível
- 0
- Limite de layout forçado
- 0 ms
- Limite do axe
- 0
- Compatível com SSR
- Sim
- Contrato publicado
- Ambiente de teste
Experimentar
Input Group
https://
Enter a domain. Add https:// when using the value.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
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>
);
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
/* 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: 44px;
/* Never narrower than tall: an icon size only has to drop the padding to be a circle. */
min-inline-size: 44px;
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: 32px;
min-inline-size: 32px;
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: 56px;
min-inline-size: 56px;
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);
corner-shape: squircle;
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);
}
[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;
min-block-size: 2.75rem;
padding-inline: var(--space-md);
border: var(--border-width) solid var(--input);
border-radius: var(--radius-md);
corner-shape: squircle;
background-color: var(--card);
color: var(--foreground);
font-size: var(--text-sm);
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;
}
[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: 2.75rem; }
.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); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
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 [inline-size:100%] [max-inline-size:24rem] [min-inline-size:0] [min-block-size:2" +
".75rem] px-md border-[length:var(--border-width)] border-solid border-input rounded-md [corner-shape:squi" +
"rcle] bg-card text-foreground text-sm [line-height:var(--leading-normal)] [transition:var(--transition-co" +
"lors)] focus-within:border-primary focus-within:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_v" +
"ar(--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-dest" +
"ructive has-[:is(input,textarea,select):is([data-invalid],[aria-invalid=true],:user-invalid)]:focus-withi" +
"n:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--destructive)_20%,_transparent)] [&>:is(in" +
"put,textarea,select):is(:disabled,[data-disabled])]:opacity-50 [&>[data-part=addon][data-align=start]:not" +
"(:has(:is(a[href],button,input,select,textarea,[tabindex])))]:[-order:1] [&>[data-part=addon][data-align=" +
"end]:not(:has(:is(a[href],button,input,select,textarea,[tabindex])))]:[order:1] [&>[data-part=addon]>[dat" +
"a-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]]:[overf" +
"low-wrap:anywhere]"
}
>
<InputGroupAddon
className="
flex items-center [flex:0_1_auto] [min-inline-size:0] [max-inline-size:50%] text-muted-foreground
[overflow-wrap:anywhere]
"
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>
);
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
/* app.css */
@layer reset, ui, app, tokens, base, demo, components, utilities, theme;
@import "@gunkin/uiify/styles/reset";
@import "@gunkin/uiify/components/behavior.css";
@import "@gunkin/uiify/styles/tokens";
@import "@gunkin/uiify/styles/tailwind";
@import "tailwindcss/utilities.css";
@theme { --spacing: 0.25rem; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; }
@source inline('flex items-center gap-sm [inline-size:100%] [max-inline-size:24rem] [min-inline-size:0] [min-block-size:2.75rem] px-md border-[length:var(--border-width)] border-solid border-input rounded-md [corner-shape:squircle] bg-card text-foreground text-sm [line-height:var(--leading-normal)] [transition:var(--transition-colors)] 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-destructive 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)] [&>:is(input,textarea,select):is(:disabled,[data-disabled])]:opacity-50 [&>[data-part=addon][data-align=start]:not(:has(:is(a[href],button,input,select,textarea,[tabindex])))]:[-order:1] [&>[data-part=addon][data-align=end]:not(:has(:is(a[href],button,input,select,textarea,[tabindex])))]:[order:1] [&>[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:anywhere] [flex:0_1_auto] [max-inline-size:50%] text-muted-foreground [overflow-wrap:anywhere] flex-1 [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] grid gap-2 m-0');
@layer base { :root { --uiify-control-text: var(--text-sm); } @media (pointer: coarse) { :root { --uiify-control-text: var(--text-base); } } }
/* Authored animation and registered properties used by the Tailwind skin.
* Visual rules belong in utility recipes; only at-rule definitions live here. */
@layer components {
@keyframes uiify-presence-in {
from {
opacity: 0;
scale: var(--presence-scale);
}
}
}
@layer components {
@keyframes uiify-presence-out {
to {
opacity: 0;
scale: var(--presence-scale);
}
}
}
@property --uiify-spinner-tail {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --uiify-spinner-length {
syntax: "<angle>";
inherits: false;
initial-value: 36deg;
}
@layer components {
@keyframes uiify-spinner-rotate {
to {
transform: rotate(360deg);
}
}
}
@layer components {
@keyframes uiify-spinner-orbit {
0% {
--uiify-spinner-tail: 0deg;
--uiify-spinner-length: 36deg;
}
50% {
--uiify-spinner-tail: 90deg;
--uiify-spinner-length: 324deg;
}
100% {
--uiify-spinner-tail: 360deg;
--uiify-spinner-length: 36deg;
}
}
}
@layer components {
@keyframes uiify-skeleton-pulse {
50% {
opacity: 0.45;
}
}
}
@layer components {
@keyframes uiify-skeleton-shimmer {
to {
background-position: -200% 0;
}
}
}
@layer components {
@keyframes uiify-skeleton-reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
@property --candle-body-height {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
@property --candle-body-top {
syntax: "<percentage>";
inherits: true;
initial-value: 50%;
}
@property --candle-slot {
syntax: "<percentage>";
inherits: true;
initial-value: 100%;
}
@property --candle-wick-height {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
@property --candle-wick-top {
syntax: "<percentage>";
inherits: true;
initial-value: 50%;
}
@property --candle-x {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
@property --candle-scale {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@property --line-y {
syntax: "<percentage>";
inherits: true;
initial-value: 50%;
}
@property --line-prev-y {
syntax: "<percentage>";
inherits: true;
initial-value: 50%;
}
@property --line-count {
syntax: "<integer>";
inherits: false;
initial-value: 1;
}
@property --line-scale {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@layer components {
@keyframes uiify-otp-input-caret-blink {
50% {
opacity: 0;
}
}
}Exemplos
Currency and units
Use passive affixes for context and keep a visible label on the input. The amount is in US dollars.
$USD
Amount in US dollars.
import { Input, InputGroup, InputGroupAddon } from "@gunkin/uiify";
export default function Example() {
return (
<div className="example-field">
<label htmlFor="input-group-amount">Price</label>
<InputGroup>
<InputGroupAddon align="start">$</InputGroupAddon>
<Input id="input-group-amount" name="price" inputMode="decimal" defaultValue="25.00" aria-describedby="input-group-amount-help" />
<InputGroupAddon align="end">USD</InputGroupAddon>
</InputGroup>
<p className="example-description" id="input-group-amount-help">Amount in US dollars.</p>
</div>
);
}A native search form
This demo submits q to the current page with GET; it does not filter the documentation. Replace action with your own search endpoint.
import { Button, Input, InputGroup, InputGroupAddon } from "@gunkin/uiify";
export default function Example() {
return (
<search className="example-field">
<form className="example-field" action="/components/input-group" method="get">
<label htmlFor="input-group-search">Search</label>
<InputGroup>
<Input id="input-group-search" name="q" type="search" defaultValue="accessibility" />
<InputGroupAddon align="end"><Button size="sm" type="submit">Search</Button></InputGroupAddon>
</InputGroup>
</form>
</search>
);
}Invalid and disabled fields
Describe server errors below the input. A disabled input does not disable a related Configure link.
https://
Enter a valid domain.
import { Button, Input, InputGroup, InputGroupAddon } from "@gunkin/uiify";
export default function Example() {
return (
<div className="example-form">
<div className="example-field">
<label htmlFor="input-group-states-website">Website</label>
<InputGroup>
<InputGroupAddon align="start">https://</InputGroupAddon>
<Input id="input-group-states-website" name="website" invalid defaultValue="not a domain" aria-describedby="input-group-states-error" />
</InputGroup>
<p className="example-error" id="input-group-states-error">Enter a valid domain.</p>
</div>
<div className="example-field">
<label htmlFor="input-group-states-endpoint">Endpoint</label>
<InputGroup>
<Input id="input-group-states-endpoint" name="endpoint" disabled defaultValue="api.uiify.dev" />
<InputGroupAddon align="end"><Button as="a" href="/docs/installation" size="sm" variant="ghost">Configure</Button></InputGroupAddon>
</InputGroup>
</div>
</div>
);
}API
InputGroup
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
children | ReactNode | — | Native controls and optional start/end addons. |
className / style / ref / native div props | native div props | — | Native wrapper props, styles, events, and ref are forwarded. |
render | ReactElement | ((props, state) => ReactElement) | — | Substitua o elemento renderizado preservando comportamento e conexões de acessibilidade. |
InputGroupAddon
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
align* | "start" | "end" | — | Places the addon at the logical inline start or end. |
children | ReactNode | — | Addon content; it does not label the input. |
render | ReactElement | ((props, state) => ReactElement) | — | Substitua o elemento renderizado preservando comportamento e conexões de acessibilidade. |
Teclado
| Tecla | Ação |
|---|
Acessibilidade
A layout wrapper around consumer-owned native children. Label the input independently; addons are not labels, and actions remain native links or buttons. The wrapper adds no focus, validation, or client behavior.
Pontos de integração de estilos
data-uiify-input-groupdata-part="root|addon"data-align