Skip to content

feat: adopt t-shirt size prop across components#796

Draft
cb-ekuersch wants to merge 19 commits into
masterfrom
cds-t-shirts
Draft

feat: adopt t-shirt size prop across components#796
cb-ekuersch wants to merge 19 commits into
masterfrom
cds-t-shirts

Conversation

@cb-ekuersch

@cb-ekuersch cb-ekuersch commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Rolls out the cross-platform t-shirt size prop across CDS components, deprecating the legacy compact boolean in favor of the new sizing API. Each component's per-size styles are derived from its Figma component set and applied consistently across web and mobile. Backward compatibility is preserved throughout: compact keeps working exactly as before (size wins when both are set), no fixed heights are introduced (except Slide Button, which is inherently fixed-height by design sign-off), and no public exports are renamed or dropped. compact is deprecated with @deprecationExpectedRemoval v10.

Delivered as 7 verified commits (unit tests + lint + typecheck + format green for each):

Component(s) Issue Commit
Button CDS-2168 9292808
Icon Button CDS-2445 3b244bb
Search Input CDS-2450 0d2bf44
Select (alpha) CDS-2449 54790be
Slide Button CDS-2448 4d1eca3
Date Picker (+ DateInput) CDS-2451 0ea0327
Input Chip · Select Chip · TabbedChips CDS-2223 · CDS-2224 · CDS-2173 016d95e

Notable decisions:

  • Chips: size was added to the shared base Chip and compact deprecated across the whole chip family; Input Chip, Select Chip, and TabbedChips inherit from it (union xs | s, default s). cds-common getMediaChipSpacingProps is size-aware but backward compatible.
  • Date Picker: size added to both DatePicker and DateInput (web + mobile); label placement is decoupled from size via TextInput's density logic (size="l" stacks inside labels vertically).
  • Slide Button: a new medium (48px) constant was added; m reuses l's handle icon size.

Linear Issues

Deferred (not in this PR)

These hit hard stops during planning and need design/product input before implementation:

  • CDS-2447 — Button Group: Figma component set has no size variant; it is a layout-only wrapper that delegates sizing to child Buttons. Needs design to add a size variant or a decision to forward size to children.
  • CDS-2446 — Icon Button Group: no size variant in Figma and no IconButtonGroup component exists in code (Code Connect maps it to plain ButtonGroup). Needs the ticket reframed onto a real target component.

Next steps

Intentionally deferred from this PR to keep scope focused on the size API + component/stories work:

  • Docs site (apps/docs): Update component examples to document the new size prop and remove/replace deprecated compact examples for each rolled-out component.
  • Figma Code Connect (__figma__ templates): Refresh Code Connect mappings to include the new size variant property for each rolled-out component.

Approach

This work was fully executed by agents orchestrated by a Claude Dynamic Workflow. The workflow is comprised of 3 phases: plan, review and implement, with the plan phase being fully parallel. To define a clear, repeatable workflow for the plan agents to follow, an agent skill was created (cds-tshirt-sizes) and kept in this PR for illustrative purposes. We can remove this skill in a later PR since it is specific to only the work in this PR.

Input prompt:

@component-sizes.md

  Create a workflow that systematically implements the new `size` prop for all the components listed as linear issues in the attached md file. The workflow should roughly follow these phases:

  1. Break the components in the file up into parallel agents and use the cds-tshirt-sizes skill for each to analyze the component design, current implementation and formulate a plan.
  2. Review the plans:
    - check for general accurateness and feasibility
    - check for overlap - if any components' plan overlap in the files they will need to touch they cannot be run in parallel and should be joined together in a single plan that handles the multiple componetns in one pass
    - check for major open questions - if a component's plan has genuine implementation concerns that require user intput raise this to the user and provide instructions for how they can resuem the work for that component once the information has been provided by whatever means is the most convenient
  3. Implement the plans: once the plans have been deemed ready execute all the plans in sequence - commiting the changes once all the verifications (unit tests, lint, format, typecheck) have passed
  4. Finally, create a draft PR with all the commits that you were able to complete. Add a link to each completed component's linear issue in a section of the PR descrioption body

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS 🟡 See below

