Skip to content

feat(Toggle): add new Toggle, ToggleGroup component#628

Open
noahchoii wants to merge 13 commits into
mainfrom
feature/vapor-185
Open

feat(Toggle): add new Toggle, ToggleGroup component#628
noahchoii wants to merge 13 commits into
mainfrom
feature/vapor-185

Conversation

@noahchoii

@noahchoii noahchoii commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

Summary by CodeRabbit

  • New Features

    • Added a new Toggle control with size, variant, disabled, and pressed states.
    • Added a new ToggleGroup control with separator support and multi-select behavior.
    • Exposed both components through the package’s main public entry points.
  • Tests

    • Added accessibility, keyboard, and interaction coverage for both Toggle and ToggleGroup.
  • Documentation

    • Added Storybook examples and test-bed demos for the new controls.
  • Toggle, ToggleGroup 컴포넌트를 추가했습니다.
  • Toggle과 ToggleGroup은 비교적 결합도가 높은 컴포넌트라 따로 PR을 분리하지는 않았습니다.
  • 파일이 많아져서 개발 문서는 PR 분리해서 반영해두겠습니다.

Checklist

Before submitting the PR, please make sure you have checked all of the following items.

  • The PR title follows the Conventional Commits convention. (e.g., feat, fix, docs, style, refactor, test, chore)
  • I have added tests for my changes.
  • I have updated the Storybook or relevant documentation.
  • I have added a changeset for this change. (e.g., for any changes that affect users, such as component prop changes or new features).
  • I have performed a self-code review.
  • I have followed the project's coding conventions and component patterns.

@noahchoii
noahchoii requested a review from MaxLee-dev as a code owner July 7, 2026 07:18
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7c6ee67

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

This PR includes changesets to release 1 package
Name Type
@vapor-ui/core 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

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vapor-ui Ready Ready Preview, Comment Jul 14, 2026 5:30am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@noahchoii, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 741657f8-1c8d-48b7-8d8b-319e1f62c44e

📥 Commits

Reviewing files that changed from the base of the PR and between 556211f and 7c6ee67.

⛔ Files ignored due to path filters (8)
  • packages/core/__tests__/screenshots/toggle--test-bed-1-chrome-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/toggle--test-bed-1-edge-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/toggle--test-bed-1-firefox-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/toggle--test-bed-1-safari-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-chrome-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-edge-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-firefox-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-safari-darwin-.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • packages/core/src/components/toggle/toggle.css.ts
  • packages/core/src/components/toggle/toggle.stories.tsx
  • packages/core/src/components/toggle/toggle.test.tsx
  • packages/core/src/components/toggle/toggle.tsx
📝 Walkthrough

Walkthrough

This PR introduces two new components, Toggle and ToggleGroup, in the @vapor-ui/core package. Each includes a vanilla-extract styling module, a component implementation supporting size/variant props and context sharing, Storybook stories, a test suite, barrel exports, and package-level index exports, plus corresponding changeset entries.

Changes

Toggle and ToggleGroup Components

