Skip to content

feat(components): add labelPosition to Switch - #1996

Open
hsadhvani wants to merge 3 commits into
mainfrom
hsadhvani/switch-label-position
Open

feat(components): add labelPosition to Switch#1996
hsadhvani wants to merge 3 commits into
mainfrom
hsadhvani/switch-label-position

Conversation

@hsadhvani

@hsadhvani hsadhvani commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Switch renders [track][children], so a visible label always sits after the toggle and there's no way to put the label first without leaving the switch. Consumers that want label-before-toggle currently either override flex-direction locally (see launchdarkly/gonfalon#69412) or render the text as a sibling element with aria-label on the switch — the latter drops the text out of the <label>, so clicking the words no longer toggles and the visible text is no longer the accessible name.

This adds a labelPosition variant, defaulting to end (current behavior):

<Switch labelPosition="start">AI assist</Switch>
.switch.labelStart {
  flex-direction: row-reverse;
}

The label stays children of the switch, so it remains inside the <label> — clickable and still the accessible name — and only the visual order changes. Track internals (On/Off labels, handle translation, compact) are untouched.

Naming: RAC's Switch has no equivalent prop, so this follows React Spectrum's labelPosition name with CSS-logical start/end values rather than inventing something new.

Screenshots (if appropriate):

New Storybook story Components/Forms/Switch → LabelStart covers it (Chromatic will diff it); to follow.

Testing approaches

  • pnpm vitest run packages/components/__tests__/Switch.spec.tsx — 7 passed, including new cases for the default (label after track) and labelPosition="start".
  • pnpm typecheck, pnpm oxlint:js:path on the changed files, pnpm fmt:check, pnpm lint:css:path packages/components/src/styles/Switch.module.css.
  • Changeset included (minor on @launchpad-ui/components).

Follow-up: once this is released, gonfalon can drop its local row-reverse override in favor of the prop.

Link to Devin session: https://app.devin.ai/sessions/6458b07f375e44219f97c3a2beb73491
Requested by: @hsadhvani


Note

Overview
Adds labelPosition (start | end, default end) on Switch so visible children can render before the track without leaving the <label>.

Layout uses a labelStart class that sets order: 1 on the track (not row-reverse), keeping the label clickable and the accessible name aligned with the visible text. On/Off track internals are unchanged.

Tests, Storybook stories, and a minor changeset for @launchpad-ui/components are included.

Reviewed by Cursor Bugbot for commit 2fa9404. Bugbot is set up for automated code reviews on this repo. Configure here.

Co-Authored-By: Harsh Sadhvani <hsadhvani@launchdarkly.com>
@hsadhvani hsadhvani self-assigned this Aug 12, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@changeset-bot

changeset-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2fa9404

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@launchpad-ui/components Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@hsadhvani
hsadhvani marked this pull request as ready for review August 12, 2026 17:39
@hsadhvani
hsadhvani requested a review from a team as a code owner August 12, 2026 17:39
@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown
yarn add https://pkg.pr.new/@launchpad-ui/components@1996.tgz
yarn add https://pkg.pr.new/@launchpad-ui/icons@1996.tgz
yarn add https://pkg.pr.new/@launchpad-ui/tokens@1996.tgz

commit: 2fa9404

@erangeles

Copy link
Copy Markdown
Contributor

@hsadhvani I was wondering if we could we support this through composition rather than adding a labelPosition prop? For example, could the label and switch control be exposed as composable parts so consumers can choose their order while preserving the accessible labeling relationship?

@devin-ai-integration

Copy link
Copy Markdown
Contributor

@erangeles good call — composition is doable, and upstream is already heading there.

RAC 1.19 (the version we're on) deprecates Switch in favor of SwitchField + SwitchButton, where the consumer orders the parts and RAC keeps the label/input association:

<SwitchField>
  <Label>AI assist</Label>
  <SwitchButton />
</SwitchField>
// react-aria-components 1.19.0, dist/types/src/Switch.d.ts
/** @deprecated Use SwitchField + SwitchButton instead. */
export declare const Switch: ...

Doing this properly in Launchpad means a new SwitchField/SwitchButton pair (track/handle/On-Off markup moves into SwitchButton, styles keyed off the field wrapper) plus a deprecation path for the ~existing Switch call sites — bigger than this PR, but the right destination.

Composition within today's Switch isn't as clean: the track is rendered by Switch itself, so exposing it as a part (<Switch><SwitchTrack /></Switch>) would either break every existing <Switch>Label</Switch> call site or need child-type sniffing to decide whether to auto-render the track.

So: do you want me to (a) land labelPosition as-is now as a stopgap and file the SwitchField/SwitchButton migration separately, or (b) drop this PR and do the SwitchField/SwitchButton composition here? Happy to do (b) — just want your call since it touches the component's public shape and existing consumers.

Context: this is unblocking a Gonfalon experiment form where the "AI assist" label should read left of its toggle (launchdarkly/gonfalon#69412), currently shipping a local flex-direction: row-reverse override.

devin-ai-integration Bot and others added 2 commits August 12, 2026 17:52
Co-Authored-By: Harsh Sadhvani <hsadhvani@launchdarkly.com>
Co-Authored-By: Harsh Sadhvani <hsadhvani@launchdarkly.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

Runtime verification — labelPosition="start" (Storybook)

Tested in Storybook, story group Components/Forms/Switch.

Note the implementation changed since the first commit: flex-direction: row-reverse right-aligned the whole switch (.switch is a display: flex <label>, so full-width — reversing the main axis moves main-start to the right edge). It now reorders only the track (.switch.labelStart { & .track { order: 1 } }), leaving the main axis and any consumer justify-content intact.

Label renders before the track, and the switch stays flush left

Default (WithChildren) New (LabelStart)

Both start at the same left edge of the canvas — full window: LabelStart / WithChildren.

Clicking the label text still toggles (the point of keeping the label as children): green track, On moves to the track's left, handle animates right, label stays before the track.

Compact variant (switchLabels={false} + labelPosition="start")

Added as a permanent LabelStartHideLabels story.

Unselected Selected
Regressions checked

WithChildren selected state and the existing Hide Labels compact story render as before.

Recording: https://app.devin.ai/attachments/23a97943-d7fc-48da-94fd-7fa5c4c15383/rec-cfa879e9-9c30-40eb-a620-0dac90f1160a-edited.mp4

Not covered: flipping labelPosition live via the Controls panel (URL args were inert in this Storybook build); the per-story comparison covers the same behavior.

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.

3 participants