🟡 CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 🟡 0/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

cb-ekuersch and others added 10 commits July 15, 2026 14:49
Add xs/s/m/l size prop (default l) to web + mobile Button, deprecate compact at v10 with size wins over compact, plus stories, docs, and Figma Code Connect updates.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Add xs/s/m/l size prop to web and mobile IconButton, deprecate compact at v10, refresh stories/docs/Figma Code Connect while preserving byte-for-byte backward compatibility.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Plumb size (s/m/l) through SearchInput to TextInput on web + mobile, deprecate compact, and remove web's fixed-height computation so height stays content-derived.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Add size (s/m/l) to the alpha Select on web + mobile, resolving compact to size='s' for backward compatibility, deprecate compact for v10, and refresh stories, docs, and Figma Code Connect.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Add s/m/l size prop (default l) routed to the existing fixed-height mechanism, deprecate compact in favor of size="s" at v10, and refresh docs, stories, and Figma Code Connect.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Thread size (s/m/l, default l) through DatePicker and DateInput on web and mobile, reusing TextInput density, and deprecate compact at v10 (size wins over compact).

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…s (CDS-2224, CDS-2223, CDS-2173)

InputChip: inherits size from the shared base Chip; compact deprecated at v10.
SelectChip: threads size through the alpha control into MediaChip; compact deprecated at v10.
TabbedChips: forwards resolved size to each tab chip; compact deprecated at v10.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
expect(screen.getByRole('button')).toBeTruthy();
});

it('renders with size="xs"', () => {

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.

test cases that if something renders aren't valuable and should be removed, across all unit test files. A more useful test would be to try and measure the derived height to see if it matches the expectation based on the spec in figma. This may not be possible (or easy enough to justify) for components without a forwarded ref or if that ref is assigned the wrong element

* @deprecated Use `size="xs"` instead. This will be removed in a future major release.
* @deprecationExpectedRemoval v10
*/
compact?: boolean;

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.

do we really need to override compact and size from Chip? Chip should have both these changes and I think the jsdocs there should be generic enough to work in this context - let's rethink this

invertColorScheme={invertColorScheme}
maxWidth={maxWidth}
numberOfLines={numberOfLines}
{...props}

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.

i dont think we should have moved the props spread to the top, this can result in different behavior than before

maxWidth,
displayValue,
}: SelectControlProps<Type, SelectOptionValue> &
}: Omit<SelectControlProps<Type, SelectOptionValue>, 'size' | 'compact'> &

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 feels werid to omit these but i guess it is because they are already coming along for the ride on SelectChipBaseProps - aren't there other props that are on both since i imagine much of the select control's props come from the select itselft

// The `s` size collapses the label into the input row (the legacy compact layout).
const isCompactLayout = resolvedSize === 's';
// When using the compact layout, labelVariant is ignored
const labelVariant = isCompactLayout ? undefined : labelVariantProp;

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 would be true if compact is true however isCompactLayout can be true is size is equal to s as far as I can tell. When using size it is totally valid for label to be set to etierh inside or outside.


export type ButtonSize = 'xs' | 's' | 'm' | 'l';

type ButtonSizeConfig = {

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.

no - this should Pick from the ButtonBaseProps which should be the authoritative source

},
} as const satisfies Record<ButtonSize, ButtonSizeConfig>;

const defaultButtonSize: ButtonSize = 'l';

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.

why? just inline this during prop destructuring - same goes for all other components we changed

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.

nevermind - i think we want to understand if size is undefined or not while compact is still not fully removed


type IconButtonFeedback = 'light' | 'normal';

