Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/input-size-variants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@launchpad-ui/components': minor
---

Add a `size` prop to `Input` with `small` (24px tall) and `medium` (32px tall, the default) variants, matching the corresponding `Button` sizes so inputs and buttons align when placed side by side. `small` uses 11px text to match `Button`'s small height.
6 changes: 3 additions & 3 deletions packages/components/src/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useLPContextProps } from './utils';

const groupStyles = cva(styles.group);

interface GroupProps extends AriaGroupProps, InputVariants {
interface GroupProps extends AriaGroupProps, Omit<InputVariants, 'size'> {
ref?: Ref<HTMLDivElement>;
}

Expand All @@ -27,11 +27,11 @@ const GroupContext = createContext<ContextValue<GroupProps, HTMLDivElement>>(nul
*/
const Group = ({ ref, ...props }: GroupProps) => {
[props, ref] = useLPContextProps(props, ref, GroupContext);
const { variant = 'default' } = props;
const { variant = 'default', ...rest } = props;

return (
<AriaGroup
{...props}
{...rest}
Comment thread
zakk-verrilli-ld marked this conversation as resolved.
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
cx(inputStyles({ variant }), groupStyles({ ...renderProps, className })),
Expand Down
17 changes: 13 additions & 4 deletions packages/components/src/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@ import { useLPContextProps } from './utils';

const inputStyles = cva(styles.base, {
variants: {
size: {
small: styles.small,
medium: null,
},
variant: {
default: styles._default,
minimal: styles.minimal,
},
},
defaultVariants: {
size: 'medium',
variant: 'default',
},
});

interface InputVariants extends VariantProps<typeof inputStyles> {}
interface InputProps extends AriaInputProps, InputVariants {
Comment thread
cursor[bot] marked this conversation as resolved.
// `size` is omitted from the underlying React Aria props (the native numeric
// `size` HTML attribute) so it can be redefined as the design-system size scale.
interface InputProps extends Omit<AriaInputProps, 'size'>, InputVariants {
ref?: Ref<HTMLInputElement>;
}

Expand All @@ -37,14 +44,16 @@ const InputContext = createContext<ContextValue<InputProps, HTMLInputElement>>(n
*/
const Input = ({ ref, ...props }: InputProps) => {
[props, ref] = useLPContextProps(props, ref, InputContext);
const { variant = 'default' } = props;
// Pull the style-only variants out so they aren't forwarded to the DOM
// `<input>` (React Aria's Input spreads unknown props through unfiltered).
const { size = 'medium', variant = 'default', ...rest } = props;

return (
<AriaInput
{...props}
{...rest}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
inputStyles({ ...renderProps, variant, className }),
inputStyles({ ...renderProps, size, variant, className }),
)}
/>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/components/src/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useLPContextProps } from './utils';

const textAreaStyles = cva(styles.area);

interface TextAreaProps extends AriaTextAreaProps, InputVariants {
interface TextAreaProps extends AriaTextAreaProps, Omit<InputVariants, 'size'> {
ref?: Ref<HTMLTextAreaElement>;
}

Expand All @@ -27,11 +27,11 @@ const TextAreaContext = createContext<ContextValue<TextAreaProps, HTMLTextAreaEl
*/
const TextArea = ({ ref, ...props }: TextAreaProps) => {
[props, ref] = useLPContextProps(props, ref, TextAreaContext);
const { variant = 'default' } = props;
const { variant = 'default', ...rest } = props;

return (
<AriaTextArea
{...props}
{...rest}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
cx(inputStyles({ variant }), textAreaStyles({ ...renderProps, className })),
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/styles/Group.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

& input[data-rac],
[role='presentation'][data-rac] {
min-height: 0;
Comment thread
zakk-verrilli-ld marked this conversation as resolved.
padding: 0;
outline: none;
border: none;
Expand Down
14 changes: 10 additions & 4 deletions packages/components/src/styles/Input.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
composes: field from './base.module.css';
font: var(--lp-text-body-2-regular);
color: var(--lp-color-text-ui-primary-base);
padding: 5px var(--lp-spacing-300);
min-height: var(--lp-size-32);
padding-inline: var(--lp-spacing-300);
border-radius: var(--lp-border-radius-medium);
border-width: var(--lp-border-width-200);
border-style: solid;
flex: 1;
min-width: 0;

&:is([data-focused], [data-focus-within]) {
outline: 2px solid var(--lp-color-shadow-interactive-focus);
outline-offset: -2px;
outline: var(--lp-size-2) solid var(--lp-color-shadow-interactive-focus);
outline-offset: calc(-1 * var(--lp-size-2));
z-index: 1;
}

Expand All @@ -30,14 +31,19 @@
}
}

.small {
font: var(--lp-text-small-1-regular);
min-height: var(--lp-size-24);
}

._default {
border-color: var(--lp-color-border-field-base);
background-color: var(--lp-color-bg-field-base);
}

.minimal {
background-color: inherit;
margin-left: calc(-1 * var(--lp-spacing-300) - 1px);
margin-left: calc(-1 * var(--lp-spacing-300) - var(--lp-size-1));

&:not([data-invalid]) {
border-color: transparent;
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/styles/TextArea.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.area {
flex: unset;
/* +1 to align with input elements who are granted a 1px useragent buffer */
padding-block: calc(var(--lp-size-4) + var(--lp-size-1));

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.

What is this about?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This replaces the 5px that Input used to apply - it was composed into TextArea. min-height on Input now replaces the math which included the padding.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The comment is about my amusement that browsers bless input with 1px padding
Screenshot 2026-07-23 at 8 30 35 PM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

context: this Multi Line story regressed due to the Input composition. I'm still testing with meticulous to not regress any of the composed fields (Group, Button, TextArea). Added some storybook examples for better coverage

}
58 changes: 58 additions & 0 deletions packages/components/stories/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/react-vite';

import { Input } from '../src/Input';

const meta: Meta<typeof Input> = {
title: 'Components/Forms/Input',
component: Input,
parameters: {
figma: {
design:
'https://www.figma.com/design/98HKKXL2dTle29ikJ3tzk7/%F0%9F%9A%80-LaunchPad?node-id=1-34456&m=dev',
},
docs: {
description: {
component: `
An input allows a user to input text.

Usually composed within a [TextField](/docs/components-forms-textfield--docs) alongside a [Label](/docs/components-content-label--docs) for an accessible field.
`,
},
},
},
};

export default meta;

type Story = StoryObj<typeof Input>;

export const Default: Story = {
args: {
'aria-label': 'Example',
defaultValue: 'Value',
},
};

/**
* Use the `size` prop to match the height of adjacent controls such as [Button](/docs/components-buttons-button--docs).
*/
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Input aria-label="Small" size="small" defaultValue="Small" />
<Input aria-label="Medium" size="medium" defaultValue="Medium" />
</div>
),
};

/**
* The `minimal` variant removes the resting border for use in denser or inline layouts.
*/
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Input aria-label="Default" variant="default" defaultValue="Default" />
<Input aria-label="Minimal" variant="minimal" defaultValue="Minimal" />
</div>
),
};
Loading