Skip to content

Add new InputGroup compound component with Addon, Suffix, and Button support#249

Open
pedromenezes1 wants to merge 22 commits intocloudflare:mainfrom
pedromenezes1:feat/input-group-addon
Open

Add new InputGroup compound component with Addon, Suffix, and Button support#249
pedromenezes1 wants to merge 22 commits intocloudflare:mainfrom
pedromenezes1:feat/input-group-addon

Conversation

@pedromenezes1
Copy link
Copy Markdown
Collaborator

@pedromenezes1 pedromenezes1 commented Mar 18, 2026

New InputGroup compound component for building inputs with icons, addons, inline suffixes, and action buttons.

Includes comprehensive documentation page with demos and unit tests.

Features

  • Field Integration — InputGroup accepts label, description, error, required, and labelTooltip props directly; automatically wraps in Field when label is provided
  • Addons — Place icons or text before/after the input using align="start" or align="end"
  • Compact Button — Small button inside an Addon for secondary actions (copy, clear, toggle visibility)
  • Action Button — Full-height flush button as a direct child for primary actions (submit, search)
  • Inline Suffix — Text that flows seamlessly next to the typed value (e.g., .workers.dev); input width adjusts automatically as user types
  • Size Variants — xs, sm, base, lg sizes cascade to all children via context
  • Error State — Error flows through context; InputGroup.Input auto-sets aria-invalid="true" when error is present
  • Disabled State — disabled prop disables all interactive children

Sub-components

Component Description
InputGroup Root container; provides context and accepts Field props
InputGroup.Input Styled input; inherits size, disabled, error from context
InputGroup.Addon Container for icons, text, or compact buttons; align="start" (default) or align="end"
InputGroup.Button Full-height button (direct child) or compact button (inside Addon)
InputGroup.Suffix Inline text suffix with automatic width measurement

Usage Examples

// With Field props (label, description, error, tooltip)
<InputGroup
  label="Email"
  description="We'll never share your email"
  error={{ message: "Invalid email", match: "typeMismatch" }}
  labelTooltip="Used for account recovery"
>
  <InputGroup.Input type="email" />
  <InputGroup.Addon align="end">@example.com</InputGroup.Addon>
</InputGroup>

// Inline suffix with auto-measuring width
<InputGroup>
  <InputGroup.Input placeholder="my-worker" />
  <InputGroup.Suffix>.workers.dev</InputGroup.Suffix>
</InputGroup>

// Search with icon addon and action button
<InputGroup>
  <InputGroup.Addon><MagnifyingGlassIcon /></InputGroup.Addon>
  <InputGroup.Input placeholder="Search..." />
  <InputGroup.Button variant="primary">Search</InputGroup.Button>
</InputGroup>

// Password with compact toggle button inside addon
<InputGroup label="Password">
  <InputGroup.Input type={show ? "text" : "password"} />
  <InputGroup.Addon align="end">
    <InputGroup.Button variant="ghost" size="sm" onClick={toggle}>
      {show ? <EyeSlashIcon /> : <EyeIcon />}
    </InputGroup.Button>
  </InputGroup.Addon>
</InputGroup>

Caveats

  • InputGroup.Input omits label, description, error, size, and labelTooltip props — these are handled by the parent InputGroup
  • When using InputGroup.Suffix, the input width is measured dynamically via a hidden ghost element
  • InputGroup.Button renders differently based on placement: full-height flush when direct child, compact when inside Addon

Screenshots

Screenshot 2026-03-18 at 14 13 05 Screenshot 2026-03-18 at 14 13 10 Screenshot 2026-03-18 at 14 13 13
Screenshot 2026-03-18 at 14 13 15 Screenshot 2026-03-18 at 14 13 18 Screenshot 2026-03-18 at 14 13 21
Screenshot 2026-03-18 at 14 13 25
Screenshot 2026-03-18 at 14 13 39 Screenshot 2026-03-18 at 14 13 42 Screenshot 2026-03-18 at 14 13 46
Screenshot 2026-03-18 at 14 13 50 Screenshot 2026-03-18 at 14 13 53 Screenshot 2026-03-18 at 14 13 59
Screenshot 2026-03-18 at 14 14 02