Layer / File(s) Summary
Toggle styling and implementation
packages/core/src/components/toggle/toggle.css.ts, toggle.tsx
Introduces root recipe with size/variant/disabled/pressed styling and the Toggle component wrapping BaseToggle, resolving size/variant from props or toggle-group context.
Toggle exports, stories, tests
packages/core/src/components/toggle/index.ts, toggle.stories.tsx, toggle.test.tsx
Barrel export, Storybook Default/TestBed stories across sizes and states, and tests covering accessibility, ARIA pressed state, controlled/uncontrolled behavior, disabled state, and keyboard interaction.
ToggleGroup styling and implementation
packages/core/src/components/toggle-group/toggle-group.css.ts, toggle-group.tsx
Introduces root/separator recipes and ToggleGroupProvider/useToggleGroupContext, ToggleGroupRoot wrapping BaseToggleGroup, and ToggleGroupSeparator rendering a divider element.
ToggleGroup exports, stories, tests
packages/core/src/components/toggle-group/index.ts, index.parts.ts, toggle-group.stories.tsx, toggle-group.test.tsx
Re-exports ToggleGroup namespace and useToggleGroupContext, Storybook stories, and tests covering accessibility, click behavior, controlled value, multiple mode, disabled state, and keyboard navigation.
Package exports and changesets
packages/core/src/index.ts, .changeset/*.md
Adds toggle/toggle-group re-exports to the package index and adds minor-version changeset entries for both components.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: scope: docs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: adding new Toggle and ToggleGroup components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/vapor-185

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vapor-ui

vapor-ui commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

All tests passed!

Tests Passed Failed Duration Report
164 164 0 1m 27s Open report ↗︎

Click here if you need to update snapshots.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/src/components/toggle/toggle.tsx (1)

24-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer ?? over || for fallback.

sizeProp || contextSize / variantProp || contextVariant should use nullish coalescing since these are enum-string props; ?? more precisely expresses "use context only when the prop is unset" and avoids accidental fallback if a future variant value were falsy.

♻️ Proposed fix
-    const size = sizeProp || contextSize;
-    const variant = variantProp || contextVariant;
+    const size = sizeProp ?? contextSize;
+    const variant = variantProp ?? contextVariant;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toggle/toggle.tsx` around lines 24 - 25, Use
nullish coalescing in toggle.tsx for the fallback logic in the Toggle component
so only unset props fall back to context. Update the size and variant resolution
in the Toggle implementation to replace the current `sizeProp`/`contextSize` and
`variantProp`/`contextVariant` fallback behavior with `??`, keeping the logic in
the same place and preserving the existing prop/context precedence in the Toggle
component.

Source: Coding guidelines

packages/core/src/components/toggle-group/toggle-group.tsx (1)

27-46: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Context value doesn't adopt the recipe's resolved defaults; relies on two recipes' defaultVariants staying in sync.

variantsProps is derived purely from createSplitProps, so when a consumer doesn't set size/variant explicitly, ToggleGroupProvider provides { size: undefined, variant: undefined } to descendants instead of the resolved 'md'/'default'. This currently "works" only because toggle.css.ts's recipe happens to declare the same defaultVariants as toggle-group.css.ts's recipe, so Toggle's own styles.root({size: undefined, ...}) falls back to matching values. If either recipe's defaults diverge in the future, group and item styling will silently disagree.

Consider resolving variantsProps against the recipe's own defaults (e.g. merge with { size: 'md', variant: 'default' } or read root.defaultVariants /use a shared constant) before passing it to the context provider, so the context value reflects the actually-applied style rather than raw possibly-undefined props.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toggle-group/toggle-group.tsx` around lines 27 -
46, The ToggleGroup context is passing raw variant props with undefined values,
so descendants don’t receive the recipe’s resolved defaults and can drift from
the applied styling. Update ToggleGroupRoot to resolve variantsProps against the
ToggleGroup recipe defaults before giving it to ToggleGroupProvider, using the
same default source as toggle-group.css.ts (or a shared constant) so size and
variant always reflect the actual applied values. Keep the existing
createSplitProps flow, but merge in the defaultVariants before rendering
BaseToggleGroup and providing context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/components/toggle-group/toggle-group.css.ts`:
- Around line 6-32: ToggleGroup의 `root`와 `separator` 스타일이 `data-orientation`를
반영하지 않아 `orientation="vertical"`에서 레이아웃이 깨집니다. `toggle-group.css.ts`의
`root`/`separator` 정의에 `data-orientation="vertical"` 분기를 추가해 `root`는 세로 배치로
전환하고, `separator`는 축과 여백을 세로 모드에 맞게 바꾸세요. `componentRecipe`, `componentStyle`,
`root`, `separator`를 기준으로 수정하면 됩니다.

---

Nitpick comments:
In `@packages/core/src/components/toggle-group/toggle-group.tsx`:
- Around line 27-46: The ToggleGroup context is passing raw variant props with
undefined values, so descendants don’t receive the recipe’s resolved defaults
and can drift from the applied styling. Update ToggleGroupRoot to resolve
variantsProps against the ToggleGroup recipe defaults before giving it to
ToggleGroupProvider, using the same default source as toggle-group.css.ts (or a
shared constant) so size and variant always reflect the actual applied values.
Keep the existing createSplitProps flow, but merge in the defaultVariants before
rendering BaseToggleGroup and providing context.

In `@packages/core/src/components/toggle/toggle.tsx`:
- Around line 24-25: Use nullish coalescing in toggle.tsx for the fallback logic
in the Toggle component so only unset props fall back to context. Update the
size and variant resolution in the Toggle implementation to replace the current
`sizeProp`/`contextSize` and `variantProp`/`contextVariant` fallback behavior
with `??`, keeping the logic in the same place and preserving the existing
prop/context precedence in the Toggle component.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: edb8c3d3-215f-44ef-825d-eb3fd79bd519

📥 Commits

Reviewing files that changed from the base of the PR and between d90b538 and 556211f.

⛔ Files ignored due to path filters (8)
  • packages/core/__tests__/screenshots/toggle--test-bed-1-chrome-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/toggle--test-bed-1-edge-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/toggle--test-bed-1-firefox-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/toggle--test-bed-1-safari-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-chrome-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-edge-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-firefox-darwin-.png is excluded by !**/*.png
  • packages/core/__tests__/screenshots/togglegroup--test-bed-1-safari-darwin-.png is excluded by !**/*.png
📒 Files selected for processing (14)
  • .changeset/famous-bugs-guess.md
  • .changeset/tricky-files-cheat.md
  • packages/core/src/components/toggle-group/index.parts.ts
  • packages/core/src/components/toggle-group/index.ts
  • packages/core/src/components/toggle-group/toggle-group.css.ts
  • packages/core/src/components/toggle-group/toggle-group.stories.tsx
  • packages/core/src/components/toggle-group/toggle-group.test.tsx
  • packages/core/src/components/toggle-group/toggle-group.tsx
  • packages/core/src/components/toggle/index.ts
  • packages/core/src/components/toggle/toggle.css.ts
  • packages/core/src/components/toggle/toggle.stories.tsx
  • packages/core/src/components/toggle/toggle.test.tsx
  • packages/core/src/components/toggle/toggle.tsx
  • packages/core/src/index.ts

Comment on lines +6 to +32
export const root = componentRecipe({
base: {
display: 'inline-flex',
alignItems: 'center',
gap: vars.size.space['050'],

padding: vars.size.space['050'],

borderRadius: vars.size.borderRadius['400'],
border: '1px solid',
borderColor: vars.color.border.normal,
backgroundColor: vars.color.background.canvas[100],
},

defaultVariants: { size: 'md', variant: 'default' },
variants: {
size: { sm: {}, md: {}, lg: {}, xl: {} },
variant: { default: {}, accent: {} },
},
});

export const separator = componentStyle({
width: '0.0625rem',
alignSelf: 'stretch',
marginBlock: vars.size.space['050'],
backgroundColor: vars.color.border.normal,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'packages/core/src/components/toggle-group/*' && \
echo '--- outline: toggle-group.tsx ---' && \
ast-grep outline packages/core/src/components/toggle-group/toggle-group.tsx --view expanded && \
echo '--- outline: toggle-group.css.ts ---' && \
ast-grep outline packages/core/src/components/toggle-group/toggle-group.css.ts --view expanded && \
echo '--- search orientation usage ---' && \
rg -n --no-heading 'orientation|data-orientation|ToggleGroup' packages/core/src/components/toggle-group

Repository: goorm-dev/vapor-ui

Length of output: 8932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- toggle-group.tsx ---'
cat -n packages/core/src/components/toggle-group/toggle-group.tsx

echo '--- toggle-group.css.ts ---'
cat -n packages/core/src/components/toggle-group/toggle-group.css.ts

echo '--- toggle-group.test.tsx ---'
cat -n packages/core/src/components/toggle-group/toggle-group.test.tsx

echo '--- toggle-group.stories.tsx ---'
cat -n packages/core/src/components/toggle-group/toggle-group.stories.tsx

Repository: goorm-dev/vapor-ui

Length of output: 20641


🌐 Web query:

@base-ui/react toggle-group orientation data-orientation horizontal vertical documentation

💡 Result:

In the @base-ui/react library, the ToggleGroup component accepts an orientation prop that determines the component's orientation, which defaults to 'horizontal' [1][2]. The orientation prop accepts the values 'horizontal' or 'vertical' [1]. When this prop is set, the ToggleGroup component updates its rendered data-orientation attribute on the underlying DOM element (e.g., data-orientation="horizontal" or data-orientation="vertical") [3]. It is important to note that, as of the current implementation, setting the orientation prop primarily affects the data-orientation attribute and accessibility metadata [3]. It does not currently constrain keyboard navigation behavior; the component's composite root typically handles navigation across both axes regardless of the specified orientation [3]. To use the orientation prop: Option 1 Option 2

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

p = Path('packages/core/src/components/toggle-group/toggle-group.tsx')
text = p.read_text()
print('has aria-orientation strip:', 'aria-orientation={undefined}' in text)
print('base props extend full base ui props:', 'VaporUIComponentProps<typeof BaseToggleGroup, State>' in text)
PY

Repository: goorm-dev/vapor-ui

Length of output: 232


orientation="vertical" 지원이 빠져 있습니다. ToggleGroup.Root가 Base UI의 orientation prop을 그대로 받지만, root/separator 스타일은 data-orientation를 전혀 반영하지 않아 세로 모드에서도 가로 배치 + 세로 구분선으로 렌더링됩니다. data-orientation="vertical" 분기에서 flexDirection과 separator 축을 바꿔 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/toggle-group/toggle-group.css.ts` around lines 6
- 32, ToggleGroup의 `root`와 `separator` 스타일이 `data-orientation`를 반영하지 않아
`orientation="vertical"`에서 레이아웃이 깨집니다. `toggle-group.css.ts`의 `root`/`separator`
정의에 `data-orientation="vertical"` 분기를 추가해 `root`는 세로 배치로 전환하고, `separator`는 축과
여백을 세로 모드에 맞게 바꾸세요. `componentRecipe`, `componentStyle`, `root`, `separator`를
기준으로 수정하면 됩니다.

Comment on lines +109 to +122
describe('keyboard interaction', () => {
it('should toggle pressed state when Space key is pressed', async () => {
const rendered = render(<ToggleTest />);
const toggle = rendered.getByRole('button', { name: LABEL_TEXT });

await userEvent.tab();
await userEvent.keyboard('[Space]');

expect(toggle).toHaveAttribute('aria-pressed', 'true');

await userEvent.keyboard('[Space]');

expect(toggle).toHaveAttribute('aria-pressed', 'false');
});

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.

혹시 키보드 인터렉션 space만 추가하신 이유가 있을까요? enter로도 가능해야 할 것 같은데 테스트케이스에는 없어서 추가 제안드립니다.!

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.

엇 사실 Space를 입력했을 때와 다르게 Enter를 입력했을 때는 hover 같은 상호작용 이벤트가 발생하지 않아서 이벤트가 발생하지 않는다고 생각했네요.. 케이스는 추가해두겠습니다!

defaultVariants: { size: 'md', variant: 'default' },
variants: {
size: { sm: {}, md: {}, lg: {}, xl: {} },
variant: { default: {}, accent: {} },

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.

여기 variant가 들어간 이유가 있을까요? 시안에는 variant관련 속성이 없어서 여쭤봅니다.!

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.

아 이건 다른 Group 컴포넌트들처럼 그룹 전체에 variant를 적용하는 케이스를 추가해둔건데, 피그마에 반영 요청을 드리는 것을 잊었네요..! 디자이너분께 따로 요청 드리겠습니다!

Comment on lines +17 to +20
variant: {
control: { type: 'inline-radio' },
options: ['default', 'accent'],
},

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.

이 속성 추가시 모든 toggle에 동일한 값이 들어가는 것 같은데, 이렇게 모든 toggle에 대해 일괄 적용해야 하는 케이스가 있었을까요??

Comment on lines +30 to +36
const children = useRenderElement({
render: childrenRender,
props: {
'aria-hidden': 'true',
className: styles.icon,
},
});

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.

이건 단순 질문입니다.!

이렇게 하면 aria-hidden은 svg 요소 뿐만 아니라, 다른 span 요소도 들어오게 됩니다.! 그럼 text의 경우 aria-hidden이 적용될 것으로 예상됩니다.

시안에도 아이콘만 적용되어 있던데, 지금 구조상 권장하는 방식은 icon만 children으로 들어 오는 것을 권장하는 것인가요??

그리고 여기 예시처럼 텍스트가 들어오는 경우도 있는데, 이렇게 쓰는 경우 사용자에게 aria-hidden 적용 가능성을 어떻게 전달하면 좋을까요?

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.

넵 우선은 IconButton과 마찬가지로 아이콘만 포함되는 것으로 계획했습니다! 아직까지 Toggle이 텍스트와 함께 사용되는 케이스가 없는 것으로 확인했고, 그런 상황에서 Text를 포함하는 케이스까지 고려하는 게 당장은 불필요하다고 결론을 내렸습니다!

그리고 Text와 Icon이 모두 포함되는 케이스 처리 방법에 대해 잠깐 고민해봤는데, 그런 케이스가 필요하다면 Toggle은 이제 어떤 요소든 포함될 수 있는 것으로 두고, Toggle.Icon처럼 아이콘만을 처리하는 별도 컴포넌트가 있으면 좋을 것 같아요!

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