diff --git a/.changeset/blue-dryers-knock.md b/.changeset/blue-dryers-knock.md
new file mode 100644
index 0000000000..c730042a6d
--- /dev/null
+++ b/.changeset/blue-dryers-knock.md
@@ -0,0 +1,7 @@
+---
+'@storefront-ui/react': minor
+'@storefront-ui/vue': minor
+'@storefront-ui/shared': minor
+---
+
+Added SfBadge component in react and vue
diff --git a/apps/docs/components/components/badge.md b/apps/docs/components/components/badge.md
new file mode 100644
index 0000000000..3aaaa2f25f
--- /dev/null
+++ b/apps/docs/components/components/badge.md
@@ -0,0 +1,97 @@
+---
+layout: AtomLayout
+hideBreadcrumbs: true
+---
+
+# Badge
+
+::: slot usage
+
+## Examples
+
+### Basic usage
+
+The badge component must be wrapped with a container that has `class="relative"`. You must provide such a container by yourself, but it gives you the flexibility to put the badge wherever you want. Bagde comes with a "dot" variant, which hides the content. When given content is of type number (or string that could be parsed to number), you can set a maximum limit of that number using `max` prop.
+
+
+
+<<<../../preview/nuxt/pages/showcases/Badge/BadgeBasic.vue
+
+
+<<<../../preview/next/pages/showcases/Badge/BadgeBasic.tsx#source
+
+
+
+### Placement
+
+You can align the Badge in every corner of the container.
+
+
+
+<<<../../preview/nuxt/pages/showcases/Badge/BadgePlacement.vue
+
+
+<<<../../preview/next/pages/showcases/Badge/BadgePlacement.tsx#source
+
+
+
+### Custom outline
+
+A nifty effect that makes the Badge a bit more attractive is to add an outline that separates the Badge from an element.
+
+
+
+<<<../../preview/nuxt/pages/showcases/Badge/BadgeOutline.vue
+
+
+<<<../../preview/next/pages/showcases/Badge/BadgeOutline.tsx#source
+
+
+
+### Avatars
+
+A common use case for the Badge is to place it on a user's avatar.
+
+
+
+<<<../../preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue
+
+
+<<<../../preview/next/pages/showcases/Badge/BadgeAvatar.tsx#source
+
+
+
+## Playground
+
+
+
+:::
+
+::: slot api
+
+## Props
+
+| Prop name | Type | Default value | Possible values |
+| ----------- | ------------------ | ------------- | ------------------------------------------------------ |
+| `content` | `string | number` | |
+| `max` | `number` | `99` | |
+| `placement` | `SfBadgePlacement` | `top-right` | `top-right`, `top-left`, `bottom-right`, `bottom-left` |
+| `variant` | `SfBadgeVariant` | `standard` | `standard`, `dot` |
+
+:::
+
+::: slot source
+
+
+
+
+<<<../../../packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue
+
+
+
+
+<<<../../../packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx
+
+
+
+:::
diff --git a/apps/preview/next/pages/examples/SfBadge.tsx b/apps/preview/next/pages/examples/SfBadge.tsx
new file mode 100644
index 0000000000..152c5128c5
--- /dev/null
+++ b/apps/preview/next/pages/examples/SfBadge.tsx
@@ -0,0 +1,58 @@
+import { SfBadge, SfButton, SfIconShoppingCart, SfBadgePlacement, SfBadgeVariant } from '@storefront-ui/react';
+import { prepareControls } from '../../components/utils/Controls';
+import ComponentExample from '../../components/utils/ComponentExample';
+import { ExamplePageLayout } from '../examples';
+
+function Example() {
+ const { state, controls } = prepareControls(
+ [
+ {
+ type: 'text',
+ modelName: 'content',
+ description: 'Content to display in the badge.',
+ propType: 'string | number',
+ },
+ {
+ type: 'text',
+ modelName: 'max',
+ description: 'Maximum number of counter to show.',
+ propType: 'number',
+ propDefaultValue: '99',
+ },
+ {
+ type: 'select',
+ modelName: 'variant',
+ description: 'Badge can have content or be a simple dot.',
+ options: Object.values(SfBadgeVariant),
+ propType: 'SfBadgeVariant',
+ propDefaultValue: 'standard',
+ },
+ {
+ type: 'select',
+ modelName: 'placement',
+ description: 'Position of the badge relatively to a container.',
+ options: Object.values(SfBadgePlacement),
+ propType: 'SfBadgePlacement',
+ propDefaultValue: 'top-right',
+ },
+ ],
+ {
+ content: '1',
+ max: 99,
+ variant: SfBadgeVariant.standard,
+ placement: SfBadgePlacement['top-right'],
+ },
+ );
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+Example.getLayout = ExamplePageLayout;
+export default Example;
diff --git a/apps/preview/next/pages/showcases/Badge/BadgeAvatar.tsx b/apps/preview/next/pages/showcases/Badge/BadgeAvatar.tsx
new file mode 100644
index 0000000000..a3343d249b
--- /dev/null
+++ b/apps/preview/next/pages/showcases/Badge/BadgeAvatar.tsx
@@ -0,0 +1,25 @@
+import { ShowcasePageLayout } from '../../showcases';
+// #region source
+import { SfBadge } from '@storefront-ui/react';
+
+export default function BadgeAvatar() {
+ return (
+
+ -
+
+

+
+
+
+ -
+
+

+
+
+
+
+ );
+}
+
+// #endregion source
+BadgeAvatar.getLayout = ShowcasePageLayout;
diff --git a/apps/preview/next/pages/showcases/Badge/BadgeBasic.tsx b/apps/preview/next/pages/showcases/Badge/BadgeBasic.tsx
new file mode 100644
index 0000000000..f0ff8f75d8
--- /dev/null
+++ b/apps/preview/next/pages/showcases/Badge/BadgeBasic.tsx
@@ -0,0 +1,32 @@
+import { ShowcasePageLayout } from '../../showcases';
+// #region source
+import { SfBadge, SfButton, SfIconShoppingCart } from '@storefront-ui/react';
+
+export default function BadgeBasic() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// #endregion source
+BadgeBasic.getLayout = ShowcasePageLayout;
diff --git a/apps/preview/next/pages/showcases/Badge/BadgeOutline.tsx b/apps/preview/next/pages/showcases/Badge/BadgeOutline.tsx
new file mode 100644
index 0000000000..9a9a3470f6
--- /dev/null
+++ b/apps/preview/next/pages/showcases/Badge/BadgeOutline.tsx
@@ -0,0 +1,34 @@
+import { ShowcasePageLayout } from '../../showcases';
+// #region source
+import { SfBadge, SfButton, SfIconShoppingCart } from '@storefront-ui/react';
+
+export default function BadgeOutline() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// #endregion source
+BadgeOutline.getLayout = ShowcasePageLayout;
diff --git a/apps/preview/next/pages/showcases/Badge/BadgePlacement.tsx b/apps/preview/next/pages/showcases/Badge/BadgePlacement.tsx
new file mode 100644
index 0000000000..ce9cffa89c
--- /dev/null
+++ b/apps/preview/next/pages/showcases/Badge/BadgePlacement.tsx
@@ -0,0 +1,32 @@
+import { ShowcasePageLayout } from '../../showcases';
+// #region source
+import { SfBadge, SfButton, SfIconShoppingCart } from '@storefront-ui/react';
+
+export default function BadgePlacement() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// #endregion source
+BadgePlacement.getLayout = ShowcasePageLayout;
diff --git a/apps/preview/nuxt/pages/examples/SfBadge.vue b/apps/preview/nuxt/pages/examples/SfBadge.vue
new file mode 100644
index 0000000000..c2a4815618
--- /dev/null
+++ b/apps/preview/nuxt/pages/examples/SfBadge.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue
new file mode 100644
index 0000000000..7a8e6cab5d
--- /dev/null
+++ b/apps/preview/nuxt/pages/showcases/Badge/BadgeAvatar.vue
@@ -0,0 +1,20 @@
+
+
+ -
+
+

+
+
+
+ -
+
+

+
+
+
+
+
+
+
diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgeBasic.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgeBasic.vue
new file mode 100644
index 0000000000..fe99b04b96
--- /dev/null
+++ b/apps/preview/nuxt/pages/showcases/Badge/BadgeBasic.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgeOutline.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgeOutline.vue
new file mode 100644
index 0000000000..73f70093a2
--- /dev/null
+++ b/apps/preview/nuxt/pages/showcases/Badge/BadgeOutline.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/preview/nuxt/pages/showcases/Badge/BadgePlacement.vue b/apps/preview/nuxt/pages/showcases/Badge/BadgePlacement.vue
new file mode 100644
index 0000000000..b101271f2f
--- /dev/null
+++ b/apps/preview/nuxt/pages/showcases/Badge/BadgePlacement.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/preview/shared/public/@assets/woman_avatar.png b/apps/preview/shared/public/@assets/woman_avatar.png
new file mode 100644
index 0000000000..9cf8911ab3
Binary files /dev/null and b/apps/preview/shared/public/@assets/woman_avatar.png differ
diff --git a/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx b/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx
new file mode 100644
index 0000000000..2936b4c64b
--- /dev/null
+++ b/packages/sfui/frameworks/react/components/SfBadge/SfBadge.tsx
@@ -0,0 +1,40 @@
+import { SfBadgePlacement } from '@storefront-ui/shared';
+import classNames from 'classnames';
+import type { SfBadgeProps } from './types';
+
+export default function SfBadge({
+ content,
+ variant,
+ max = 99,
+ placement = SfBadgePlacement['top-right'],
+ className,
+ ...attributes
+}: SfBadgeProps): JSX.Element {
+ const isDot = variant === 'dot';
+ let displayValue = content;
+ if (isDot) {
+ displayValue = '';
+ } else if (!Number.isNaN(content) && Number(content) > max) {
+ displayValue = `${max}+`;
+ }
+ return (
+
+ {displayValue}
+
+ );
+}
diff --git a/packages/sfui/frameworks/react/components/SfBadge/index.ts b/packages/sfui/frameworks/react/components/SfBadge/index.ts
new file mode 100644
index 0000000000..dada50f037
--- /dev/null
+++ b/packages/sfui/frameworks/react/components/SfBadge/index.ts
@@ -0,0 +1,3 @@
+export { default as SfBadge } from './SfBadge';
+
+export * from './types';
diff --git a/packages/sfui/frameworks/react/components/SfBadge/types.ts b/packages/sfui/frameworks/react/components/SfBadge/types.ts
new file mode 100644
index 0000000000..b5be16ab32
--- /dev/null
+++ b/packages/sfui/frameworks/react/components/SfBadge/types.ts
@@ -0,0 +1,11 @@
+import { SfBadgePlacement, SfBadgeVariant } from '@storefront-ui/shared';
+import type { PropsWithStyle } from '@storefront-ui/react';
+
+export { SfBadgePlacement, SfBadgeVariant };
+
+export interface SfBadgeProps extends PropsWithStyle {
+ content?: string | number;
+ max?: number;
+ placement?: `${SfBadgePlacement}`;
+ variant?: `${SfBadgeVariant}`;
+}
diff --git a/packages/sfui/frameworks/react/index.ts b/packages/sfui/frameworks/react/index.ts
index 3840d65295..099301d7f9 100644
--- a/packages/sfui/frameworks/react/index.ts
+++ b/packages/sfui/frameworks/react/index.ts
@@ -14,6 +14,7 @@ export * from './shared';
// Components
export * from './components/SfAccordionItem';
export * from './components/SfButton';
+export * from './components/SfBadge';
export * from './components/SfCheckbox';
export * from './components/SfChip';
export * from './components/SfCounter';
diff --git a/packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue b/packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue
new file mode 100644
index 0000000000..fd9ee57b0c
--- /dev/null
+++ b/packages/sfui/frameworks/vue/components/SfBadge/SfBadge.vue
@@ -0,0 +1,52 @@
+
+
+
+
+ {{ displayValue }}
+
+
diff --git a/packages/sfui/frameworks/vue/components/SfBadge/index.ts b/packages/sfui/frameworks/vue/components/SfBadge/index.ts
new file mode 100644
index 0000000000..6d448fd4bb
--- /dev/null
+++ b/packages/sfui/frameworks/vue/components/SfBadge/index.ts
@@ -0,0 +1,2 @@
+export * from './types';
+export { default as SfBadge } from './SfBadge.vue';
diff --git a/packages/sfui/frameworks/vue/components/SfBadge/types.ts b/packages/sfui/frameworks/vue/components/SfBadge/types.ts
new file mode 100644
index 0000000000..f7bbe8e8d3
--- /dev/null
+++ b/packages/sfui/frameworks/vue/components/SfBadge/types.ts
@@ -0,0 +1 @@
+export { SfBadgePlacement, SfBadgeVariant } from '@storefront-ui/shared';
diff --git a/packages/sfui/frameworks/vue/index.ts b/packages/sfui/frameworks/vue/index.ts
index 755d33b212..b847b120ef 100644
--- a/packages/sfui/frameworks/vue/index.ts
+++ b/packages/sfui/frameworks/vue/index.ts
@@ -13,6 +13,7 @@ export * from './shared';
// Components
export * from './components/SfAccordionItem';
+export * from './components/SfBadge';
export * from './components/SfButton';
export * from './components/SfCheckbox';
export * from './components/SfChip';
diff --git a/packages/sfui/shared/types/SfBadge.ts b/packages/sfui/shared/types/SfBadge.ts
new file mode 100644
index 0000000000..75d0fce717
--- /dev/null
+++ b/packages/sfui/shared/types/SfBadge.ts
@@ -0,0 +1,11 @@
+export enum SfBadgeVariant {
+ standard = 'standard',
+ dot = 'dot',
+}
+
+export enum SfBadgePlacement {
+ 'top-right' = 'top-right',
+ 'top-left' = 'top-left',
+ 'bottom-right' = 'bottom-right',
+ 'bottom-left' = 'bottom-left',
+}
diff --git a/packages/sfui/shared/types/index.ts b/packages/sfui/shared/types/index.ts
index 4e8a6652da..3d61ec6171 100644
--- a/packages/sfui/shared/types/index.ts
+++ b/packages/sfui/shared/types/index.ts
@@ -1,4 +1,5 @@
export * from './SfAlert';
+export * from './SfBadge';
export * from './SfButton';
export * from './SfChip';
export * from './SfCounter';
diff --git a/packages/tests/components/SfBadge/SfBadge.PageObject.ts b/packages/tests/components/SfBadge/SfBadge.PageObject.ts
new file mode 100644
index 0000000000..d7283bc554
--- /dev/null
+++ b/packages/tests/components/SfBadge/SfBadge.PageObject.ts
@@ -0,0 +1,13 @@
+import { BasePage } from '../../utils/BasePage';
+
+export default class SfBadgeBaseObject extends BasePage {
+ hasContent(content: string | number) {
+ this.container.contains(content);
+ return this;
+ }
+
+ hasNoContent() {
+ this.container.should('be.empty');
+ return this;
+ }
+}
diff --git a/packages/tests/components/SfBadge/SfBadge.cy.tsx b/packages/tests/components/SfBadge/SfBadge.cy.tsx
new file mode 100644
index 0000000000..8056f36c53
--- /dev/null
+++ b/packages/tests/components/SfBadge/SfBadge.cy.tsx
@@ -0,0 +1,109 @@
+///
+import React from 'react';
+import { SfBadgeVariant, SfBadgePlacement } from '@storefront-ui/react';
+import { mount, Wrapper, useComponent } from '../../utils/mount';
+
+const { vue: SfBadgeVue, react: SfBadgeReact } = useComponent('SfBadge');
+import SfBadgeBaseObject from './SfBadge.PageObject';
+
+describe('SfBadge', () => {
+ const page = () => new SfBadgeBaseObject('badge');
+
+ type InitializeComponentParams = {
+ content?: string | number;
+ variant?: SfBadgeVariant;
+ max?: number;
+ placement?: SfBadgePlacement;
+ };
+
+ const initializeComponent = ({
+ content,
+ variant = SfBadgeVariant.standard,
+ max = 99,
+ placement = SfBadgePlacement['top-left'],
+ }: InitializeComponentParams = {}) => {
+ return mount({
+ vue: {
+ component: SfBadgeVue,
+ props: {
+ content,
+ variant,
+ max,
+ placement,
+ },
+ },
+ react: ,
+ });
+ };
+
+ it('initial state', () => {
+ initializeComponent();
+ page().makeSnapshot();
+ });
+
+ describe('when prop variant is set to ', () => {
+ Object.values(SfBadgeVariant).forEach((componentVariant) => {
+ describe(`${componentVariant}`, () => {
+ it(`should render correct ${componentVariant} variant`, () => {
+ initializeComponent({ variant: componentVariant });
+
+ page().makeSnapshot();
+ });
+ });
+ });
+ });
+
+ describe('when prop placement is set to ', () => {
+ Object.values(SfBadgePlacement).forEach((componentPlacement) => {
+ describe(`${componentPlacement}`, () => {
+ it(`should render correct ${componentPlacement} variant`, () => {
+ initializeComponent({ placement: componentPlacement });
+
+ page().makeSnapshot();
+ });
+ });
+ });
+ });
+
+ describe('when prop max is set to', () => {
+ const maxValue = Math.floor(Math.random() * (99 - 20) + 22);
+ it(`should render content within ${maxValue}`, () => {
+ initializeComponent({ max: maxValue, content: 10 });
+
+ page().hasContent(10).makeSnapshot();
+ });
+ });
+
+ describe('when prop content is bigger than max value', () => {
+ const maxValue = Math.floor(Math.random() * 20);
+ it(`should render max value: ${maxValue}`, () => {
+ initializeComponent({ max: maxValue, content: 40 });
+
+ page().hasContent(maxValue).makeSnapshot();
+ });
+ });
+
+ describe('when prop content is set to string', () => {
+ it(`should render the passed string content `, () => {
+ initializeComponent({ content: 'test' });
+
+ page().hasContent('test').makeSnapshot();
+ });
+ });
+
+ describe('when prop content is set to number', () => {
+ it(`should render the passed number content `, () => {
+ initializeComponent({ content: 10 });
+
+ page().hasContent(10).makeSnapshot();
+ });
+ });
+
+ describe('when prop variant is set to dot', () => {
+ it(`should not render any content`, () => {
+ initializeComponent({ variant: SfBadgeVariant.dot, content: 10 });
+
+ page().hasNoContent().makeSnapshot();
+ });
+ });
+});