@pedromenezes1 pedromenezes1 force-pushed the feat/input-group-addon branch from b348d28 to 86e9d5d Compare March 18, 2026 01:38
@pedromenezes1 pedromenezes1 changed the title fix(InputGroup): flush button properly covers container ring border feat(InputGroup): revamp component with new compound API Mar 18, 2026
@pedromenezes1 pedromenezes1 changed the title feat(InputGroup): revamp component with new compound API feat: new InputGroup compound component with Addon, Suffix, and Button support Mar 18, 2026
@pedromenezes1 pedromenezes1 changed the title feat: new InputGroup compound component with Addon, Suffix, and Button support Add new InputGroup compound component with Addon, Suffix, and Button support Mar 18, 2026
@pedromenezes1 pedromenezes1 force-pushed the feat/input-group-addon branch 17 times, most recently from 1f88e51 to ca11f35 Compare March 18, 2026 14:10
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 18, 2026

npm i https://pkg.pr.new/@cloudflare/kumo@249

commit: ddd73ec

@pedromenezes1 pedromenezes1 force-pushed the feat/input-group-addon branch from ca11f35 to 5c4d6f2 Compare March 18, 2026 14:12
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 18, 2026

Docs Preview

View docs preview

Commit: ddd73ec

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 18, 2026

Visual Regression Report

17 screenshot(s) with visual changes:

Button / Basic

183 px (0.18%) changed

Before After Diff
Before After Diff

Button / Variant: Secondary Destructive

1,122 px (1.11%) changed

Before After Diff
Before After Diff

Button / Loading State

