diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3b8db85ee4..c8ba044f0c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1575,6 +1575,25 @@ importers:
specifier: workspace:*
version: link:../../tools/testing-utils
+ semcore/radio-cards:
+ dependencies:
+ '@semcore/dot':
+ specifier: ^17.2.1
+ version: link:../dot
+ '@semcore/typography':
+ specifier: ^17.2.1
+ version: link:../typography
+ devDependencies:
+ '@semcore/base-components':
+ specifier: workspace:*
+ version: link:../base-components
+ '@semcore/core':
+ specifier: workspace:*
+ version: link:../core
+ '@semcore/testing-utils':
+ specifier: workspace:*
+ version: link:../../tools/testing-utils
+
semcore/select:
dependencies:
'@semcore/base-trigger':
@@ -19566,7 +19585,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.17
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@9.20.3)(jsdom@22.1.0)(lightningcss@1.31.1)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.48.0)(yaml@2.8.3)
+ vitest: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(happy-dom@9.20.3)(jsdom@22.1.0)(lightningcss@1.31.1)(sass@1.100.0)(sugarss@5.0.1(postcss@8.5.15))(terser@5.48.0)(yaml@2.8.3)
'@vitest/utils@3.2.4':
dependencies:
diff --git a/semcore/pills/__tests__/pills.axe-test.ts b/semcore/pills/__tests__/pills.axe-test.ts
index 687ce55f7c..ed2409fd48 100644
--- a/semcore/pills/__tests__/pills.axe-test.ts
+++ b/semcore/pills/__tests__/pills.axe-test.ts
@@ -16,11 +16,4 @@ test.describe(`@pills ${TAG.ACCESSIBILITY}`, () => {
const violations = await getAccessibilityViolations({ page });
expect(violations).toEqual([]);
});
-
- test('Custom pills example', async ({ page }) => {
- await loadPage(page, 'stories/components/pills/docs/examples/custom_pills_example.tsx', 'en');
-
- const violations = await getAccessibilityViolations({ page });
- expect(violations).toEqual([]);
- });
});
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx b/semcore/pills/__tests__/pills.browser-test.tsx
index b1bf88e3a6..dadc1edec1 100644
--- a/semcore/pills/__tests__/pills.browser-test.tsx
+++ b/semcore/pills/__tests__/pills.browser-test.tsx
@@ -68,28 +68,6 @@ test.describe(`${TAG.VISUAL}`, () => {
});
});
- test('Verify custom pills', {
- tag: [TAG.PRIORITY_HIGH, '@pills'],
- }, async ({ page }) => {
- await loadPage(page, 'stories/components/pills/docs/examples/custom_pills_example.tsx', 'en');
-
- await test.step('Verify hover state', async () => {
- await locators.pillsItem(page).nth(1).hover();
- await expect(page).toHaveScreenshot();
- });
-
- await test.step('Verify focus state', async () => {
- await page.keyboard.press('Tab');
- await expect(page).toHaveScreenshot();
- });
-
- await test.step('Verify navigation and hover', async () => {
- await page.keyboard.press('ArrowRight');
- await locators.pillsItem(page).nth(1).hover();
- await expect(page).toHaveScreenshot();
- });
- });
-
test('Verify pills with tab panel', {
tag: [TAG.PRIORITY_HIGH, '@pills'],
}, async ({ page }) => {
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-chromium-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-chromium-linux.png
deleted file mode 100644
index 5b906eca15..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-chromium-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-firefox-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-firefox-linux.png
deleted file mode 100644
index ddae558e33..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-firefox-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-webkit-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-webkit-linux.png
deleted file mode 100644
index edb3f07702..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-1-webkit-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-chromium-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-chromium-linux.png
deleted file mode 100644
index da235bd7f1..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-chromium-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-firefox-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-firefox-linux.png
deleted file mode 100644
index 3eb4e8d993..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-firefox-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-webkit-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-webkit-linux.png
deleted file mode 100644
index e3400fc345..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-2-webkit-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-chromium-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-chromium-linux.png
deleted file mode 100644
index 5051d46c32..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-chromium-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-firefox-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-firefox-linux.png
deleted file mode 100644
index c1767778a3..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-firefox-linux.png and /dev/null differ
diff --git a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-webkit-linux.png b/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-webkit-linux.png
deleted file mode 100644
index f3186577ef..0000000000
Binary files a/semcore/pills/__tests__/pills.browser-test.tsx-snapshots/-visual-Verify-custom-pills-3-webkit-linux.png and /dev/null differ
diff --git a/semcore/radio-cards/.npmignore b/semcore/radio-cards/.npmignore
new file mode 100644
index 0000000000..036da2936b
--- /dev/null
+++ b/semcore/radio-cards/.npmignore
@@ -0,0 +1,5 @@
+node_modules
+__tests__
+src
+tsconfig.json
+tsconfig.tsbuildinfo
diff --git a/semcore/radio-cards/CHANGELOG.md b/semcore/radio-cards/CHANGELOG.md
new file mode 100644
index 0000000000..edc662bcc8
--- /dev/null
+++ b/semcore/radio-cards/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+CHANGELOG.md standards are inspired by [keepachangelog.com](https://keepachangelog.com/en/1.0.0/).
+
+## [18.0.0] - 2026-08-04
+
+### Added
+
+- `RadioCards` component.
diff --git a/semcore/radio-cards/README.md b/semcore/radio-cards/README.md
new file mode 100644
index 0000000000..08de0012d7
--- /dev/null
+++ b/semcore/radio-cards/README.md
@@ -0,0 +1,37 @@
+# @semcore/radio-cards
+
+[](https://www.npmjs.com/@semcore/radio-cards)
+[](https://www.npmjs.com/package/@semcore/radio-cards)
+[](https://github.com/semrush/intergalactic/blob/HEAD/LICENSE)
+
+> This component is part of the Intergalactic Design System
+
+### π [Component documentation](https://developer.semrush.com/intergalactic/components/radio-cards/)
+
+### π [Design system](https://developer.semrush.com/intergalactic/)
+
+## Install
+
+```sh
+npm install @semcore/radio-cards
+```
+
+
+
+## π€ Author
+
+[UI-kit team](https://github.com/semrush/intergalactic/blob/HEAD/MAINTAINERS) and [others β€οΈ](https://github.com/semrush/intergalactic/graphs/contributors)
+
+## π€ Contributing
+
+Contributions, issues and feature requests are welcome!
+
+Feel free to check [issues page](https://github.com/semrush/intergalactic/issues). You can also take a look at the [contributing guide](https://github.com/semrush/intergalactic/blob/HEAD/CONTRIBUTING.md).
+
+## Show your support
+
+Give a βοΈ if this project helped you!
+
+## π License
+
+This project is [MIT](https://github.com/semrush/intergalactic/blob/HEAD/LICENSE) licensed.
diff --git a/semcore/radio-cards/__tests__/__snapshots__/index.test.tsx.snap b/semcore/radio-cards/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000..57eb0b84dc
--- /dev/null
+++ b/semcore/radio-cards/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,42 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`RadioCards > Verify data-ui-name 1`] = `
+{
+ "children": [
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "uiName": "Text",
+ },
+ ],
+ "uiName": "Flex",
+ },
+ ],
+ "uiName": "RadioCards.Item",
+ },
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "children": [],
+ "uiName": "Text",
+ },
+ ],
+ "uiName": "Flex",
+ },
+ ],
+ "uiName": "RadioCards.Item",
+ },
+ ],
+ "uiName": "RadioCards",
+ },
+ ],
+ "uiName": null,
+}
+`;
diff --git a/semcore/radio-cards/__tests__/index.test.tsx b/semcore/radio-cards/__tests__/index.test.tsx
new file mode 100644
index 0000000000..58c4d2ee65
--- /dev/null
+++ b/semcore/radio-cards/__tests__/index.test.tsx
@@ -0,0 +1,107 @@
+import { extractUIName } from '@semcore/testing-utils/shared/extractUINameTree.ts';
+import { runDependencyCheckTests } from '@semcore/testing-utils/shared-tests';
+import { render, cleanup, userEvent } from '@semcore/testing-utils/testing-library';
+import { expect, test, describe, beforeEach, vi } from '@semcore/testing-utils/vitest';
+import React from 'react';
+
+import RadioCards from '../src';
+
+describe('pills Dependency imports', () => {
+ runDependencyCheckTests('radio-cards');
+});
+
+describe('RadioCards', () => {
+ beforeEach(cleanup);
+
+ test('Verify data-ui-name', () => {
+ const radioCards = (
+
+
+
+
+ );
+
+ const { container } = render(radioCards);
+ const result = extractUIName(container);
+
+ expect(result).toMatchSnapshot();
+ });
+
+ test('calls onChange with selected value', async () => {
+ const onChange = vi.fn();
+
+ const { getByRole } = render(
+
+ 1
+ 2
+ 4
+ ,
+ );
+
+ await userEvent.click(getByRole('radio', { name: '4' }));
+
+ expect(onChange).toHaveBeenCalledWith(
+ '4',
+ expect.any(Object),
+ );
+ });
+
+ test('clicks aren\'t propagated on disabled tab', async () => {
+ const spy = vi.fn();
+
+ const { getByRole } = render(
+
+ 1
+ 2
+ 3
+ 4
+ ,
+ );
+
+ await expect(userEvent.click(getByRole('radio', { name: '4' }))).rejects.toThrow('pointer-events: none');
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ test('does not call onChange when group is disabled', async () => {
+ const spy = vi.fn();
+
+ const { getByRole } = render(
+
+ 1
+ 2
+ ,
+ );
+
+ await expect(userEvent.click(getByRole('radio', { name: '2' }))).rejects.toThrow('pointer-events: none');
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ test('supports keyboard navigation with ArrowRight', async () => {
+ const spy = vi.fn();
+
+ const { getByRole } = render(
+
+ 1
+ 2
+ 3
+ ,
+ );
+
+ const firstRadio = getByRole('radio', { name: '1' });
+
+ firstRadio.focus();
+ expect(firstRadio).toHaveFocus();
+
+ await userEvent.keyboard('{ArrowRight}');
+
+ expect(spy).toHaveBeenCalledWith(
+ '2',
+ expect.any(Object),
+ );
+ });
+});
diff --git a/semcore/radio-cards/__tests__/radio-cards.axe-test.tsx b/semcore/radio-cards/__tests__/radio-cards.axe-test.tsx
new file mode 100644
index 0000000000..e101474a6f
--- /dev/null
+++ b/semcore/radio-cards/__tests__/radio-cards.axe-test.tsx
@@ -0,0 +1,39 @@
+import { expect, test, getAccessibilityViolations } from '@semcore/testing-utils/playwright';
+import { loadPage } from '@semcore/testing-utils/shared/helpers';
+import { TAG } from '@semcore/testing-utils/shared/tags';
+
+test.describe(`@radio-cards ${TAG.ACCESSIBILITY}`, () => {
+ test('Basic', async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/docs/examples/basic.tsx', 'en');
+
+ {
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ }
+
+ {
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('ArrowRight');
+
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ }
+ });
+
+ test('Advanced', async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/docs/examples/advanced.tsx', 'en');
+
+ {
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ }
+
+ {
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('Enter');
+
+ const violations = await getAccessibilityViolations({ page });
+ expect(violations).toEqual([]);
+ }
+ });
+});
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx
new file mode 100644
index 0000000000..da58703eac
--- /dev/null
+++ b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx
@@ -0,0 +1,104 @@
+import type { Page } from '@semcore/testing-utils/playwright';
+import { test, expect } from '@semcore/testing-utils/playwright';
+import { loadPage } from '@semcore/testing-utils/shared/helpers';
+import { TAG } from '@semcore/testing-utils/shared/tags';
+
+export const locators = {
+ group: (page: Page) => page.getByRole('radiogroup'),
+ card: (page: Page, index?: number) => {
+ const base = page.getByRole('radio');
+ return typeof index === 'number' ? base.nth(index) : base;
+ },
+};
+
+/* =====================================================
+@visual
+Visual states, hover and focus styles, paddings, margins, and snapshots.
+===================================================== */
+test.describe(`${TAG.VISUAL}`, () => {
+ test('Verify default, hover, focus and disabled states', {
+ tag: [TAG.PRIORITY_HIGH, '@radio-cards'],
+ }, async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en');
+
+ await test.step('Verify initial render with a selected card', async () => {
+ await expect(page).toHaveScreenshot('initial-render-with-selected-card.png');
+ });
+
+ await test.step('Verify hover state', async () => {
+ await locators.card(page, 0).hover();
+ await expect(page).toHaveScreenshot('hover-state.png');
+ });
+
+ await test.step('Verify focus state', async () => {
+ await page.keyboard.press('Tab');
+ await expect(page).toHaveScreenshot('focus-state.png');
+ });
+
+ await test.step('Verify disabled card opacity', async () => {
+ await expect(locators.card(page, 2)).toHaveCSS('opacity', '0.4');
+ });
+ });
+
+ test('Verify selected state after click', {
+ tag: [TAG.PRIORITY_HIGH, '@radio-cards'],
+ }, async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en');
+
+ await locators.card(page, 3).click();
+ await expect(page).toHaveScreenshot('selected-state-after-click.png');
+ });
+});
+
+/* =====================================================
+@functional
+Keyboard and mouse interactions - no snapshots here.
+We verify states, visibility, and attributes.
+===================================================== */
+test.describe(`${TAG.FUNCTIONAL}`, () => {
+ test('Verify click selects a card', {
+ tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@radio-cards'],
+ }, async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en');
+
+ await test.step('Click on first card selects it', async () => {
+ await locators.card(page, 0).click();
+
+ await expect(locators.card(page, 0)).toHaveAttribute('aria-checked', 'true');
+ await expect(locators.card(page, 1)).toHaveAttribute('aria-checked', 'false');
+ });
+ });
+
+ test('Verify disabled card cannot be selected', {
+ tag: [TAG.PRIORITY_HIGH, TAG.MOUSE, '@radio-cards'],
+ }, async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en');
+
+ await expect(locators.card(page, 2)).toHaveAttribute('aria-disabled', 'true');
+ await expect(locators.card(page, 2)).toHaveCSS('pointer-events', 'none');
+ });
+
+ test('Verify roving tabindex', {
+ tag: [TAG.PRIORITY_HIGH, TAG.KEYBOARD, '@radio-cards'],
+ }, async ({ page }) => {
+ await loadPage(page, 'stories/components/radio-cards/tests/examples/radio-card-all-props.tsx', 'en');
+
+ await test.step('Only the selected card is tabbable', async () => {
+ await expect(locators.card(page, 0)).toHaveAttribute('tabindex', '-1');
+ await expect(locators.card(page, 1)).toHaveAttribute('tabindex', '0');
+ await expect(locators.card(page, 3)).toHaveAttribute('tabindex', '-1');
+ });
+
+ await test.step('ArrowRight skips the disabled card and moves to the next enabled one', async () => {
+ await page.keyboard.press('Tab');
+ await expect(locators.card(page, 1)).toBeFocused();
+
+ await page.keyboard.press('ArrowRight');
+
+ await expect(locators.card(page, 3)).toBeFocused();
+ await expect(locators.card(page, 3)).toHaveAttribute('aria-checked', 'true');
+ });
+ });
+
+ // TODO: cover ArrowLeft wrap-around, group `disabled` prop, and uncontrolled usage without an initial `value`
+});
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-chromium-linux.png
new file mode 100644
index 0000000000..d5b529bf55
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-chromium-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-firefox-linux.png
new file mode 100644
index 0000000000..85eedc16cf
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-firefox-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-webkit-linux.png
new file mode 100644
index 0000000000..01f2a1aaa0
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/focus-state-webkit-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-chromium-linux.png
new file mode 100644
index 0000000000..27849be52e
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-chromium-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-firefox-linux.png
new file mode 100644
index 0000000000..6283c0aa7d
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-firefox-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-webkit-linux.png
new file mode 100644
index 0000000000..c30a8648b0
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/hover-state-webkit-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-chromium-linux.png
new file mode 100644
index 0000000000..52fb30fe5b
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-chromium-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-firefox-linux.png
new file mode 100644
index 0000000000..6283c0aa7d
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-firefox-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-webkit-linux.png
new file mode 100644
index 0000000000..f03acf6542
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/initial-render-with-selected-card-webkit-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-chromium-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-chromium-linux.png
new file mode 100644
index 0000000000..29b3984f53
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-chromium-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-firefox-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-firefox-linux.png
new file mode 100644
index 0000000000..1289f5d539
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-firefox-linux.png differ
diff --git a/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-webkit-linux.png b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-webkit-linux.png
new file mode 100644
index 0000000000..140628aa6b
Binary files /dev/null and b/semcore/radio-cards/__tests__/radio-cards.browser-test.tsx-snapshots/selected-state-after-click-webkit-linux.png differ
diff --git a/semcore/radio-cards/package.json b/semcore/radio-cards/package.json
new file mode 100644
index 0000000000..33e954d761
--- /dev/null
+++ b/semcore/radio-cards/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@semcore/radio-cards",
+ "description": "Semrush RadioCards Component",
+ "version": "18.0.0",
+ "main": "lib/cjs/index.js",
+ "module": "lib/es6/index.js",
+ "typings": "lib/types/index.d.ts",
+ "sideEffects": false,
+ "author": "UI-kit team ",
+ "license": "MIT",
+ "scripts": {
+ "build": "pnpm semcore-builder && pnpm vite build"
+ },
+ "exports": {
+ "types": "./lib/types/index.d.ts",
+ "import": "./lib/esm/index.mjs",
+ "require": "./lib/cjs/index.js"
+ },
+ "dependencies": {
+ "@semcore/typography": "^17.2.1",
+ "@semcore/dot": "^17.2.1"
+ },
+ "peerDependencies": {
+ "@semcore/base-components": "^17.2.1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/semrush/intergalactic.git",
+ "directory": "semcore/radio-cards"
+ },
+ "devDependencies": {
+ "@semcore/core": "workspace:*",
+ "@semcore/base-components": "workspace:*",
+ "@semcore/testing-utils": "workspace:*"
+ }
+}
diff --git a/semcore/radio-cards/src/RadioCards.tsx b/semcore/radio-cards/src/RadioCards.tsx
new file mode 100644
index 0000000000..7a642433e1
--- /dev/null
+++ b/semcore/radio-cards/src/RadioCards.tsx
@@ -0,0 +1,165 @@
+import { Flex, NeighborLocation } from '@semcore/base-components';
+import type { Intergalactic } from '@semcore/core';
+import { Root, Component, createComponent, sstyled } from '@semcore/core';
+import a11yEnhance from '@semcore/core/lib/utils/enhances/a11yEnhance';
+import Dot from '@semcore/dot';
+import { Text as SemcoreText } from '@semcore/typography';
+import React from 'react';
+
+import type { NSRadioCards } from './RadioCards.type';
+import style from './styles/radio-cards.shadow.css';
+
+const DEFAULT_VALUE = '';
+
+class RadioCardsRoot extends Component<
+ Intergalactic.InternalTypings.InferComponentProps,
+ typeof RadioCardsRoot.enhance,
+ NSRadioCards.Handlers,
+ {},
+ {},
+ NSRadioCards.DefaultProps
+> {
+ static displayName = 'RadioCards';
+ static style = style;
+
+ static enhance = [a11yEnhance({
+ onNeighborChange: (neighborElement) => {
+ if (neighborElement) {
+ neighborElement.focus();
+ neighborElement.click();
+ }
+ },
+ childSelector: () => {
+ const selector: [string, string] = ['role', 'radio'];
+
+ return selector;
+ },
+ })] as const;
+
+ static defaultProps = {
+ defaultValue: DEFAULT_VALUE,
+ } as const;
+
+ private firstNonDisabledItemIdx: number | null = null;
+
+ componentDidUpdate() {
+ this.firstNonDisabledItemIdx = null;
+ }
+
+ uncontrolledProps() {
+ return {
+ value: DEFAULT_VALUE,
+ };
+ }
+
+ handleClick = (value: NSRadioCards.Value) => (e: React.MouseEvent) => {
+ this.handlers.value(value, e);
+ };
+
+ handleKeyDown = (value: NSRadioCards.Value) => (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+
+ this.handlers.value(value, e);
+ }
+ };
+
+ getTabIndex({ props, index }: { props: NSRadioCards.Item.Props; index: number }) {
+ const { value, disabled: rootDisabled } = this.asProps;
+ const { value: itemValue, disabled: itemDisabled } = props;
+ const isChecked = value === itemValue;
+
+ if (rootDisabled) return -1;
+
+ if (this.firstNonDisabledItemIdx === null && !itemDisabled) {
+ this.firstNonDisabledItemIdx = index;
+ }
+
+ if (index === this.firstNonDisabledItemIdx && value === DEFAULT_VALUE) {
+ return 0;
+ }
+
+ return isChecked ? 0 : -1;
+ }
+
+ getItemProps(props: NSRadioCards.Item.Props, index: number) {
+ const { value: itemValue } = props;
+ const { value, disabled } = this.asProps;
+ const isChecked = value === itemValue;
+
+ return {
+ tabIndex: this.getTabIndex({ props, index }),
+ checked: isChecked,
+ disabled,
+ onClick: this.handleClick(itemValue),
+ onKeyDown: this.handleKeyDown(itemValue),
+ };
+ }
+
+ render() {
+ const SRadioCards = Root;
+ const { Children, styles, disabled } = this.asProps;
+
+ return sstyled(styles)(
+
+
+
+
+ ,
+ );
+ }
+}
+
+function Item(
+ props: Intergalactic.InternalTypings.InferChildComponentProps<
+ NSRadioCards.Item.Component,
+ typeof RadioCardsRoot,
+ 'Item'
+ >,
+) {
+ const SRadioItem = Root;
+ const SRadioItemHeader = Flex;
+ const SRadioItemHeaderLeftAddon = Flex;
+ const SRadioItemHeaderText = SemcoreText;
+ const SRadioItemHeaderRightAddon = SemcoreText;
+ const SRadioItemDescription = SemcoreText;
+ const { Children, styles, iconAddon, text, textAddon, description, disabled, checked, dot, children } = props;
+
+ const isAdvancedMode = children !== undefined;
+
+ return sstyled(styles)(
+
+ {isAdvancedMode
+ ? (
+
+ )
+ : (
+ <>
+
+ {iconAddon && {iconAddon}}
+ {text && {text}}
+ {textAddon && (
+
+ {textAddon}
+
+ )}
+
+ {description && {description}}
+ {dot && }
+ >
+ )}
+ ,
+ );
+}
+
+const RadioCards = createComponent(RadioCardsRoot, {
+ Item,
+});
+
+export default RadioCards;
diff --git a/semcore/radio-cards/src/RadioCards.type.ts b/semcore/radio-cards/src/RadioCards.type.ts
new file mode 100644
index 0000000000..57da62a5ad
--- /dev/null
+++ b/semcore/radio-cards/src/RadioCards.type.ts
@@ -0,0 +1,52 @@
+import type { NSFlex } from '@semcore/base-components';
+import type { Intergalactic } from '@semcore/core';
+
+declare namespace NSRadioCards {
+ type Value = string;
+ type Props = NSFlex.Props & Intergalactic.RequireAtLeastOne<{
+ /** Accessible name for the radio group. */
+ 'aria-label'?: string;
+ /** References an element that labels the radio group. */
+ 'aria-labelledby'?: string;
+ }> & {
+ /** Currently selected radio card value. */
+ value?: NSRadioCards.Value;
+ /** Called when the selected radio card changes. */
+ onChange?: (value: NSRadioCards.Value, event?: React.SyntheticEvent) => void;
+ /** Disables the entire radio group and all radio cards. */
+ disabled?: boolean;
+ };
+ type DefaultProps = {
+ defaultValue: '';
+ };
+ type Handlers = {
+ value: NSRadioCards.Value;
+ };
+
+ namespace Item {
+ type Props = NSFlex.Props & {
+ /** Disables radio card. */
+ disabled?: boolean;
+ /** Optional icon displayed before the content. */
+ iconAddon?: React.ReactNode;
+ /** Optional text displayed in the header. */
+ text?: string;
+ /** Unique value associated with the radio card. */
+ value: NSRadioCards.Value;
+ /** Additional text displayed in the header. */
+ textAddon?: string;
+ /** Description displayed below the header. */
+ description?: string;
+ /** Shows a dot indicator with the specified accessible label. */
+ dot?: string;
+ };
+
+ type Component = Intergalactic.Component<'button', Props>;
+ }
+
+ type Component = Intergalactic.Component<'div', Props> & {
+ Item: Item.Component;
+ };
+}
+
+export type { NSRadioCards };
diff --git a/semcore/radio-cards/src/index.ts b/semcore/radio-cards/src/index.ts
new file mode 100644
index 0000000000..6fb1a9626e
--- /dev/null
+++ b/semcore/radio-cards/src/index.ts
@@ -0,0 +1,2 @@
+export { default } from './RadioCards';
+export * from './RadioCards.type';
diff --git a/semcore/radio-cards/src/styles/radio-cards.shadow.css b/semcore/radio-cards/src/styles/radio-cards.shadow.css
new file mode 100644
index 0000000000..726bd318da
--- /dev/null
+++ b/semcore/radio-cards/src/styles/radio-cards.shadow.css
@@ -0,0 +1,50 @@
+SRadioCards {
+ gap: var(--intergalactic-spacing-content-gap-large, 8px);
+}
+
+SRadioItem {
+ position: relative;
+ flex-direction: column;
+ flex: 1 0 0;
+ gap: var(--intergalactic-spacing-content-gap-small, 4px);
+ padding: var(--intergalactic-spacing-content-padding-medium, 12px) var(--intergalactic-spacing-content-padding-xlarge, 20px);
+ align-items: flex-start;
+
+ border-radius: var(--intergalactic-pills-rounded, 6px);
+ background: var(--intergalactic-control-presets-bg, oklch(1 0 0));
+ box-shadow: var(--intergalactic-box-shadow-control-elevated, 0px 0px 1px 0px oklch(0.176 0.033 175.7 / 0.07), 0px 1px 3px 0px oklch(0.176 0.033 175.7 / 0.07));
+ cursor: pointer;
+ border: 1px solid transparent;
+
+ &:hover {
+ border: 1px solid var(--intergalactic-control-presets-border-hover, oklch(0.137 0.026 175.7 / 0.161));
+ }
+
+ &[aria-checked='true'] {
+ border: 1px solid var(--intergalactic-control-presets-border-selected, oklch(0.23 0.01 140));
+ }
+
+ &[aria-disabled='true'] {
+ pointer-events: none;
+ opacity: var(--intergalactic-disabled-opacity, 0.4);
+ }
+}
+
+SRadioItemHeader {
+ gap: var(--intergalactic-spacing-content-gap-medium, 6px);
+}
+
+SRadioItemHeaderText,
+SRadioItemDescription {
+ text-align: left;
+}
+
+SRadioItemHeaderLeftAddon {
+ transform: translateY(4px);
+ color: var(--intergalactic-icon-primary-neutral, oklch(0.092 0.024 152.2 / 0.526));
+}
+
+
+SRadioItemHeaderRightAddon {
+ color: var(--intergalactic-text-secondary, oklch(0.088 0.026 147.7 / 0.583));
+}
diff --git a/semcore/radio-cards/tsconfig.json b/semcore/radio-cards/tsconfig.json
new file mode 100644
index 0000000000..3ed2a1cc8b
--- /dev/null
+++ b/semcore/radio-cards/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json"],
+ "compilerOptions": {
+ "rootDir": "./src"
+ }
+}
diff --git a/semcore/radio-cards/vite.config.ts b/semcore/radio-cards/vite.config.ts
new file mode 100644
index 0000000000..90f46b4cb2
--- /dev/null
+++ b/semcore/radio-cards/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig, mergeConfig } from 'vite';
+
+import viteConfig from '../../commonVite.config';
+
+export default mergeConfig(
+ viteConfig,
+ defineConfig({
+ build: {
+ lib: {
+ entry: './src/index.ts',
+ },
+ rollupOptions: {
+ external: ['react', 'react-dom', 'react/jsx-runtime', /@babel\/runtime\/*/, /@semcore\/*/],
+ },
+ },
+ }),
+);
diff --git a/semcore/radio-cards/vitest.config.mts b/semcore/radio-cards/vitest.config.mts
new file mode 100644
index 0000000000..36006d4f8e
--- /dev/null
+++ b/semcore/radio-cards/vitest.config.mts
@@ -0,0 +1,17 @@
+import { resolve as resolvePath } from 'node:path';
+
+import { defineConfig } from 'vitest/config';
+
+import { vitestPlugins, vitestResolve } from '../../vitest.config.mts';
+
+export default defineConfig({
+ plugins: vitestPlugins,
+ resolve: vitestResolve,
+ test: {
+ include: [
+ '__tests__/**/*.test.tsx',
+ ],
+ environment: 'jsdom',
+ setupFiles: [resolvePath(__dirname, '../../tools/testing-utils/setupTests')],
+ },
+});
diff --git a/stories/components/pills/docs/Pills.stories.tsx b/stories/components/pills/docs/Pills.stories.tsx
index 72fd715403..3d811cddcf 100644
--- a/stories/components/pills/docs/Pills.stories.tsx
+++ b/stories/components/pills/docs/Pills.stories.tsx
@@ -2,7 +2,6 @@ import Pills from '@semcore/ui/pills';
import type { Meta, StoryObj } from '@storybook/react-vite';
import BasicUsageExample, { defaultProps as BasicUsageProps } from './examples/basic_example';
-import CustomPillsExample, { defaultProps as CustomPillsProps } from './examples/custom_pills_example';
import TabsExample, { defaultProps as TabExampleProps } from './examples/tabs_example';
const meta: Meta = {
@@ -47,21 +46,3 @@ export const Tabs: StoryObj = {
},
args: TabExampleProps,
};
-
-export const CustomPills: StoryObj = {
- render: CustomPillsExample,
- argTypes: {
- size: {
- control: { type: 'select' },
- options: ['m', 'l'],
- },
- behavior: {
- control: { type: 'select' },
- options: ['manual', 'auto'],
- },
- disabled: {
- control: { type: 'boolean' },
- },
- },
- args: CustomPillsProps,
-};
diff --git a/stories/components/pills/docs/examples/custom_pills_example.tsx b/stories/components/pills/docs/examples/custom_pills_example.tsx
deleted file mode 100644
index 39067fc765..0000000000
--- a/stories/components/pills/docs/examples/custom_pills_example.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Flex } from '@semcore/ui/base-components';
-import Pills from '@semcore/ui/pills';
-import type { NSPills } from '@semcore/ui/pills';
-import { Text } from '@semcore/ui/typography';
-import React from 'react';
-import type { CSSProperties } from 'react';
-
-const contentBLocks = [
- 'Only awake for snacks and existential crises. The rest of the time, she is contemplating the void from under a blanket.',
- 'A yoga master: sleeps upside down without dropping a single thought. Scientists are still trying to understand how her brain stays so calm.',
- 'Sleep is her full-time job. Occasionally wakes up just to sigh and go back to bed. Her alarm clock gave up and filed for early retirement.',
-];
-
-const pillStyles: CSSProperties = {
- height: 'fit-content',
- alignItems: 'stretch',
- textAlign: 'inherit',
- whiteSpace: 'normal',
-} as const;
-
-type PillExampleProps = NSPills.Props & NSPills.Pill.Props;
-const Demo = (props: PillExampleProps) => {
- const [tab, setTab] = React.useState(0);
-
- return (
-
-
- Average sleep per day
-
-
-
-
- Wombat
- 16 hours
-
-
-
-
- Bat
- 20 hours
-
-
-
-
- Koala
- 22 hours
-
-
-
-
- {contentBLocks[tab]}
-
-
- );
-};
-
-export const defaultProps: PillExampleProps = {
- size: 'm',
- disabled: undefined,
- behavior: 'manual',
-};
-
-Demo.defaultProps = defaultProps;
-
-export default Demo;
diff --git a/stories/components/radio-cards/docs/examples/advanced.tsx b/stories/components/radio-cards/docs/examples/advanced.tsx
new file mode 100644
index 0000000000..3e44ae3300
--- /dev/null
+++ b/stories/components/radio-cards/docs/examples/advanced.tsx
@@ -0,0 +1,41 @@
+import FileExport from '@semcore/icon/FileExport/m';
+import Info from '@semcore/icon/Info/m';
+import { Flex } from '@semcore/ui/base-components';
+import Dot from '@semcore/ui/dot';
+import RadioCards from '@semcore/ui/radio-cards';
+import Spin from '@semcore/ui/spin';
+import Tag, { TagContainer } from '@semcore/ui/tag';
+import { Text } from '@semcore/ui/typography';
+import React from 'react';
+
+const Demo = () => {
+ return (
+
+
+
+
+ Custom layout
+
+
+ Combine any addons and content
+ 12
+
+
+
+
+ Loading state
+
+
+
+ One
+
+
+
+
+ Any children are supported
+
+
+ );
+};
+
+export default Demo;
diff --git a/stories/components/radio-cards/docs/examples/basic.tsx b/stories/components/radio-cards/docs/examples/basic.tsx
new file mode 100644
index 0000000000..6713e777b6
--- /dev/null
+++ b/stories/components/radio-cards/docs/examples/basic.tsx
@@ -0,0 +1,35 @@
+import Fire from '@semcore/icon/Fire/m';
+import RadioCards from '@semcore/ui/radio-cards';
+import React, { useState } from 'react';
+
+const Demo = () => {
+ const [value, setValue] = useState('all');
+
+ return (
+
+
+ }
+ />
+
+
+
+ );
+};
+
+export default Demo;
diff --git a/stories/components/radio-cards/docs/radio-cards.stories.tsx b/stories/components/radio-cards/docs/radio-cards.stories.tsx
new file mode 100644
index 0000000000..96649f2cd7
--- /dev/null
+++ b/stories/components/radio-cards/docs/radio-cards.stories.tsx
@@ -0,0 +1,20 @@
+import RadioCards from '@semcore/ui/radio-cards';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import AdvancedExample from './examples/advanced';
+import BasicExample from './examples/basic';
+
+const meta: Meta = {
+ title: 'Components/RadioCards/Documentation',
+ component: RadioCards,
+};
+
+export default meta;
+
+export const BasicRadioCardsExample: StoryObj = {
+ render: BasicExample,
+};
+
+export const AdvancedRadioCardsExample: StoryObj = {
+ render: AdvancedExample,
+};
diff --git a/stories/components/radio-cards/tests/examples/radio-card-all-props.tsx b/stories/components/radio-cards/tests/examples/radio-card-all-props.tsx
new file mode 100644
index 0000000000..c847ad5be0
--- /dev/null
+++ b/stories/components/radio-cards/tests/examples/radio-card-all-props.tsx
@@ -0,0 +1,95 @@
+import Fire from '@semcore/icon/Fire/m';
+import ThumbUp from '@semcore/icon/ThumbUp/m';
+import { Flex } from '@semcore/ui/base-components';
+import RadioCards from '@semcore/ui/radio-cards';
+import type { NSRadioCards } from '@semcore/ui/radio-cards';
+import { Text } from '@semcore/ui/typography';
+import React from 'react';
+
+type ExampleProps = NSRadioCards.Props & {
+ disabledCard?: 'none' | '1' | '2' | '3' | '4';
+ text?: string;
+ textAddon?: string;
+ description?: string;
+ showIconAddon?: boolean;
+ dot?: boolean;
+};
+
+const Demo = (props: ExampleProps) => {
+ const {
+ value,
+ disabled,
+ disabledCard = 'none',
+ text = 'Lost and Vital',
+ textAddon = '24',
+ description = 'Restore these backlinks first',
+ showIconAddon = true,
+ 'aria-label': ariaLabel = 'Radio cards',
+ dot = false,
+ } = props;
+
+ const [selected, setSelected] = React.useState(value);
+
+ React.useEffect(() => {
+ setSelected(value);
+ }, [value]);
+
+ const handleChange: NSRadioCards.Props['onChange'] = (newValue, event) => {
+ setSelected(newValue);
+ props.onChange?.(newValue, event);
+ };
+
+ return (
+
+
+
+ }
+ disabled={disabledCard === '2'}
+ />
+
+ : undefined}
+ disabled={disabledCard === '4'}
+ />
+
+ Selected value: {selected || '(none)'}
+
+ );
+};
+
+export const defaultRadioCardsProps: ExampleProps = {
+ 'aria-label': 'Radio cards',
+ 'value': '2',
+ 'disabled': false,
+ 'disabledCard': '3',
+ 'text': 'Lost and Vital',
+ 'textAddon': '24',
+ 'description': 'Restore these backlinks first',
+ 'showIconAddon': true,
+ 'dot': false,
+};
+
+Demo.defaultProps = defaultRadioCardsProps;
+
+export default Demo;
diff --git a/stories/components/radio-cards/tests/radio-cards.stories.tsx b/stories/components/radio-cards/tests/radio-cards.stories.tsx
new file mode 100644
index 0000000000..dcbbbfaa06
--- /dev/null
+++ b/stories/components/radio-cards/tests/radio-cards.stories.tsx
@@ -0,0 +1,56 @@
+import RadioCards from '@semcore/ui/radio-cards';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import RadioCardAllPropsExample, { defaultRadioCardsProps } from './examples/radio-card-all-props';
+
+const meta: Meta = {
+ title: 'Components/RadioCards/Tests',
+ component: RadioCards,
+};
+
+export default meta;
+
+export const RadioCardAllProps: StoryObj = {
+ render: RadioCardAllPropsExample,
+ argTypes: {
+ 'aria-label': {
+ control: { type: 'text' },
+ description: 'Accessible name for the radio group',
+ },
+ 'value': {
+ control: { type: 'select' },
+ options: ['1', '2', '3', '4'],
+ description: 'Currently selected radio card value (group)',
+ },
+ 'disabled': {
+ control: { type: 'boolean' },
+ description: 'Disables the entire radio group and all radio cards',
+ },
+ 'disabledCard': {
+ control: { type: 'select' },
+ options: ['none', '1', '2', '3', '4'],
+ description: 'Disables the card with this value, to demonstrate item-level disabled (RadioCards.Item disabled prop)',
+ },
+ 'text': {
+ control: { type: 'text' },
+ description: 'Text of the 4th card (RadioCards.Item text prop)',
+ },
+ 'textAddon': {
+ control: { type: 'text' },
+ description: 'Text addon of the 4th card (RadioCards.Item textAddon prop)',
+ },
+ 'description': {
+ control: { type: 'text' },
+ description: 'Description of the 4th card (RadioCards.Item description prop)',
+ },
+ 'showIconAddon': {
+ control: { type: 'boolean' },
+ description: 'Toggles iconAddon on the 4th card (RadioCards.Item iconAddon prop)',
+ },
+ 'dot': {
+ control: { type: 'boolean' },
+ description: 'Shows a Dot indicator with the "New" label on the 1st card (RadioCards.Item dot prop)',
+ },
+ },
+ args: defaultRadioCardsProps,
+};
diff --git a/website/docs/.vitepress/sidebarConfig.ts b/website/docs/.vitepress/sidebarConfig.ts
index a5f9562fe8..b367fece4f 100644
--- a/website/docs/.vitepress/sidebarConfig.ts
+++ b/website/docs/.vitepress/sidebarConfig.ts
@@ -354,6 +354,11 @@ export const sideBarConfig: SidebarConfig = [
activeMatch: '/components/radio/',
text: 'Radio',
},
+ {
+ link: '/components/radio-cards/radio-cards',
+ activeMatch: '/components/radio-cards/',
+ text: 'RadioCards',
+ },
{
link: '/components/scroll-area/scroll-area',
activeMatch: '/components/scroll-area/',
diff --git a/website/docs/components/pills/pills-code.md b/website/docs/components/pills/pills-code.md
index 7106877072..11ec92d6df 100644
--- a/website/docs/components/pills/pills-code.md
+++ b/website/docs/components/pills/pills-code.md
@@ -27,15 +27,3 @@ Don't forget to add role `tabpanel` and `aria-labelledby` to the content of each
:::
-
-## Custom pills
-
-You can place additional information like some metrics inside `Pills.Item`. In this case, we recommend using Pills strictly as **tabs**.
-
-::: sandbox
-
-
-
-:::
diff --git a/website/docs/components/radio-cards/radio-cards-a11y.md b/website/docs/components/radio-cards/radio-cards-a11y.md
new file mode 100644
index 0000000000..2f5dcc6ae7
--- /dev/null
+++ b/website/docs/components/radio-cards/radio-cards-a11y.md
@@ -0,0 +1,46 @@
+---
+title: Radio Cards
+fileSource: radio-cards
+tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog')
+---
+
+## What component has
+
+Component `RadioCards` functions as a group of radio buttons.
+
+### Keyboard support
+
+Table: Keyboard support
+
+| Key | Function |
+|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `Tab` | When focus moves to the `RadioCards`, it moves to the active `RadioCards.Item`. |
+| `Left Arrow` , `Right Arrow` | Set `checked` value to the next/previous button in the group. If focus is on the last/first button, moves focus to the first/last button respectively. |
+
+See detailed information about the keyboard support for clickable elements in the [Keyboard control guide](/core-principles/a11y/a11y-keyboard#any-other-controls-filtertrigger-pills-tabline-etc).
+
+### Roles & attributes
+
+The following list describes roles and attributes that the component with already has.
+
+Table: Roles & attributes
+
+| Component / Element | Roles & attributes | Usage |
+|---------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `RadioCards` | `radiogroup` | Identifies the `div` element as a container for a group of radio buttons. Isnβt focusable because focus is managed using a roving tabindex strategy as described below. |
+| `RadioCards.Item` | `radio` | Identifies the `div` element as an ARIA radio button. The accessible name is computed from the child text content of the `div` element. |
+| | `tabIndex="0"` | Makes the radio button focusable and includes it in the page `Tab` sequence. Set on only one radio in the radio group. Moves with focus inside the radio group so the most recently focused radio button is included in the page `Tab` sequence. This approach to managing focus is described in the section on [roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd-roving-tabindex). |
+| | `tabindex="-1"` | Makes the element focusable but not part of the page `Tab` sequence. Applied to all radio buttons contained in the radio group except for one that's included in the page `Tab` sequence. This approach to managing focus is described in the section on [Roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyboardnavigationinsidecomponents). |
+| | `aria-checked="true/false"` | Identifies whether radio buttons are checked or unchecked. | |
+
+## Considerations for developers
+
+Since `RadioCards` behaves as a radio group, do not forget to add the `aria-labelledby` or `aria-label` attribute to the root element.
+
+## Resources
+
+- [A11y style guide](https://a11y-style-guide.com/style-guide/section-forms.html#kssref-forms-radio-buttons) gives core recommendations for the accessible radio buttons.
+
+## Other recommendations
+
+Find more accessibility recommendations in the common [Accessibility guide](/core-principles/a11y/a11y).
diff --git a/website/docs/components/radio-cards/radio-cards-api.md b/website/docs/components/radio-cards/radio-cards-api.md
new file mode 100644
index 0000000000..ebe37f4880
--- /dev/null
+++ b/website/docs/components/radio-cards/radio-cards-api.md
@@ -0,0 +1,25 @@
+---
+title: Radio Cards
+fileSource: radio-cards
+tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog')
+---
+
+## RadioCards
+
+```jsx
+import RadioCards from '@semcore/ui/radio-cards';
+;
+```
+
+
+
+## RadioCards.Item
+
+```jsx
+import RadioCards from '@semcore/ui/radio-cards';
+;
+```
+
+
+
+
diff --git a/website/docs/components/radio-cards/radio-cards-changelog.md b/website/docs/components/radio-cards/radio-cards-changelog.md
new file mode 100644
index 0000000000..6df201a9cc
--- /dev/null
+++ b/website/docs/components/radio-cards/radio-cards-changelog.md
@@ -0,0 +1,7 @@
+---
+title: Radio Cards
+fileSource: radio-cards
+tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog')
+---
+
+::: changelog radio-cards :::
diff --git a/website/docs/components/radio-cards/radio-cards-code.md b/website/docs/components/radio-cards/radio-cards-code.md
new file mode 100644
index 0000000000..9b5edb9ebe
--- /dev/null
+++ b/website/docs/components/radio-cards/radio-cards-code.md
@@ -0,0 +1,25 @@
+---
+title: Radio Cards
+fileSource: radio-cards
+tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog')
+---
+
+## Basic usage
+
+::: sandbox
+
+
+
+:::
+
+## Advanced usage
+
+::: sandbox
+
+
+
+:::
\ No newline at end of file
diff --git a/website/docs/components/radio-cards/radio-cards.md b/website/docs/components/radio-cards/radio-cards.md
new file mode 100644
index 0000000000..5ad38b27c6
--- /dev/null
+++ b/website/docs/components/radio-cards/radio-cards.md
@@ -0,0 +1,7 @@
+---
+title: Radio Cards
+fileSource: radio-cards
+tabs: Design('radio-cards'), A11y('radio-cards-a11y'), API('radio-cards-api'), Examples('radio-cards-code'), Changelog('radio-cards-changelog')
+---
+
+TODO: Needs to be filled.
\ No newline at end of file
diff --git a/website/docs/style/design-tokens/design-tokens.json b/website/docs/style/design-tokens/design-tokens.json
index 466b219300..3c0090ca7d 100644
--- a/website/docs/style/design-tokens/design-tokens.json
+++ b/website/docs/style/design-tokens/design-tokens.json
@@ -1735,6 +1735,7 @@
"drag-and-drop",
"feature-highlight",
"notice",
+ "radio-cards",
"typography"
]
},
@@ -2062,19 +2063,25 @@
"name": "--intergalactic-control-presets-bg",
"value": "oklch(1 0 0)",
"description": "Background of the Presets items.",
- "components": []
+ "components": [
+ "radio-cards"
+ ]
},
{
"name": "--intergalactic-control-presets-border-selected",
"value": "oklch(0.23 0.01 140)",
"description": "Border color of the selected Preset item.",
- "components": []
+ "components": [
+ "radio-cards"
+ ]
},
{
"name": "--intergalactic-control-presets-border-hover",
"value": "oklch(0.137 0.026 175.7 / 0.161)",
"description": "Border color of the hovered Preset item.",
- "components": []
+ "components": [
+ "radio-cards"
+ ]
},
{
"name": "--intergalactic-progress-bar-bg",
@@ -3017,6 +3024,7 @@
"flags",
"fullscreen-modal",
"pills",
+ "radio-cards",
"slider",
"switch",
"tab-panel",
@@ -3155,6 +3163,7 @@
"link",
"pills",
"radio",
+ "radio-cards",
"slider",
"switch",
"tab-line",
@@ -3184,6 +3193,7 @@
"description": "Shadow of the Switch toggle.",
"components": [
"pills",
+ "radio-cards",
"switch"
]
},
@@ -3355,6 +3365,7 @@
"input-tags",
"modal",
"pills",
+ "radio-cards",
"textarea",
"time-picker",
"tooltip"
@@ -3376,7 +3387,8 @@
"description": "Extra large padding for content inside controls and surfaces.",
"components": [
"bulk-textarea",
- "card"
+ "card",
+ "radio-cards"
]
},
{
@@ -3412,6 +3424,7 @@
"dropdown-menu",
"inline-input",
"pagination",
+ "radio-cards",
"tag"
]
},
@@ -3428,6 +3441,7 @@
"input",
"pills",
"radio",
+ "radio-cards",
"select"
]
},
@@ -3443,6 +3457,7 @@
"notice-bubble",
"pagination",
"radio",
+ "radio-cards",
"select",
"tag"
]
@@ -3570,7 +3585,8 @@
"value": "6px",
"description": "Use for rounding Pills.",
"components": [
- "pills"
+ "pills",
+ "radio-cards"
]
},
{