Adding Input Group, cleaning up other new components

This commit is contained in:
Ronan Berder
2025-11-03 11:51:39 +08:00
parent ab44d6d48a
commit 0c16664e4b
5 changed files with 848 additions and 283 deletions
+411 -38
View File
@@ -11,6 +11,7 @@
--color-amber-100: oklch(96.2% 0.059 95.617);
--color-amber-900: oklch(41.4% 0.112 45.904);
--color-amber-950: oklch(27.9% 0.077 45.635);
--color-yellow-500: oklch(79.5% 0.184 86.047);
--color-green-50: oklch(98.2% 0.018 155.826);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214);
@@ -24,7 +25,6 @@
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-blue-950: oklch(28.2% 0.091 267.935);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-neutral-300: oklch(87% 0 0);
--color-neutral-800: oklch(26.9% 0 0);
--color-black: #000;
@@ -304,6 +304,9 @@
.right-1 {
right: calc(var(--spacing) * 1);
}
.right-1\.5 {
right: calc(var(--spacing) * 1.5);
}
.right-2 {
right: calc(var(--spacing) * 2);
}
@@ -313,12 +316,18 @@
.right-3 {
right: calc(var(--spacing) * 3);
}
.bottom-0 {
bottom: calc(var(--spacing) * 0);
}
.bottom-2 {
bottom: calc(var(--spacing) * 2);
}
.bottom-3 {
bottom: calc(var(--spacing) * 3);
}
.left-1\.5 {
left: calc(var(--spacing) * 1.5);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
@@ -355,6 +364,9 @@
max-width: 96rem;
}
}
.m-0 {
margin: calc(var(--spacing) * 0);
}
.mx-auto {
margin-inline: auto;
}
@@ -450,6 +462,10 @@
width: calc(var(--spacing) * 2.5);
height: calc(var(--spacing) * 2.5);
}
.size-3 {
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
}
.size-3\.5 {
width: calc(var(--spacing) * 3.5);
height: calc(var(--spacing) * 3.5);
@@ -458,10 +474,6 @@
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.size-5 {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
@@ -486,10 +498,6 @@
width: calc(var(--spacing) * 12);
height: calc(var(--spacing) * 12);
}
.size-16 {
width: calc(var(--spacing) * 16);
height: calc(var(--spacing) * 16);
}
.h-4 {
height: calc(var(--spacing) * 4);
}
@@ -508,8 +516,8 @@
.h-14 {
height: calc(var(--spacing) * 14);
}
.h-px {
height: 1px;
.h-auto {
height: auto;
}
.max-h-64 {
max-height: calc(var(--spacing) * 64);
@@ -526,18 +534,42 @@
.max-h-\[650px\] {
max-height: 650px;
}
.min-h-21 {
min-height: calc(var(--spacing) * 21);
}
.min-h-22 {
min-height: calc(var(--spacing) * 22);
}
.min-h-24 {
min-height: calc(var(--spacing) * 24);
}
.min-h-25 {
min-height: calc(var(--spacing) * 25);
}
.min-h-26 {
min-height: calc(var(--spacing) * 26);
}
.min-h-27 {
min-height: calc(var(--spacing) * 27);
}
.min-h-28 {
min-height: calc(var(--spacing) * 28);
}
.min-h-30 {
min-height: calc(var(--spacing) * 30);
}
.min-h-32 {
min-height: calc(var(--spacing) * 32);
}
.min-h-64 {
min-height: calc(var(--spacing) * 64);
.min-h-77 {
min-height: calc(var(--spacing) * 77);
}
.min-h-\[350px\] {
min-height: 350px;
}
.w-0 {
width: calc(var(--spacing) * 0);
}
.w-1\/2 {
width: calc(1/2 * 100%);
}
@@ -586,6 +618,15 @@
.w-max {
width: max-content;
}
.max-w-23 {
max-width: calc(var(--spacing) * 23);
}
.max-w-24 {
max-width: calc(var(--spacing) * 24);
}
.max-w-72 {
max-width: calc(var(--spacing) * 72);
}
.max-w-\[75\%\] {
max-width: 75%;
}
@@ -814,14 +855,14 @@
.self-start {
align-self: flex-start;
}
.self-stretch {
align-self: stretch;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
@@ -834,6 +875,9 @@
.rounded-\[2px\] {
border-radius: 2px;
}
.rounded-\[calc\(var\(--radius\)-5px\)\] {
border-radius: calc(var(--radius) - 5px);
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
@@ -843,12 +887,23 @@
.rounded-md {
border-radius: var(--radius-md);
}
.rounded-none {
border-radius: 0;
}
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-xl {
border-radius: var(--radius-xl);
}
.rounded-l-md {
border-top-left-radius: var(--radius-md);
border-bottom-left-radius: var(--radius-md);
}
.rounded-r-md {
border-top-right-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
@@ -861,10 +916,22 @@
border-top-style: var(--tw-border-style);
border-top-width: 1px;
}
.border-r {
border-right-style: var(--tw-border-style);
border-right-width: 1px;
}
.border-r-0 {
border-right-style: var(--tw-border-style);
border-right-width: 0px;
}
.border-b {
border-bottom-style: var(--tw-border-style);
border-bottom-width: 1px;
}
.border-l-0 {
border-left-style: var(--tw-border-style);
border-left-width: 0px;
}
.border-dashed {
--tw-border-style: dashed;
border-style: dashed;
@@ -881,9 +948,18 @@
border-color: color-mix(in oklab, var(--color-border) 50%, transparent);
}
}
.border-input {
border-color: var(--color-input);
}
.border-muted {
border-color: var(--color-muted);
}
.border-muted-foreground\/20 {
border-color: var(--color-muted-foreground);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-muted-foreground) 20%, transparent);
}
}
.border-transparent {
border-color: transparent;
}
@@ -902,21 +978,18 @@
.bg-background {
background-color: var(--color-background);
}
.bg-blue-500 {
background-color: var(--color-blue-500);
}
.bg-card {
background-color: var(--color-card);
}
.bg-foreground {
background-color: var(--color-foreground);
}
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-muted {
background-color: var(--color-muted);
}
.bg-muted-foreground {
background-color: var(--color-muted-foreground);
}
.bg-muted\/50 {
background-color: var(--color-muted);
@supports (color: color-mix(in lab, red, red)) {
@@ -926,9 +999,6 @@
.bg-primary {
background-color: var(--color-primary);
}
.bg-purple-500 {
background-color: var(--color-purple-500);
}
.bg-sidebar-primary {
background-color: var(--color-sidebar-primary);
}
@@ -980,6 +1050,9 @@
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.px-9 {
padding-inline: calc(var(--spacing) * 9);
}
.px-\[0\.3rem\] {
padding-inline: 0.3rem;
}
@@ -995,54 +1068,135 @@
.py-4 {
padding-block: calc(var(--spacing) * 4);
}
.py-15 {
padding-block: calc(var(--spacing) * 15);
}
.py-\[0\.2rem\] {
padding-block: 0.2rem;
}
.pt-1\.5 {
padding-top: calc(var(--spacing) * 1.5);
}
.pt-2 {
padding-top: calc(var(--spacing) * 2);
}
.pt-3 {
padding-top: calc(var(--spacing) * 3);
}
.pt-10 {
padding-top: calc(var(--spacing) * 10);
}
.pt-12 {
padding-top: calc(var(--spacing) * 12);
}
.pt-14 {
padding-top: calc(var(--spacing) * 14);
}
.pt-15 {
padding-top: calc(var(--spacing) * 15);
}
.pr-2 {
padding-right: calc(var(--spacing) * 2);
}
.pr-9 {
padding-right: calc(var(--spacing) * 9);
}
.pr-10 {
padding-right: calc(var(--spacing) * 10);
}
.pr-12 {
padding-right: calc(var(--spacing) * 12);
}
.pr-16 {
padding-right: calc(var(--spacing) * 16);
}
.pr-20 {
padding-right: calc(var(--spacing) * 20);
}
.pr-22 {
padding-right: calc(var(--spacing) * 22);
}
.pr-24 {
padding-right: calc(var(--spacing) * 24);
}
.pr-26 {
padding-right: calc(var(--spacing) * 26);
}
.pr-28 {
padding-right: calc(var(--spacing) * 28);
}
.pr-30 {
padding-right: calc(var(--spacing) * 30);
}
.pr-32 {
padding-right: calc(var(--spacing) * 32);
}
.pr-129 {
padding-right: calc(var(--spacing) * 129);
}
.pb-3 {
padding-bottom: calc(var(--spacing) * 3);
}
.pb-4 {
padding-bottom: calc(var(--spacing) * 4);
}
.pb-10 {
padding-bottom: calc(var(--spacing) * 10);
}
.pb-10\.5 {
padding-bottom: calc(var(--spacing) * 10.5);
}
.pb-11 {
padding-bottom: calc(var(--spacing) * 11);
}
.pb-12 {
padding-bottom: calc(var(--spacing) * 12);
}
.pb-14 {
padding-bottom: calc(var(--spacing) * 14);
}
.pb-17 {
padding-bottom: calc(var(--spacing) * 17);
}
.pl-6 {
padding-left: calc(var(--spacing) * 6);
}
.pl-7 {
padding-left: calc(var(--spacing) * 7);
}
.pl-8 {
padding-left: calc(var(--spacing) * 8);
}
.pl-8\.5 {
padding-left: calc(var(--spacing) * 8.5);
}
.pl-9 {
padding-left: calc(var(--spacing) * 9);
}
.pl-10 {
padding-left: calc(var(--spacing) * 10);
}
.pl-12 {
padding-left: calc(var(--spacing) * 12);
}
.pl-14 {
padding-left: calc(var(--spacing) * 14);
}
.pl-15 {
padding-left: calc(var(--spacing) * 15);
}
.pl-16 {
padding-left: calc(var(--spacing) * 16);
}
.pl-19 {
padding-left: calc(var(--spacing) * 19);
}
.pl-20 {
padding-left: calc(var(--spacing) * 20);
}
.pl-21 {
padding-left: calc(var(--spacing) * 21);
}
.text-center {
text-align: center;
}
@@ -1162,9 +1316,6 @@
.text-sidebar-primary-foreground {
color: var(--color-sidebar-primary-foreground);
}
.text-white {
color: var(--color-white);
}
.uppercase {
text-transform: uppercase;
}
@@ -1285,6 +1436,21 @@
visibility: visible;
}
}
.group-data-\[disabled\=true\]\:pointer-events-none {
&:is(:where(.group)[data-disabled="true"] *) {
pointer-events: none;
}
}
.group-data-\[disabled\=true\]\:opacity-50 {
&:is(:where(.group)[data-disabled="true"] *) {
opacity: 50%;
}
}
.group-data-\[disabled\=true\]\/field\:opacity-50 {
&:is(:where(.group\/field)[data-disabled="true"] *) {
opacity: 50%;
}
}
.group-\[\.copied\]\:block {
&:is(:where(.group):is(.copied) *) {
display: block;
@@ -1300,6 +1466,16 @@
border-color: var(--color-primary);
}
}
.peer-disabled\:cursor-not-allowed {
&:is(:where(.peer):disabled ~ *) {
cursor: not-allowed;
}
}
.peer-disabled\:opacity-50 {
&:is(:where(.peer):disabled ~ *) {
opacity: 50%;
}
}
.last\:border-b-0 {
&:last-child {
border-bottom-style: var(--tw-border-style);
@@ -1385,6 +1561,13 @@
}
}
}
.hover\:bg-foreground {
&:hover {
@media (hover: hover) {
background-color: var(--color-foreground);
}
}
}
.hover\:bg-muted {
&:hover {
@media (hover: hover) {
@@ -1392,6 +1575,13 @@
}
}
}
.hover\:text-accent-foreground {
&:hover {
@media (hover: hover) {
color: var(--color-accent-foreground);
}
}
}
.hover\:text-foreground {
&:hover {
@media (hover: hover) {
@@ -1466,11 +1656,60 @@
pointer-events: none;
}
}
.disabled\:border {
&:disabled {
border-style: var(--tw-border-style);
border-width: 1px;
}
}
.disabled\:opacity-50 {
&:disabled {
opacity: 50%;
}
}
.has-data-\[state\=checked\]\:border-primary {
&:has(*[data-state="checked"]) {
border-color: var(--color-primary);
}
}
.has-data-\[state\=checked\]\:bg-primary\/5 {
&:has(*[data-state="checked"]) {
background-color: var(--color-primary);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-primary) 5%, transparent);
}
}
}
.has-\[\:checkd\]\:border {
&:has(*:is(:checkd)) {
border-style: var(--tw-border-style);
border-width: 1px;
}
}
.has-\[\:checked\]\:border-primary {
&:has(*:is(:checked)) {
border-color: var(--color-primary);
}
}
.has-\[\:checked\]\:bg-muted {
&:has(*:is(:checked)) {
background-color: var(--color-muted);
}
}
.has-\[\:checked\]\:bg-primary\/5 {
&:has(*:is(:checked)) {
background-color: var(--color-primary);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-primary) 5%, transparent);
}
}
}
.has-\[input\:checkd\]\:border {
&:has(*:is(input:checkd)) {
border-style: var(--tw-border-style);
border-width: 1px;
}
}
.has-\[input\[type\=\'checkbox\'\]\:checked\]\:border-blue-600 {
&:has(*:is(input[type='checkbox']:checked)) {
border-color: var(--color-blue-600);
@@ -1491,6 +1730,32 @@
background-color: var(--color-green-50);
}
}
.has-\[\>\[data-slot\=field\]\]\:w-full {
&:has(>[data-slot=field]) {
width: 100%;
}
}
.has-\[\>\[data-slot\=field\]\]\:flex-col {
&:has(>[data-slot=field]) {
flex-direction: column;
}
}
.has-\[\>\[data-slot\=field\]\]\:rounded-md {
&:has(>[data-slot=field]) {
border-radius: var(--radius-md);
}
}
.has-\[\>\[data-slot\=field\]\]\:border {
&:has(>[data-slot=field]) {
border-style: var(--tw-border-style);
border-width: 1px;
}
}
.has-\[\>svg\]\:px-2 {
&:has(>svg) {
padding-inline: calc(var(--spacing) * 2);
}
}
.has-\[\>svg\]\:px-2\.5 {
&:has(>svg) {
padding-inline: calc(var(--spacing) * 2.5);
@@ -1509,6 +1774,35 @@
}
}
}
.aria-pressed\:bg-transparent {
&[aria-pressed="true"] {
background-color: transparent;
}
}
.aria-pressed\:text-blue-500 {
&[aria-pressed="true"] {
color: var(--color-blue-500);
}
}
.aria-pressed\:text-blue-600 {
&[aria-pressed="true"] {
color: var(--color-blue-600);
}
}
.aria-pressed\:text-yellow-500 {
&[aria-pressed="true"] {
color: var(--color-yellow-500);
}
}
.aria-pressed\:hover\:bg-accent {
&[aria-pressed="true"] {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
}
}
}
}
.max-sm\:flex-col {
@media (width < 40rem) {
flex-direction: column;
@@ -1691,6 +1985,18 @@
}
}
}
.dark\:hover\:bg-accent\/50 {
&:is(.dark *) {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-accent) 50%, transparent);
}
}
}
}
}
.dark\:hover\:bg-destructive\/20 {
&:is(.dark *) {
&:hover {
@@ -1713,6 +2019,26 @@
}
}
}
.dark\:has-data-\[state\=checked\]\:bg-primary\/10 {
&:is(.dark *) {
&:has(*[data-state="checked"]) {
background-color: var(--color-primary);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-primary) 10%, transparent);
}
}
}
}
.dark\:has-\[\:checked\]\:bg-primary\/10 {
&:is(.dark *) {
&:has(*:is(:checked)) {
background-color: var(--color-primary);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-primary) 10%, transparent);
}
}
}
}
.dark\:has-\[input\[type\=\'checkbox\'\]\:checked\]\:border-blue-900 {
&:is(.dark *) {
&:has(*:is(input[type='checkbox']:checked)) {
@@ -1751,6 +2077,20 @@
}
}
}
.dark\:aria-pressed\:hover\:bg-accent\/50 {
&:is(.dark *) {
&[aria-pressed="true"] {
&:hover {
@media (hover: hover) {
background-color: var(--color-accent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-accent) 50%, transparent);
}
}
}
}
}
}
.\[\&_\[data-new-link\]\:\:after\]\:ml-auto {
& [data-new-link]::after {
margin-left: auto;
@@ -1962,11 +2302,6 @@
padding-left: calc(var(--spacing) * 4);
}
}
.\[\&\+\[data-slot\=item-content\]\]\:flex-none {
&+[data-slot=item-content] {
flex: none;
}
}
.md\:\*\:\[\.card\]\:basis-1\/4 {
@media (width >= 48rem) {
:is(& > *) {
@@ -1995,6 +2330,19 @@
}
}
}
.\[disabled\]\:border {
&:is(disabled) {
border-style: var(--tw-border-style);
border-width: 1px;
}
}
.\[\&\>\*\]\:data-\[slot\=field\]\:p-4 {
&>* {
&[data-slot="field"] {
padding: calc(var(--spacing) * 4);
}
}
}
.\[\&\>a\]\:underline {
&>a {
text-decoration-line: underline;
@@ -2015,12 +2363,37 @@
margin-bottom: calc(var(--spacing) * 3);
}
}
.\[\&\>svg\]\:size-3 {
&>svg {
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
}
}
.\[\&\>svg\]\:size-4 {
&>svg {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
.\[\&\>svg\]\:size-6 {
&>svg {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
}
.aria-pressed\:\[\&\>svg\]\:fill-blue-600 {
&[aria-pressed="true"] {
&>svg {
fill: var(--color-blue-600);
}
}
}
.\[\&\>svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 {
&>svg:not([class*='size-']) {
width: calc(var(--spacing) * 3.5);
height: calc(var(--spacing) * 3.5);
}
}
}
:root {
--radius: 0.625rem;
@@ -3387,11 +3760,11 @@
line-height: var(--tw-leading, var(--text-base--line-height));
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
> .description {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-muted-foreground);
& + p {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-muted-foreground);
}
}
}
.field {
+166 -97
View File
@@ -29,9 +29,9 @@ toc:
{% set code_demo %}
<form class="w-full max-w-md space-y-6">
<fieldset class="fieldset" aria-describedby="payment-desc">
<fieldset class="fieldset">
<legend>Payment Method</legend>
<p id="payment-desc" class="description">All transactions are secure and encrypted</p>
<p>All transactions are secure and encrypted</p>
<div role="group" class="field">
<label for="card-name">Name on Card</label>
@@ -47,7 +47,7 @@ toc:
<div class="grid grid-cols-3 gap-4">
<div role="group" class="field">
<label for="exp-month">Month</label>
<select id="exp-month" class="select">
<select id="exp-month" class="select w-full">
<option value="">MM</option>
<option value="01">01</option>
<option value="02">02</option>
@@ -65,7 +65,7 @@ toc:
</div>
<div role="group" class="field">
<label for="exp-year">Year</label>
<select id="exp-year" class="select">
<select id="exp-year" class="select w-full">
<option value="">YYYY</option>
<option value="2024">2024</option>
<option value="2025">2025</option>
@@ -84,13 +84,15 @@ toc:
<hr role="separator" class="my-6 border-border">
<fieldset class="fieldset" aria-describedby="billing-desc">
<fieldset class="fieldset">
<legend>Billing Address</legend>
<p id="billing-desc" class="description">The billing address associated with your payment method</p>
<p>The billing address associated with your payment method</p>
<div class="flex items-center gap-3">
<input type="checkbox" id="same-as-shipping" checked>
<label for="same-as-shipping" class="font-normal">Same as shipping address</label>
<div role="group" class="field">
<label for="same-as-shipping" class="gap-3">
<input type="checkbox" id="same-as-shipping" checked>
Same as shipping address
</label>
</div>
</fieldset>
@@ -101,7 +103,7 @@ toc:
</div>
</fieldset>
<div class="flex gap-2">
<div class="flex gap-3">
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn-outline">Cancel</button>
</div>
@@ -116,10 +118,10 @@ toc:
<p>Field provides semantic HTML patterns for accessible forms:</p>
<ul>
<li><code>&lt;fieldset&gt;</code> and <code>&lt;legend&gt;</code> for grouping related fields</li>
<li><code>role="group"</code> with <code>.field</code> class for individual fields</li>
<li><code>.description</code> class for helper text</li>
<li><code>&lt;p&gt;</code> after <code>&lt;legend&gt;</code> is automatically styled as description text</li>
<li><code>role="group"</code> with <code>.field</code> class for individual field containers</li>
<li><code>.description</code> class for field-level helper text (requires <code>aria-describedby</code>)</li>
<li><code>.error</code> class with <code>role="alert"</code> for error messages</li>
<li><code>aria-describedby</code> to associate descriptions and errors with inputs</li>
</ul>
</section>
@@ -130,53 +132,91 @@ toc:
<h3 id="example-input"><a href="#example-input">Input</a></h3>
{% set code_input %}
<div class="grid gap-6 w-full max-w-md">
<div class="grid gap-6">
<div role="group" class="field">
<label for="username">Username</label>
<input id="username" type="text" placeholder="evilrabbit" aria-describedby="username-desc">
<p id="username-desc" class="description">Choose a unique username for your account</p>
<p id="username-desc" class="description">Choose a unique username for your account.</p>
</div>
<div role="group" class="field">
<label for="password">Password</label>
<input id="password" type="password" placeholder="••••••••" aria-invalid="true" aria-describedby="password-desc password-error">
<p id="password-desc" class="description">Must be at least 8 characters</p>
<p id="password-error" class="error" role="alert">Password is too short</p>
<p id="password-desc" class="description">Must be at least 8 characters long.</p>
<input id="password" type="password" placeholder="••••••••" aria-describedby="password-desc">
</div>
</div>
{% endset %}
{{ code_preview("field-input", code_input) }}
{{ code_preview("field-input", code_input, class="w-full max-w-md") }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
{% set code_textarea %}
<div role="group" class="field w-full max-w-md">
<label for="bio">Bio</label>
<textarea id="bio" rows="4" placeholder="Tell us about yourself" aria-describedby="bio-desc"></textarea>
<p id="bio-desc" class="description">Brief description for your profile (max 200 characters)</p>
<div role="group" class="field">
<label for="bio">Feedback</label>
<textarea id="bio" rows="4" placeholder="Your feedback helps us improve..." aria-describedby="bio-desc"></textarea>
<p id="bio-desc" class="description">Share your thoughts about our service.</p>
</div>
{% endset %}
{{ code_preview("field-textarea", code_textarea) }}
{{ code_preview("field-textarea", code_textarea, class="w-full max-w-md") }}
<h3 id="example-select"><a href="#example-select">Select</a></h3>
{% set code_select %}
<div role="group" class="field w-full max-w-md">
<label for="country">Country</label>
<select id="country" class="select" aria-describedby="country-desc">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
<div role="group" class="field">
<label for="country">Department</label>
<select id="country" class="select w-full" aria-describedby="country-desc">
<option value="">Choose department</option>
<option value="engineering">Engineering</option>
<option value="design">Design</option>
<option value="marketing">Marketing</option>
<option value="sales">Sales</option>
<option value="support">Customer Support</option>
<option value="hr">Human Resources</option>
<option value="finance">Finance</option>
<option value="operations">Operations</option>
</select>
<p id="country-desc" class="description">Choose your country of residence</p>
<p id="country-desc" class="description">Select your department or area of work.</p>
</div>
{% endset %}
{{ code_preview("field-select", code_select) }}
{{ code_preview("field-select", code_select, class="w-full max-w-md") }}
<h3 id="example-slider"><a href="#example-slider">Slider</a></h3>
{% set code %}
<div role="group" class="field">
<label for="price-range">Price</label>
<p id="price-range-desc" class="description">Set your budget: $<span id="price-range-value">150</span></p>
<input id="price-range" type="range" min="0" max="500" value="150">
</div>
<script>
(() => {
const sliders = document.querySelectorAll('input[type="range"]');
if (!sliders) return;
const updateSlider = (el) => {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
};
sliders.forEach(slider => {
updateSlider(slider);
slider.addEventListener('input', (event) => updateSlider(event.target));
});
const priceSlider = document.getElementById('price-range');
const priceValue = document.getElementById('price-range-value');
priceSlider.addEventListener('input', (event) => priceValue.innerHTML = event.target.value);
})();
</script>
{% endset %}
{{ code_preview("slider", code, "w-full max-w-sm") }}
<h3 id="example-fieldset"><a href="#example-fieldset">Fieldset</a></h3>
@@ -186,9 +226,9 @@ toc:
{% set code_fieldset %}
<form class="w-full max-w-md">
<fieldset class="fieldset" aria-describedby="profile-desc">
<fieldset class="fieldset">
<legend>Profile</legend>
<p id="profile-desc" class="description">This information will be displayed on your profile</p>
<p>This information will be displayed on your profile</p>
<div role="group" class="field">
<label for="full-name">Full name</label>
@@ -217,54 +257,84 @@ toc:
<h3 id="example-checkbox"><a href="#example-checkbox">Checkbox</a></h3>
{% set code_checkbox %}
<fieldset class="fieldset w-full max-w-md" aria-describedby="tasks-desc">
<legend>Tasks</legend>
<p id="tasks-desc" class="description">Select the tasks you want to receive updates for</p>
<fieldset class="fieldset">
<legend>Show these items on the desktop</legend>
<p>Select the items you want to show on the desktop.</p>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<input type="checkbox" id="push-notifications" checked>
<label for="push-notifications" class="font-normal">Push notifications</label>
<div class="field">
<label class="gap-3">
<input type="checkbox">
Hard disks
</label>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="email-notifications" checked>
<label for="email-notifications" class="font-normal">Email notifications</label>
<div class="field">
<label class="gap-3">
<input type="checkbox">
External disks
</label>
</div>
<div class="flex items-center gap-3">
<input type="checkbox" id="sms-notifications">
<label for="sms-notifications" class="font-normal">SMS notifications</label>
<div class="field">
<label class="gap-3">
<input type="checkbox">
CDs, DVDs and iPods
</label>
</div>
<div class="field">
<label class="gap-3">
<input type="checkbox">
Connected servers
</label>
</div>
</div>
<hr role="separator">
<div class="field">
<div class="flex w-full gap-3 flex-row items-center">
<input id="sync-desktop-documents" type="checkbox" checked class="input self-start">
<div class="flex flex-1 flex-col gap-1.5 leading-snug">
<label for="sync-desktop-documents">Sync Desktop & Documents folders</label>
<p class="description">Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.</p>
</div>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-checkbox", code_checkbox) }}
{{ code_preview("field-checkbox", code_checkbox, class="w-full max-w-md") }}
<h3 id="example-radio"><a href="#example-radio">Radio Group</a></h3>
{% set code_radio %}
<fieldset class="fieldset w-full max-w-md" aria-describedby="notify-desc">
<legend>Notify me about</legend>
<p id="notify-desc" class="description">Choose which notifications you want to receive</p>
<fieldset class="fieldset">
<legend>Subscription plan</legend>
<p>Yearly and lifetime plans offer significant savings.</p>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<input type="radio" id="notify-all" name="notifications" checked>
<label for="notify-all" class="font-normal">All new messages</label>
<div role="radiogroup" class="grid gap-3">
<div class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan" checked>
Monthly ($9.99/month)
</label>
</div>
<div class="flex items-center gap-3">
<input type="radio" id="notify-direct" name="notifications">
<label for="notify-direct" class="font-normal">Direct messages and mentions</label>
<div class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan">
Yearly ($99.99/year)
</label>
</div>
<div class="flex items-center gap-3">
<input type="radio" id="notify-none" name="notifications">
<label for="notify-none" class="font-normal">Nothing</label>
<div class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan">
Lifetime ($299.99)
</label>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-radio", code_radio) }}
{{ code_preview("field-radio", code_radio, class="w-full max-w-md") }}
<h3 id="example-switch"><a href="#example-switch">Switch</a></h3>
@@ -273,44 +343,43 @@ toc:
</section>
{% set code_switch %}
<fieldset class="fieldset w-full max-w-md">
<legend>Notifications</legend>
<p class="description">Configure how you receive notifications</p>
<div class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<div>
<label for="marketing" class="font-medium">Marketing emails</label>
<p class="description text-sm">Receive emails about new products and features</p>
</div>
<input type="checkbox" id="marketing" class="toggle" checked>
</div>
<div class="flex items-center justify-between">
<div>
<label for="security" class="font-medium">Security emails</label>
<p class="description text-sm">Receive emails about account security</p>
</div>
<input type="checkbox" id="security" class="toggle" checked>
</div>
<div role="group" class="field flex-row">
<div class="flex flex-1 flex-col gap-1.5 leading-snug">
<label for="multi-factor-authentication">Multi-factor authentication</label>
<p class="description">Enable multi-factor authentication. If you do not have a two-factor device, you can use a one-time code sent to your email.</p>
</div>
</fieldset>
<input id="multi-factor-authentication" type="checkbox" role="switch">
</div>
{% endset %}
{{ code_preview("field-switch", code_switch) }}
{{ code_preview("field-switch", code_switch, class="w-full max-w-md") }}
<h2 id="accessibility"><a href="#accessibility">Accessibility</a></h2>
<section class="prose">
<ul>
<li>Always use <code>&lt;label&gt;</code> elements with <code>for</code> attribute matching input <code>id</code></li>
<li>Associate helper text and errors using <code>aria-describedby</code> on the input</li>
<li>Use <code>aria-invalid="true"</code> on inputs with errors</li>
<li>Use <code>role="alert"</code> on error messages for screen reader announcements</li>
<li>Group related fields with <code>&lt;fieldset&gt;</code> and <code>&lt;legend&gt;</code></li>
<li>Use <code>role="group"</code> for individual field wrappers</li>
</ul>
</section>
<h3 id="example-choice-card"><a href="#example-choice-card">Choice Card</a></h3>
{% set code_choice_card %}
<fieldset class="fieldset">
<legend class="text-sm">Compute Environment</legend>
<p>Select the compute environment for your cluster.</p>
<div role="group" class="field">
<label class="flex gap-3 border border-transparent rounded-md p-4 has-[:checked]:border-primary has-[:checked]:bg-primary/5 dark:has-[:checked]:bg-primary/10">
<div class="flex flex-1 flex-col gap-1.5 leading-snug">
<h3>Kubernetes</h3>
<p class="description text-">Run GPU workloads on a K8s configured cluster.</p>
</div>
<input id="choice-card" type="radio" name="choice-card" class="self-start" checked>
</label>
</div>
<div role="group" class="field">
<label class="flex gap-3 border border-transparent rounded-md p-4 has-[:checked]:border-primary has-[:checked]:bg-primary/5 dark:has-[:checked]:bg-primary/10">
<div class="flex flex-1 flex-col gap-1.5 leading-snug">
<h3>Virtual Machine</h3>
<p class="description text-">Access a VM configured cluster to run GPU workloads.</p>
</div>
<input id="choice-card" type="radio" name="choice-card" class="self-start">
</label>
</div>
</div>
{% endset %}
{{ code_preview("field-choice-card", code_choice_card, class="w-full max-w-md") }}
+259 -145
View File
@@ -1,247 +1,361 @@
---
layout: layouts/page.njk
title: Input Group
description: Display additional information or actions alongside inputs using simple HTML patterns.
description: Display additional information or actions to an input or textarea.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Icon Prefix
id: example-icon-prefix
- label: Icon Suffix
id: example-icon-suffix
- label: Text Prefix/Suffix
- label: Icon
id: example-icon
- label: Text
id: example-text
- label: Button Suffix
- label: Button
id: example-button
- label: Multiple Elements
id: example-multiple
- label: Tooltip
id: example-tooltip
- label: Textarea
id: example-textarea
- label: Spinner
id: example-spinner
- label: Dropdown
id: example-dropdown
- label: Group
id: example-group
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "dropdown-menu.njk" import dropdown_menu %}
{% from "popover.njk" import popover %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Input Group component in Basecoat.</h2>
</div>
{% set code_default %}
<div class="relative">
<input type="text" class="input pl-10" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
{% lucide "search" %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-9 pr-20" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">12 results</div>
</div>
<div class="relative">
<input type="text" class="input pl-15 pr-9" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">https://</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="This is content in a tooltip.">{% lucide "info" %}</div>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-27 pb-12" placeholder="Ask, Search or Chat..."></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
<button type="button" class="btn-icon-outline rounded-full size-6 text-muted-foreground hover:text-accent-foreground">
{% lucide "plus" %}
</button>
{{ dropdown_menu(
id="demo-dropdown-menu",
trigger="Auto",
trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground h-6 p-2"},
popover_attrs={"data-side": "top", "class": "min-w-32"},
items=[
{ type: "item", label: "Auto" },
{ type: "item", label: "Agent" },
{ type: "item", label: "Manual" }
]
) }}
<div class="text-muted-foreground text-sm ml-auto">52% used</div>
<hr class="w-0 h-4 border-r border-border shrink-0 m-0">
<button type="button" class="btn-icon rounded-full size-6 bg-muted-foreground hover:bg-foreground" disabled>
{% lucide "arrow-up" %}
</button>
</footer>
</div>
<div class="relative">
<input type="text" class="input pr-9" placeholder="@shadcn">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full [&>svg]:size-3">{% lucide "check" %}</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-default", code_default) }}
{{ code_preview("input-group-default", code_default, class="w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Input groups are achieved through composition using relative positioning and Tailwind utilities. No special component needed - just wrap your input with a positioned container and add your icons, text, or buttons.</p>
<p><strong>Key patterns:</strong></p>
<ul>
<li>Use <code>relative</code> wrapper around input</li>
<li>Position content with <code>absolute</code> + <code>top-1/2 -translate-y-1/2</code></li>
<li>Add padding to input (<code>pl-*</code> or <code>pr-*</code>) to make room for content</li>
<li>Use <code>pointer-events-none</code> for decorative icons</li>
</ul>
<p>Input groups are pure HTML composition using Tailwind utility classes. There is no dedicated component: use <code>relative</code> positioning on a wrapper, add your content with <code>absolute</code> positioning, and adjust input padding to make room.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
</section>
{{ code_block(code_default) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-icon-prefix"><a href="#example-icon-prefix">Icon Prefix</a></h3>
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
<section class="prose">
<p>Add an icon at the start of the input field.</p>
</section>
{% set code_icon_prefix %}
<div class="flex flex-col gap-4 w-full max-w-sm">
{% set code_icon %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-10" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
<input type="text" class="input pl-9" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
</div>
<div class="relative">
<input type="email" class="input pl-10" placeholder="Email">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
<input type="email" class="input pl-9" placeholder="Enter your email">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "mail" %}
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-icon-prefix", code_icon_prefix) }}
<h3 id="example-icon-suffix"><a href="#example-icon-suffix">Icon Suffix</a></h3>
<section class="prose">
<p>Add an icon at the end of the input field.</p>
</section>
{% set code_icon_suffix %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<input type="text" class="input pr-10" placeholder="Username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-green-500">
<input type="text" class="input px-9" placeholder="Card number">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "credit-card" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "check" %}
</div>
</div>
<div class="relative">
<input type="text" class="input pr-16" placeholder="Card number">
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1 pointer-events-none text-muted-foreground">
{% lucide "credit-card" %}
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 flex items-center gap-2">
{% lucide "star" %}
{% lucide "info" %}
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-icon-suffix", code_icon_suffix) }}
{{ code_preview("input-group-icon-prefix", code_icon, class="w-full max-w-sm") }}
<h3 id="example-text"><a href="#example-text">Text Prefix/Suffix</a></h3>
<section class="prose">
<p>Display text before or after the input value.</p>
</section>
<h3 id="example-text"><a href="#example-text">Text</a></h3>
{% set code_text %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-12 pr-16" placeholder="0.00">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
$
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
USD
</div>
<input type="text" class="input pl-7 pr-12" placeholder="0.00">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">$</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">USD</div>
</div>
<div class="relative">
<input type="text" class="input pl-20" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
https://
</div>
<input type="text" class="input pl-15 pr-12" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">https://</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">.com</div>
</div>
<div class="relative">
<input type="text" class="input pr-32" placeholder="username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
@company.com
</div>
<input type="text" class="input pr-30" placeholder="Enter your username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">@company.com</div>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-25 pb-12" placeholder="Enter your message"></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
<div class="text-muted-foreground text-sm">120 characters left</div>
</footer>
</div>
</div>
{% endset %}
{{ code_preview("input-group-text", code_text) }}
{{ code_preview("input-group-text", code_text, class="w-full max-w-sm") }}
<h3 id="example-button"><a href="#example-button">Button Suffix</a></h3>
<section class="prose">
<p>Add interactive buttons inside the input.</p>
</section>
<h3 id="example-button"><a href="#example-button">Button</a></h3>
{% set code_button %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-10" placeholder="Search...">
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "search" %}
</button>
<input type="text" readonly class="input pr-9" value="https://x.com/hunvreus">
<button class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">{% lucide "copy" %}</button>
</div>
<div class="relative">
<input type="password" class="input pr-10" placeholder="Password">
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "eye" %}
</button>
<input type="text" class="input pl-21 pr-9 rounded-full">
<button
aria-pressed="false"
onclick="this.ariaPressed = this.ariaPressed === 'true' ? 'false' : 'true'"
class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-icon-ghost text-muted-foreground hover:text-accent-foreground size-6 rounded-full aria-pressed:bg-transparent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent/50 aria-pressed:text-blue-600 aria-pressed:[&>svg]:fill-blue-600"
>{% lucide "star" %}</button>
<div class="absolute left-1.5 top-1/2 -translate-y-1/2 flex items-center gap-1 z-10">
{% set trigger %}
{% lucide "info" %}
{% endset %}
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-sm-icon-ghost rounded-full size-6"},
popover_attrs={"class": "max-w-72"}
) %}
<h3 class="font-medium mb-1">Your connection is not secure.</h3>
<p>You should not enter any sensitive information on this site.</p>
{% endcall %}
<div class="text-muted-foreground text-sm pointer-events-none">https://</div>
</div>
</div>
<div class="relative">
<input type="text" class="input pr-10" value="https://example.com/page">
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "copy" %}
</button>
<input type="text" readonly class="input pr-9" placeholder="Type to search...">
<button type="button" class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-secondary h-6 p-2 rounded">Search</button>
</div>
</div>
{% endset %}
{{ code_preview("input-group-button", code_button) }}
{{ code_preview("input-group-button", code_button, class="w-full max-w-sm") }}
<h3 id="example-multiple"><a href="#example-multiple">Multiple Elements</a></h3>
<h3 id="example-tooltip"><a href="#example-tooltip">Tooltip</a></h3>
<section class="prose">
<p>Combine multiple icons, text, and buttons.</p>
</section>
{% set code_multiple %}
<div class="flex flex-col gap-4 w-full max-w-sm">
{% set code_tooltip %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-10 pr-20" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
{% lucide "search" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
<span class="text-xs text-muted-foreground pointer-events-none">⌘K</span>
</div>
<input type="password" class="input pr-9" placeholder="Enter password">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="Password must be at least 8 characters long.">{% lucide "info" %}</div>
</div>
<div class="relative">
<input type="text" class="input pl-20 pr-10" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">
https://
</div>
<button type="button" class="absolute right-1 top-1/2 -translate-y-1/2 btn-icon-sm-ghost">
{% lucide "info" %}
</button>
<input type="email" class="input pr-9" placeholder="Your email address">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="We'll use this to send your notifications.">{% lucide "circle-help" %}</div>
</div>
<div class="relative">
<input type="email" class="input pl-9" placeholder="Your email address">
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="Click for help with API keys." data-side="left">{% lucide "circle-help" %}</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-multiple", code_multiple) }}
{{ code_preview("input-group-tooltip", code_tooltip, class="w-full max-w-sm") }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
<section class="prose">
<p>Add buttons to textareas for actions like submit.</p>
</section>
{% set code_textarea %}
<div class="flex flex-col gap-4 w-full max-w-sm">
<div class="relative">
<textarea class="textarea pr-10 min-h-24" placeholder="Enter your message..."></textarea>
<button type="button" class="absolute right-2 bottom-2 btn-icon-sm">
{% lucide "send" %}
<div class="relative">
<textarea class="textarea pt-15 pb-17 min-h-77" placeholder="console.log('Hello, world!')."></textarea>
<header class="absolute top-0 flex items-center w-full gap-2 p-3 border-b">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-brand-javascript size-4 text-muted-foreground"><path d="M20 4l-2 14.5l-6 2l-6 -2l-2 -14.5z"></path><path d="M7.5 8h3v8l-2 -1"></path><path d="M16.5 8h-2.5a.5 .5 0 0 0 -.5 .5v3a.5 .5 0 0 0 .5 .5h1.423a.5 .5 0 0 1 .495 .57l-.418 2.93l-2 .5"></path></svg>
<span class="font-mono text-sm text-muted-foreground mr-auto">script.js</span>
<button class="btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">
{% lucide "refresh-ccw" %}
</button>
<button class="btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">
{% lucide "copy" %}
</button>
</header>
<footer class="absolute bottom-0 flex items-center w-full gap-2 p-3 border-t">
<span class="text-sm text-muted-foreground mr-auto">Line 1, Column 1</span>
<button type="button" class="btn-sm">
Run
{% lucide "corner-down-left" %}
</button>
</footer>
</div>
{% endset %}
{{ code_preview("input-group-textarea", code_textarea, class="w-full max-w-md") }}
<h3 id="example-spinner"><a href="#example-spinner">Spinner</a></h3>
{% set code_spinner %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Searching..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 animate-spin opacity-50">
{% lucide "loader-circle" %}
</div>
</div>
<div class="relative">
<textarea class="textarea pb-10 min-h-32" placeholder="Type something..."></textarea>
<div class="absolute bottom-3 left-3 right-3 flex items-center justify-between gap-2">
<span class="text-xs text-muted-foreground">120 characters left</span>
<button type="button" class="btn-sm">Submit</button>
<input type="text" class="input pl-9" placeholder="Processing..." disabled>
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 animate-spin opacity-50">
{% lucide "loader-circle" %}
</div>
</div>
<div class="relative">
<input type="text" class="input pr-24" placeholder="Saving changes..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2 text-sm text-muted-foreground opacity-50">
<div>Saving...</div>
<div class="[&>svg]:size-4 animate-spin">
{% lucide "loader-circle" %}
</div>
</div>
</div>
<div class="relative">
<input type="text" class="input pl-9 pr-26" placeholder="Refreshing data..." disabled>
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 animate-spin opacity-50">
{% lucide "loader" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2 text-sm text-muted-foreground opacity-50">
Please wait...
</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-textarea", code_textarea) }}
{{ code_preview("input-group-spinner", code_spinner, class="w-full max-w-md") }}
<h2 id="accessibility"><a href="#accessibility">Accessibility</a></h2>
<h3 id="example-dropdown"><a href="#example-dropdown">Dropdown</a></h3>
<section class="prose">
<ul>
<li>Use <code>pointer-events-none</code> for decorative icons that shouldn't be interactive</li>
<li>Ensure buttons have proper <code>aria-label</code> or visible text</li>
<li>Maintain sufficient padding so text doesn't overlap with icons/buttons</li>
<li>Use semantic button types (<code>type="button"</code> vs <code>type="submit"</code>)</li>
</ul>
</section>
{% set code_dropdown %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Enter file name">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{{ dropdown_menu(
main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2"},
trigger=trigger,
trigger_attrs={"class": "btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6"},
popover_attrs={"data-align": "end", "class": "min-w-32"},
items=[
{ type: "item", label: "Settings" },
{ type: "item", label: "Copy path" },
{ type: "item", label: "Open location" }
]
) }}
</div>
<div class="relative">
<input type="text" class="input pr-30" placeholder="Enter search query">
{% set trigger %}
Search in...
{% lucide "chevron-down", { "class": "size-3" } %}
{% endset %}
{{ dropdown_menu(
main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2"},
trigger=trigger,
trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground p-2 h-6"},
popover_attrs={"data-align": "end", "class": "min-w-32"},
items=[
{ type: "item", label: "Documentation" },
{ type: "item", label: "Blog Posts" },
{ type: "item", label: "Changelog" }
]
) }}
</div>
</div>
{% endset %}
{{ code_preview("input-group-dropdown", code_dropdown, class="w-full max-w-sm") }}
<h3 id="example-group"><a href="#example-group">Group</a></h3>
{% set code_group %}
<div class="flex w-fit items-stretch">
<label for="url" class="text-sm flex items-center bg-muted border border-r-0 px-4 rounded-l-md">https://</label>
<div class="relative">
<input type="text" class="input pr-9 rounded-none">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4">{% lucide "link-2" %}</div>
</div>
<div class="text-sm flex items-center bg-muted border border-l-0 px-4 rounded-r-md">.com</div>
</div>
{% endset %}
{{ code_preview("input-group-group", code_group, class="w-full max-w-sm") }}
+9
View File
@@ -3,6 +3,8 @@ layout: layouts/page.njk
title: Item
description: A versatile component that you can use to display any content.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
@@ -58,6 +60,13 @@ toc:
{{ code_preview("item-basic", code_basic | prettyHtml, class="w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Items are pure HTML composition using Tailwind utility classes. Use semantic elements like <code>&lt;article&gt;</code> or <code>&lt;a&gt;</code>, apply flex layout, and compose with standard HTML elements like <code>&lt;h3&gt;</code>, <code>&lt;p&gt;</code>, <code>&lt;img&gt;</code>, or <code>&lt;button&gt;</code>.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-variants"><a href="#example-variants">Variants</a></h3>
+3 -3
View File
@@ -693,10 +693,10 @@
> legend {
@apply text-base font-medium mb-3;
}
> .description {
@apply text-sm text-muted-foreground;
& + p {
@apply text-sm text-muted-foreground;
}
}
}
.field {