203 px (0.2%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Actions

191 px (0.19%) changed

Before After Diff
Before After Diff

Dialog / Dialog Basic

192 px (0.19%) changed

Before After Diff
Before After Diff

Dialog / Dialog Alert

354 px (0.35%) changed

Before After Diff
Before After Diff

Dialog / Dialog Confirmation

707 px (0.7%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Select

244 px (0.24%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Combobox

130 px (0.13%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Dropdown

244 px (0.24%) changed

Before After Diff
Before After Diff

Dialog (Open)

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select With Field

633 px (0.54%) changed

Before After Diff
Before After Diff

Select / Select Custom Rendering

412 px (0.41%) changed

Before After Diff
Before After Diff

Select / Select Loading

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Multiple

801 px (0.79%) changed

Before After Diff
Before After Diff

Select / Select Complex

1,373 px (1.16%) changed

Before After Diff
Before After Diff

Select (Open)

324 px (0%) changed

Before After Diff
Before After Diff
14 screenshot(s) unchanged
  • Button / Variant: Primary
  • Button / Variant: Secondary
  • Button / Variant: Ghost
  • Button / Variant: Destructive
  • Button / Variant: Outline
  • Button / Sizes
  • Button / With Icon
  • Button / Icon Only
  • Button / Disabled State
  • Button / Title
  • Select / Select Basic
  • Select / Select Without Label
  • Select / Select Placeholder
  • Select / Select With Tooltip

Generated by Kumo Visual Regression

@mattrothenberg mattrothenberg force-pushed the feat/input-group-addon branch from 0caf233 to 5bbecc8 Compare March 18, 2026 14:40
@pedromenezes1 pedromenezes1 force-pushed the feat/input-group-addon branch 4 times, most recently from 09d553b to 5917fe8 Compare March 23, 2026 22:15
pedromenezes1 and others added 19 commits March 30, 2026 18:25
…on support

New InputGroup compound component for building inputs with icons, addons, inline suffixes, and action buttons.

Features:
- Field Integration — Accepts label, description, error, required, and labelTooltip props
- Addons — Place icons or text before/after the input using align="start" or align="end"
- Compact Button — Small button inside an Addon for secondary actions
- Action Button — Full-height flush button as a direct child for primary actions
- Inline Suffix — Text that flows seamlessly next to the typed value
- Size Variants — xs, sm, base, lg sizes cascade to all children via context
- Error State — Error flows through context; InputGroup.Input auto-sets aria-invalid
- Disabled State — disabled prop disables all interactive children

Sub-components:
- InputGroup — Root container; provides context and accepts Field props
- InputGroup.Input — Styled input; inherits size, disabled, error from context
- InputGroup.Addon — Container for icons, text, or compact buttons
- InputGroup.Button — Full-height button (direct child) or compact button (inside Addon)
- InputGroup.Suffix — Inline text suffix with automatic width measurement

Includes comprehensive documentation page with demos and unit tests.
…on support

New InputGroup compound component for building inputs with icons, addons, inline suffixes, and action buttons.

Features:
- Field Integration — Accepts label, description, error, required, and labelTooltip props
- Addons — Place icons or text before/after the input using align="start" or align="end"
- Compact Button — Small button inside an Addon for secondary actions
- Action Button — Full-height flush button as a direct child for primary actions
- Inline Suffix — Text that flows seamlessly next to the typed value (uses CSS field-sizing)
- Size Variants — xs, sm, base, lg sizes cascade to all children via context
- Error State — Error flows through context; InputGroup.Input auto-sets aria-invalid
- Disabled State — disabled prop disables all interactive children

Sub-components:
- InputGroup — Root container; provides context and accepts Field props
- InputGroup.Input — Styled input; inherits size, disabled, error from context
- InputGroup.Addon — Container for icons, text, or compact buttons
- InputGroup.Button — Full-height button (direct child) or compact button (inside Addon)
- InputGroup.Suffix — Inline text suffix with CSS-based automatic width sizing

Includes comprehensive documentation page with demos and unit tests.
- Suppress outline on inner input with outline-none!
- Add has-[:focus-visible]:outline-auto to container in grouped mode
- When inner input receives keyboard focus, the native focus ring appears on the entire InputGroup container
- Uses browser's native -webkit-focus-ring-color for consistent appearance
- Fix addon padding with explicit pl-*/pr-* classes (Tailwind JIT compatible)
- Add type="button" to flush Button to prevent form submission
- Add forwardRef to InputGroup.Input for ref forwarding
- Add tests for error state (aria-invalid) and Field integration
- Update component registry with ring-kumo-line color token
- Revert flush button to render inside container (fixes focus ring issues)
- Make addon padding symmetric (start and end now match)
- Add forwardRef to InputGroup.Button for consistency
- Pass disabled state from context to Button
- Add tests for button variant and disabled state
- Fix stale JSDoc comment
- Use ring-0 instead of ring ring-transparent for clarity
- Add data-slot="input-group" for CSS targeting
- Remove overflow-visible rule for buttons (breaks clipping)
- Remove inline ring-transparent (was hiding focus ring)
- Add outline-none to inner input (prevent double outline)
- Let kumo-binding.css handle native outline
Match the docs format change from main (commit adfc9ab)
- Remove dead has-[[data-slot=input-group-button]] CSS selectors from context.ts
- Add forwardRef to InputGroup.Input, InputGroup.Addon, and InputGroup.Suffix
- Export missing InputGroupButtonProps from barrel index
- Delete accidental empty dist.md from kumo-docs-astro
… element

- Change InputGroup container from <div> to <label>, eliminating ~47 lines
  of imperative JS (useEffect, useRef, useCallback, useId, setTimeout,
  handleMouseDown) in favor of native HTML click-to-focus behavior
- Remove inputId from InputGroup context (no longer needed)
- Remove manual id assignment from InputGroup.Input (label handles focus
  natively, Field's htmlFor handles label association)
- Update InputGroupRootProps to extend ComponentPropsWithoutRef<"label">
- Rewrite test file to match actual demo patterns: remove 2 tests for
  non-existent combinations, update all 31 remaining tests
…adata

Add size and focusMode variants to InputGroup. The focusMode variant
(container | individual) controls whether a single focus ring wraps the
whole group or each interactive child shows its own indicator.

- Populate KUMO_INPUT_GROUP_VARIANTS with size and focusMode definitions
- Scope container focus-ring CSS to data-focus-mode="container"
- Conditionally apply focus styles in InputGroup.Input based on mode
- Add additional props metadata for Input, Addon, Button, Suffix subs
- Support sub-component prop injection in registry codegen
- Add InputGroup.Button PropsTable to docs
…e-colors rule

This fixes a false positive where focus-visible:outline-auto was flagged as an invalid color token. outline-auto is a CSS style value (outline-style: auto), not a color.
@pedromenezes1 pedromenezes1 force-pushed the feat/input-group-addon branch from be46320 to 43a1be4 Compare March 30, 2026 17:26
@stritt
Copy link
Copy Markdown
Collaborator

stritt commented Mar 31, 2026

@bonk review this pr

@pedromenezes1
Copy link
Copy Markdown
Collaborator Author

@bonk review this pr

I think it's /bonk

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: InputGroup Compound Component

This is a well-designed and comprehensive addition to the component library. The compound component pattern is cleanly implemented, and the Field integration is a nice ergonomic improvement. The test coverage is thorough and the documentation is excellent.

Summary

Strengths:

  • Clean compound component architecture with proper context handling
  • Thoughtful Field integration for seamless form building
  • Excellent test coverage (531 lines)
  • Well-documented with comprehensive demos and MDX page
  • CSS-first sizing system with static Tailwind classes (avoiding JIT detection issues)
  • Proper a11y handling (aria-invalid from context, tooltip→aria-label derivation)

Issues to address

  1. Empty file committed: packages/kumo-docs-astro/dist.md is an empty file that appears to have been accidentally committed. The dist/ directory is typically gitignored for build outputs. Please remove this file.

  2. Changeset update: Per the earlier review comment from @pedromenezes1 - the changeset may need to be updated to reflect the final state of the proposal. Please confirm if the changeset description accurately represents the shipped component API.

Suggestions (non-blocking)

See inline comments for minor code quality suggestions:

  • input-group-addon.tsx: The string check child.type === "button" will never match
  • input-group-button.tsx: The variant/shape props are accepted but always overwritten
  • Consider adding dev-mode warnings when sub-components are used outside InputGroup context

Overall this is excellent work and ready to merge once the empty file is removed.

if (!isValidElement(child)) return child;
const props = child.props as { size?: unknown };
if (props.size !== undefined) return child;
if (child.type === "button" || child.type === Button) return child;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check child.type === "button" will never match since React elements from JSX have the component function as their type, not the string "button".

The child.type === Button check is the one doing the actual work here. Consider simplifying:

Suggested change
if (child.type === "button" || child.type === Button) return child;
if (child.type === Button) return child;

disabled={isDisabled}
aria-label={tooltipAriaLabel}
{...props}
variant="ghost"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variant and shape props are accepted in the type definition (lines 7-9) but are always overwritten here with variant="ghost". This could be confusing for consumers who might try to pass a different variant.

Consider either:

  1. Removing variant and shape from InputGroupButtonProps entirely
  2. Or documenting that only ghost is supported

Minor issue - current behavior works correctly.

}

export const InputGroupContext = createContext<InputGroupContextValue | null>(
null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good use of null as the default context value - this allows detection of sub-components used outside InputGroup.

Consider adding a dev-mode warning when useContext(InputGroupContext) returns null inside sub-components (similar to the accessible name warnings used elsewhere in the codebase). This would help developers catch misuse early.

// In each sub-component:
if (process.env.NODE_ENV === 'development' && !context) {
  console.warn('InputGroup.X must be used within InputGroup');
}


const container = (
<InputGroupContext.Provider value={contextValue}>
<label
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever use of <label> as the container element - clicking anywhere in the InputGroup will focus the input without needing JS event handlers. Nice native behavior.

One consideration: when the InputGroup has no label prop but is wrapped externally by a Field, there could be nested labels. Might be worth documenting this edge case in the component JSDoc.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Mar 31, 2026

I've submitted a comprehensive code review for PR #249. Here's a summary of my review:

Review Summary

Overall assessment: This is a well-designed InputGroup compound component that's ready to merge with a couple minor fixes.

Issues to address:

  1. Empty file committed: packages/kumo-docs-astro/dist.md should be removed (accidentally committed empty file)
  2. Changeset needs confirmation: Per the earlier review, the changeset should reflect the final API

Inline comments provided on:

  • input-group-addon.tsx:40 - Unreachable string check for button type
  • input-group-button.tsx:56 - Props accepted but always overwritten
  • context.ts:121 - Suggestion to add dev warnings for misuse
  • input-group.tsx:115 - Noting the clever <label> container pattern

Strengths highlighted:

  • Clean compound component architecture
  • Thorough Field integration
  • Excellent test coverage (531 lines)
  • Good accessibility handling
  • Well-documented with demos and MDX

github run

@stritt
Copy link
Copy Markdown
Collaborator

stritt commented Mar 31, 2026

a few things bonk didn't catch:

  • re: the nested <label> issue... when you pass a label prop, the <label> container renders inside Field.Label which also renders a <label>. checked the base-ui source, FieldLabel always renders a native <label> with htmlFor. nested labels are invalid html and AT behavior is undefined
  • in kumo-binding.css the outline: auto line before outline: solid 2px -webkit-focus-ring-color is dead code, second line immediately overrides it
  • data-focus-scope="proposed" in InputGroupIconsDemo looks like leftover scaffolding
  • focusMode is @internal in props but fully documented in the public KUMO_INPUT_GROUP_VARIANTS export — should pick one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants