Klarhimmel UI

Input group

Stable

One field made of parts: a prefix or suffix, the input and a button, sharing one border and one focus ring.

npx shadcn add @klarhimmel/input-group @klarhimmel/input

When to use

  • Use for

    A value with its unit, scheme or action beside it: kr, https://, a copy button, a search icon.

  • Not for

    A plain field. Use Input. A choice from a list. Use Select.

  • Pairs with

    Field, Kbd, Button.

Preview

krper night

Including VAT.

input-group.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { Field } from '@/components/klarhimmel/ui/input';
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/klarhimmel/ui/input-group';

export default function Example() {
  return (
    <Theme theme="brand">
      <Field label="Price per night">
        {(p) => (
          <InputGroup>
            <InputGroupAddon>kr</InputGroupAddon>
            <InputGroupInput {...p} inputMode="numeric" />
          </InputGroup>
        )}
      </Field>
    </Theme>
  );
}

Install

Import

import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupButton } from '@/components/klarhimmel/ui/input-group';
Lands in
components/klarhimmel/ui/input-group.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
Render in
Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.

Examples

  • Url

    https://

    Share this address with guests.

    import { useState } from 'react';
    import { Check, Copy } from 'lucide-react';
    import { Field } from '@/components/klarhimmel/ui/input';
    import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/klarhimmel/ui/input-group';
    
    export default function InputGroupUrl() {
      const [copied, setCopied] = useState(false);
      return (
        <div className="flex w-[min(24rem,calc(100cqw-3rem))] flex-col gap-3">
          <Field label="Booking page address">
            {(p) => (
              <InputGroup>
                <InputGroupAddon>https://</InputGroupAddon>
                <InputGroupInput {...p} defaultValue="exempelhotellet.example/book" />
                <InputGroupButton aria-label="Copy the address" onClick={() => setCopied(true)}>
                  {copied ? <Check aria-hidden /> : <Copy aria-hidden />}
                </InputGroupButton>
              </InputGroup>
            )}
          </Field>
          <p role="status" className="text-kh-small text-kh-muted">{copied ? 'Copied.' : 'Share this address with guests.'}</p>
        </div>
      );
    }
  • States

    /
    %

    That code has expired.

    kr
    import { Search } from 'lucide-react';
    import { Field } from '@/components/klarhimmel/ui/input';
    import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/klarhimmel/ui/input-group';
    import { Kbd } from '@/components/klarhimmel/ui/kbd';
    
    export default function InputGroupStates() {
      return (
        <div className="flex w-[min(24rem,calc(100cqw-3rem))] flex-col gap-5">
          <Field label="Search rooms">
            {(p) => (
              <InputGroup>
                <InputGroupAddon><Search aria-hidden /></InputGroupAddon>
                <InputGroupInput {...p} placeholder="Sea view, balcony…" />
                <InputGroupAddon align="end"><Kbd>/</Kbd></InputGroupAddon>
              </InputGroup>
            )}
          </Field>
          <Field label="Discount code" error="That code has expired.">
            {(p) => (
              <InputGroup invalid>
                <InputGroupAddon>%</InputGroupAddon>
                <InputGroupInput {...p} defaultValue="SUMMER24" />
              </InputGroup>
            )}
          </Field>
          <Field label="Currency (set by your plan)">
            {(p) => (
              <InputGroup disabled>
                <InputGroupAddon>kr</InputGroupAddon>
                <InputGroupInput {...p} defaultValue="SEK" disabled />
              </InputGroup>
            )}
          </Field>
        </div>
      );
    }

API reference

PropTypeDefaultDescription
InputGroup: invalid / disabledbooleanfalseDraws the red border or the disabled look on the whole group. Put aria-invalid or disabled on the input too.
InputGroupAddon: align'start' | 'end''start'Which side of the input. Text is read in order, so write it as part of the question. An icon alone is decorative.
InputGroupInput<input> attributes-The input, without a frame of its own. Pass the Field's props through it.
InputGroupButton<button> attributestype='button'A button inside the frame. Give an icon-only one an aria-label.
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
TabMoves through the input and the buttons in the group
Enter / SpacePresses a focused button

The group draws the frame and the focus ring; the input is named by its Field label. Text addons are read in order, so write them as part of the question.

Accessibility