type IconButtonSizeConfig = {

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.

no - use IconButtonBaseProps

* IconButton is a special case: because `compact` defaults to `true`, an IconButton
* with no `size` (and no `compact={false}`) resolves to `s`, NOT `l`. An explicit
* `size` always wins over `compact` when both are provided.
* @default 's' (because `compact` defaults to `true`; resolves to `l` only when `compact={false}`)

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.

i think we should document as l being the default as that is what it will be when compact is fully removed


export type SlideButtonSize = 's' | 'm' | 'l';

type SlideButtonSizeConfig = {

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.

no - use properties from SlideButtonBasePriops

cb-ekuersch and others added 7 commits July 16, 2026 14:09
… padding (web + mobile)

Rework how TextInput derives and applies its density/padding, kept in sync across web and mobile:

- Deliver field padding via InputStack's styles.input (the field container that
  surrounds start/label/input/end and sits inside the border) instead of
  distributing padding across the input and start/end wrappers. Fixes outer
  padding landing on the wrong side of start/end nodes; input now carries none.
- Split the old single density hook into two focused hooks:
  - useTextInputPlacement -> returns just the label placement
    ('outside' | 'inside-horizontal' | 'inside-vertical')
  - useTextInputDensity -> returns { contentPadding, contentGap }
  Both take already-resolved compact/size; TextInput owns the "size wins over
  compact" precedence (consistent with the rest of CDS).
- Collapse the redundant 'legacy-compact' placement into 'inside-horizontal'
  driven by the compact flag.
- Padding fixes: legacy compact field height 42px (start-slot label paddingY 0);
  vertically-stacked inside label (size l) 58px via symmetric space-0.75.
- NativeInput: deprecate compact/containerSpacing; default padding handled by the
  container, input padding reset to 0.
- Update tests + NativeInput story accordingly.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
The mobile NativeInput set minHeight to the font's lineHeight token but never set
lineHeight itself, so input text rendered at the font's natural line height
(~19.7px for 16px body) instead of the theme's 24px. Web gets lineHeight from the
Box font token, so its input line box is already 24px.

This left mobile inputs a few px shorter than web (and design) for the same size,
and the minHeight floor only engaged when the input had no padding. Setting
lineHeight: theme.lineHeight[font] aligns the mobile input line box with web.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…tainer

We initially applied the whole content-padding box to the InputStack field
container via styles.input (all four edges on the container). That regressed the
field height whenever a start/end adornment is taller than the text line: with
vertical padding on the container, the adornment becomes the tallest child and the
container's top/bottom padding stacks *around* it. SearchInput (a ~44px icon
button) grew from ~58px to ~74px at size l.

So we backed off using styles.input for the *vertical* padding and split the box
by axis:

  - Horizontal (left/right) + the inter-slot gap stay on the container via
    styles.input — this is what correctly hugs the field edges and separates the
    start / input / end slots (and fixed the original "outer padding trapped on the
    wrong side of an adornment" bug).
  - Vertical (top/bottom) moves back onto the content — the input, and for a
    stacked inside-vertical label its top band. The padded input now drives the
    field height, so a tall adornment centers within it instead of inflating it.

Also documents the model on ContentPadding, names the vertical-padding values in
TextInput for readability, and updates the affected tests. Web and mobile kept in
sync.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…put alignment

Strip baked-in padding from InputIcon/InputIconButton so field density owns
spacing, and on mobile keep vertical sizing on a wrapper View so RN TextInput
does not mis-center glyphs or stretch end icons to the suffix line box.

Co-authored-by: Cursor <cursoragent@cursor.com>
…sition

Consolidate one-off *Size.stories into each component's main stories, add a
cross-control SizeComposition story for row alignment, and fix DateInput
helperText="" plus DatePicker popover offset when helper text is suppressed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restore apps/docs examples and __figma__ templates to master so this PR
stays focused on component size API + stories. Those follow-ups will land
in separate PRs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants