Repository: marigold-ui/marigold
Pull request #1983 (2022-04-11 11:30:52)

fix(docs): remove old Field component and replace with Textfield


Patch:
diff --git a/.changeset/cyan-ligers-sin.md b/.changeset/cyan-ligers-sin.md
new file mode 100644
index 000000000..5abb17fc3
--- /dev/null
+++ b/.changeset/cyan-ligers-sin.md
@@ -0,0 +1,5 @@
+---
+"@marigold/system": patch
+---
+
+feat: transform state to corresponding css selector and data-attr
diff --git a/.changeset/modern-berries-refuse.md b/.changeset/modern-berries-refuse.md
new file mode 100644
index 000000000..d0378baac
--- /dev/null
+++ b/.changeset/modern-berries-refuse.md
@@ -0,0 +1,9 @@
+---
+"@marigold/docs": patch
+"@marigold/components": patch
+"@marigold/theme-b2b": patch
+"@marigold/theme-core": patch
+"@marigold/theme-unicorn": patch
+---
+
+refa: Badge uses new styling solution
diff --git a/docs/content/components/badge.mdx b/docs/content/components/badge.mdx
index f45b64b1a..bfdfb45f6 100644
--- a/docs/content/components/badge.mdx
+++ b/docs/content/components/badge.mdx
@@ -7,17 +7,7 @@ title: Badge
 A badge tags an element with a short note and color. The content of the badge can be anything but preferably a string and/or icon.
 
 <MarigoldTheme>
-  <Inline space="xsmall">
-    <Badge bgColor="info">
-      <Text color="white">Info</Text>
-    </Badge>
-    <Badge bgColor="success">
-      <Text color="white">Success</Text>
-    </Badge>
-    <Badge bgColor="error">
-      <Text color="white">Error</Text>
-    </Badge>
-  </Inline>
+  <Badge variant="info">New</Badge>
 </MarigoldTheme>
 
 ```tsx onlyCode
@@ -26,34 +16,16 @@ import { Badge } from '@marigold/components';
 
 ## Props
 
-| Property                 | Type     | Default       |
-| :----------------------- | :------- | :------------ |
-| `variant` (optional)     | `string` | `__default`   |
-| `borderColor` (optional) | `string` | `transparent` |
-| `bgColor` (optional)     | `string` | `inherit`     |
+| Property             | Type     | Default     |
+| :------------------- | :------- | :---------- |
+| `variant` (optional) | `string` | `__default` |
 
 ## Examples
 
-### Background color
-
-```tsx expandCode
-<Badge bgColor="error">
-  <Text color="white">Error</Text>
-</Badge>
-```
-
-Border color
-
-```tsx
-<Badge borderColor="info" bgColor="white">
-  <Text color="info">New</Text>
-</Badge>
-```
-
 ### Badge with icon
 
 ```tsx
-<Badge borderColor="secondary">
+<Badge>
   <Inline space="xsmall">
     type:documentation
     <ResaleLogbook size={18} />
diff --git a/docs/content/components/card.mdx b/docs/content/components/card.mdx
index 869acb1d0..f0a31c3f6 100644
--- a/docs/content/components/card.mdx
+++ b/docs/content/components/card.mdx
@@ -60,8 +60,8 @@ import { Card } from '@marigold/components';
 
 ```tsx
 <Card title="Name">
-  <Field label="Firstname" htmlFor="myId1" />
-  <Field label="Lastname" htmlFor="myId2" />
+  <TextField label="Firstname" />
+  <TextField label="Lastname" />
 </Card>
 ```
 
diff --git a/docs/src/theme/index.ts b/docs/src/theme/index.ts
index 1abebb833..791034b70 100644
--- a/docs/src/theme/index.ts
+++ b/docs/src/theme/index.ts
@@ -46,4 +46,5 @@ export const theme: Theme = {
   },
   ...typography,
   ...components,
+  components: {},
 } as const;
diff --git a/packages/components/src/Badge/Badge.stories.tsx b/packages/components/src/Badge/Badge.stories.tsx
index 46fc0052b..afe570481 100644
--- a/packages/components/src/Badge/Badge.stories.tsx
+++ b/packages/components/src/Badge/Badge.stories.tsx
@@ -2,7 +2,6 @@ import React from 'react';
 import type { Meta, ComponentStory } from '@storybook/react';
 
 import { Badge } from './Badge';
-import { Check } from '@marigold/icons';
 
 export default {
   title: 'Components/Badge',
@@ -14,25 +13,14 @@ export default {
       },
       defaultValue: '',
     },
-    bgColor: {
-      description: 'background color',
+    children: {
       control: {
         type: 'text',
       },
-      defaultValue: 'primary',
-    },
-    borderColor: {
-      description: 'outline color',
-      control: {
-        type: 'text',
-      },
-      defaultValue: 'transparent',
+      description: 'Contents of the badge',
+      defaultValue: 'new',
     },
   },
 } as Meta;
 
-export const Basic: ComponentStory<typeof Badge> = args => (
-  <Badge {...args}>
-    <Check /> Check
-  </Badge>
-);
+export const Basic: ComponentStory<typeof Badge> = args => <Badge {...args} />;
diff --git a/packages/components/src/Badge/Badge.tsx b/packages/components/src/Badge/Badge.tsx
index 3552f4c9f..8301f66df 100755
--- a/packages/components/src/Badge/Badge.tsx
+++ b/packages/components/src/Badge/Badge.tsx
@@ -1,37 +1,34 @@
 import React from 'react';
 import { ComponentProps } from '@marigold/types';
+import { useComponentStyles } from '@marigold/system';
+
 import { Box } from '../Box';
 
 // Theme Extension
 // ---------------
 export interface BadgeThemeExtension<Value> {
-  badge?: {
-    [key: string]: Value;
+  Badge?: {
+    base: Value;
+    variant?: {
+      [key: string]: Value;
+    };
   };
 }
 
 // Props
 // ---------------
 export interface BadgeProps extends ComponentProps<'div'> {
+  children?: React.ReactNode;
   variant?: string;
-  bgColor?: string;
-  borderColor?: string;
 }
 
 // Component
 // ---------------
-export const Badge: React.FC<BadgeProps> = ({
-  variant = '',
-  bgColor = 'transparent',
-  borderColor = 'transparent',
-  children,
-  ...props
-}) => (
-  <Box
-    css={{ bg: bgColor, borderColor: borderColor }}
-    variant={`badge.${variant}`}
-    {...props}
-  >
-    {children}
-  </Box>
-);
+export const Badge = ({ variant, children, ...props }: BadgeProps) => {
+  const styles = useComponentStyles('Badge', { variant });
+  return (
+    <Box {...props} css={styles}>
+      {children}
+    </Box>
+  );
+};
diff --git a/packages/components/src/theme.ts b/packages/components/src/theme.ts
index 266d7fba5..ba4125c5f 100644
--- a/packages/components/src/theme.ts
+++ b/packages/components/src/theme.ts
@@ -27,10 +27,11 @@ import { type TextAreaThemeExtension } from './TextArea';
 import { type TooltipThemeExtension } from './Tooltip';
 import { type ValidationMessageThemeExtension } from './ValidationMessage';
 
+interface ComponentStyles extends BadgeThemeExtension<CSSObject> {}
+
 export interface Theme
   extends Scales,
     RootThemeExtension<CSSObject>,
-    BadgeThemeExtension<CSSObject>,
     ButtonThemeExtension<CSSObject>,
     CardThemeExtension<CSSObject>,
     CheckboxThemeExtension<CSSObject>,
@@ -49,4 +50,6 @@ export interface Theme
     TextThemeExtension<CSSObject>,
     TextAreaThemeExtension<CSSObject>,
     TooltipThemeExtension<CSSObject>,
-    ValidationMessageThemeExtension<CSSObject> {}
+    ValidationMessageThemeExtension<CSSObject> {
+  components: ComponentStyles;
+}
diff --git a/packages/create-theme/src/create-theme.ts b/packages/create-theme/src/create-theme.ts
index a8af7f41d..586bd0385 100644
--- a/packages/create-theme/src/create-theme.ts
+++ b/packages/create-theme/src/create-theme.ts
@@ -1,4 +1,4 @@
-import type { ScaleValue, Theme as Scales } from '@marigold/system';
+import type { ScaleValue } from '@marigold/system';
 import type { Theme } from '@marigold/components';
 
 import * as Token from '@marigold/tokens';
@@ -15,6 +15,8 @@ export const base: Theme = {
 
   zIndices: Token.layer,
   opacities: Token.opacity,
+
+  components: {},
 };
 
 type AvailableColor = keyof typeof Token.color;
@@ -41,7 +43,7 @@ export interface ThemeConfig {
    */
   dimensions?: 'fixed' | 'fluid';
 
-  components?: Omit<Theme, keyof Scales>;
+  components?: Theme['components'];
 }
 
 /*#__PURE__*/
@@ -50,7 +52,7 @@ export const createTheme = ({
   fonts: configFonts,
   typography = 'fixed',
   dimensions = 'fixed',
-  components,
+  components = {},
 }: ThemeConfig): Theme => {
   // Create colors (pick from Token or custom)
   let colors: Theme['colors'] = {};
@@ -81,6 +83,6 @@ export const createTheme = ({
     fontSizes: Token.typography.size[typography],
 
     // Add component styles
-    ...components,
+    components,
   };
 };
diff --git a/packages/system/src/components/Box/Box.stories.tsx b/packages/system/src/components/Box/Box.stories.tsx
new file mode 100644
index 000000000..3c31d47b4
--- /dev/null
+++ b/packages/system/src/components/Box/Box.stories.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import type { Meta, ComponentStory } from '@storybook/react';
+import { Box } from './Box';
+
+export default {
+  title: 'Components/Box',
+  argTypes: {
+    children: {
+      control: {
+        type: 'text',
+      },
+      description: 'The content of the box',
+      defaultValue: 'I am a box!',
+    },
+    as: {
+      control: {
+        type: 'text',
+      },
+      description: 'The element to render',
+      defaultValue: 'div',
+    },
+    variant: {
+      control: {
+        type: 'text',
+      },
+      description: 'The variant of the box',
+    },
+    css: {
+      control: {
+        type: 'object',
+      },
+      description: 'The css object to apply to the box',
+      defaultValue: {
+        width: 100,
+        borderRadius: 5,
+        px: 8,
+        py: 2,
+        marginInline: 'auto',
+        color: '#0b7285',
+        bg: '#99e9f2',
+        cursor: 'pointer',
+        '&:hover': {
+          bg: '#c5f6fa',
+          transform: 'scale(2)',
+        },
+      },
+    },
+  },
+} as Meta;
+
+export const Basic: ComponentStory<typeof Box> = args => <Box {...args} />;
diff --git a/packages/system/src/components/Box/Box.tsx b/packages/system/src/components/Box/Box.tsx
index 16d806abc..ccd1606a7 100644
--- a/packages/system/src/components/Box/Box.tsx
+++ b/packages/system/src/components/Box/Box.tsx
@@ -1,6 +1,8 @@
+import { forwardRef } from 'react';
 import { jsx, Theme } from '@emotion/react';
 import { css as transformStyleObject } from '@theme-ui/css';
-import { forwardRef } from 'react';
+import merge from 'deepmerge';
+
 import {
   PolymorphicPropsWithRef,
   PolymorphicComponentWithRef,
@@ -9,6 +11,7 @@ import {
 import { getNormalizedStyles } from '../../normalize';
 import { CSSObject } from '../../types/system';
 import { ensureArrayVariant } from '../../variant';
+import { transformPseudos } from './utils';
 
 export interface StyleProps
   extends Pick<
@@ -65,12 +68,6 @@ export interface BoxOwnProps extends StyleProps {
 
 export interface BoxProps extends PolymorphicPropsWithRef<BoxOwnProps, 'div'> {}
 
-/**
- * Check if there is any falsy value or empty object
- */
-const isNotEmpty = (val: any) =>
-  !(val && Object.keys(val).length === 0 && val.constructor === Object);
-
 interface CreateStyleProps {
   as?: BoxProps['as'];
   __baseCSS?: BoxOwnProps['__baseCSS'];
@@ -82,7 +79,7 @@ interface CreateStyleProps {
 const createThemedStyle =
   ({ as, __baseCSS, variant, styles, css }: CreateStyleProps) =>
   (theme: Theme) => {
-    return [
+    const themedStyles = merge.all([
       getNormalizedStyles(as),
       transformStyleObject(__baseCSS)(theme),
       ...ensureArrayVariant(variant).map(v =>
@@ -90,7 +87,9 @@ const createThemedStyle =
       ),
       transformStyleObject(styles)(theme),
       transformStyleObject(css)(theme),
-    ].filter(isNotEmpty);
+    ]) as CSSObject;
+
+    return transformPseudos(themedStyles);
   };
 
 export const Box: PolymorphicComponentWithRef<BoxOwnProps, 'div'> = forwardRef(
diff --git a/packages/system/src/components/Box/utils.ts b/packages/system/src/components/Box/utils.ts
new file mode 100644
index 000000000..461da1e1b
--- /dev/null
+++ b/packages/system/src/components/Box/utils.ts
@@ -0,0 +1,40 @@
+import { StyleObject } from '../../types';
+
+const pseudos = {
+  '&:hover': '&:hover, &[data-hover]',
+  '&:focus': '&:focus, &[data-focus]',
+  '&:active': '&:active, &[data-active]',
+  '&:disabled': '&[disabled], &[aria-disabled=true], &[data-disabled]',
+  '&:read-only': '&[readonly], &[aria-readonly=true], &[data-readonly]',
+  '&:checked': '&[aria-checked=true], &[data-checked]',
+  '&:indeterminate':
+    '&:indeterminate, &[aria-checked=mixed], &[data-indeterminate]',
+  '&:invalid': '&:invalid, &[aria-invalid=true], &[data-invalid]',
+} as const;
+
+/**
+ * Transform a states (hover, focus, ...) in a StyleObject to
+ * our format (pseudo selector plus corresponding data-attribute).
+ *
+ * We stole this idea from https://chakra-ui.com/.
+ */
+export const transformPseudos = (styles: StyleObject) => {
+  let result: StyleObject = {};
+
+  for (let key in styles) {
+    const value = styles[key as keyof StyleObject];
+
+    if (key in pseudos) {
+      key = pseudos[key as keyof typeof pseudos];
+    }
+
+    if (typeof value === 'object') {
+      result[key] = transformPseudos(value);
+      continue;
+    }
+
+    result[key] = value;
+  }
+
+  return result;
+};
diff --git a/packages/system/src/hooks/useComponentStyles.ts b/packages/system/src/hooks/useComponentStyles.ts
index 447c2f6ba..90252bc19 100644
--- a/packages/system/src/hooks/useComponentStyles.ts
+++ b/packages/system/src/hooks/useComponentStyles.ts
@@ -9,17 +9,6 @@ import { useTheme } from './useTheme';
 // ---------------
 type IndexObject = { [key: string]: any };
 
-export type ComponentState =
-  | 'hover'
-  | 'focus'
-  | 'active'
-  | 'visited'
-  | 'disabled'
-  | 'readOnly'
-  | 'checked'
-  | 'indeterminate'
-  | 'error';
-
 // Helper
 // ---------------
 /**
@@ -41,35 +30,11 @@ const get = (obj: object, path: string, fallback?: any): any => {
   return result === undefined ? fallback : result;
 };
 
-/**
- * Convert an object of states, where the key is the state name and
- * the value is a boolean, to an array of strings.
- */
-const statesToFlags = ({
-  disabled,
-  ...states
-}: { [key in ComponentState]?: boolean } = {}): ComponentState[] => {
-  let flags = Object.keys(states).filter(
-    key => states[key as keyof typeof states]
-  ) as ComponentState[];
-
-  /**
-   * Adding `disabled` at the end of the array so that it
-   * will be the most prominent state and override the others.
-   */
-  if (disabled) {
-    flags.push('disabled');
-  }
-
-  return flags;
-};
-
 // Hook
 // ---------------
 export interface ComponentStylesProps {
   variant?: string;
   size?: string;
-  states?: { [key in ComponentState]?: boolean };
 }
 
 export function useComponentStyles(
@@ -108,18 +73,16 @@ export function useComponentStyles(
     const base = componentStyles.base || {};
     const size = componentStyles.size?.[props.size as any] || {};
     const variant = componentStyles.variant?.[props.variant as any] || {};
-    const states = statesToFlags(props.states).map(
-      state => componentStyles.state?.[state] || {}
-    );
 
     // We deep merge so that parts (if they exists) also get put together
-    const styles = merge.all([base, size, ...states, variant]) as IndexObject;
+    let styles = merge.all([base, size, variant]) as IndexObject;
 
-    // If a part does not exists in the theme, well add an empty object
+    // Only return requested parts. If they don't exist, set them as empty object
     if (options.parts) {
-      options.parts.forEach((part: string) => {
-        styles[part] = styles[part] ?? {};
-      });
+      styles = options.parts.reduce((result: IndexObject, part: string) => {
+        result[part] = styles[part] || {};
+        return result;
+      }, {});
     }
 
     if (!isEqual(stylesRef.current, styles)) {
diff --git a/packages/system/src/hooks/useStateProps.ts b/packages/system/src/hooks/useStateProps.ts
new file mode 100644
index 000000000..693e5c9cc
--- /dev/null
+++ b/packages/system/src/hooks/useStateProps.ts
@@ -0,0 +1,45 @@
+import { useRef } from 'react';
+import isEqual from 'react-fast-compare';
+
+export type ComponentState =
+  | 'hover'
+  | 'focus'
+  | 'active'
+  | 'visited'
+  | 'disabled'
+  | 'readOnly'
+  | 'checked'
+  | 'indeterminate'
+  | 'invalid';
+
+export type StateAttrsKey = `data-${Lowercase<ComponentState>}`;
+
+export type StateAttrs = {
+  [key in StateAttrsKey]?: '';
+};
+
+/**
+ * Given a map of states (e.g. `{ hover: true, focus: false }`) returns an
+ * object that can be used to set state props on a component
+ * (e.g. `[data-hover]` and `[data-focus]`).
+ */
+export const useStateProps = (states: {
+  [key in ComponentState]?: boolean;
+}): StateAttrs => {
+  // Store states in ref to prevent re-computation
+  const statePropsRef = useRef({});
+
+  let stateProps: StateAttrs = {};
+  for (let state in states) {
+    if (states[state as ComponentState]) {
+      const key = `data-${state.toLocaleLowerCase()}` as StateAttrsKey;
+      stateProps[key] = '';
+    }
+  }
+
+  if (!isEqual(statePropsRef.current, stateProps)) {
+    statePropsRef.current = stateProps;
+  }
+
+  return statePropsRef.current;
+};
diff --git a/themes/create-theme/CHANGELOG.md b/themes/create-theme/CHANGELOG.md
deleted file mode 100644
index c09092187..000000000
--- a/themes/create-theme/CHANGELOG.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# @marigold/create-theme
-
-## 1.0.0
-
-### Patch Changes
-
-- [#1809](https://github.com/marigold-ui/marigold/pull/1809) [`539d4198`](https://github.com/marigold-ui/marigold/commit/539d41987118db125ee37a4a83231335ea15830a) Thanks [@sebald](https://github.com/sebald)! - feat(create-theme): Introduce helper package to create themes with sensible defaults
-
-- Updated dependencies [[`4cc0ad3b`](https://github.com/marigold-ui/marigold/commit/4cc0ad3b85b993e01b4d85b6f30b1a81cdee2351), [`d162a6a8`](https://github.com/marigold-ui/marigold/commit/d162a6a8d1f9d99bfc08d59f6dae294a6fa95310), [`539d4198`](https://github.com/marigold-ui/marigold/commit/539d41987118db125ee37a4a83231335ea15830a), [`e053b7b9`](https://github.com/marigold-ui/marigold/commit/e053b7b903f02c56cf10e6b9aecbedd29399895d), [`527ba94f`](https://github.com/marigold-ui/marigold/commit/527ba94fa1a3255dc6f846fcc9def978ec906bf3), [`b55e10bc`](https://github.com/marigold-ui/marigold/commit/b55e10bc7119554373d43f9d8872346c1e6d6c39), [`9a95a6f5`](https://github.com/marigold-ui/marigold/commit/9a95a6f541dec34e27eac8557186f929e811c07c), [`f62033b6`](https://github.com/marigold-ui/marigold/commit/f62033b642a623a75f311af701febc4b54f77120), [`21f72b34`](https://github.com/marigold-ui/marigold/commit/21f72b3499c7ed2f88a0b96315f0374140e98e85), [`25c8675e`](https://github.com/marigold-ui/marigold/commit/25c8675ed470cc71f8f968a83b9781b66412c493)]:
-  - @marigold/system@0.6.0
-  - @marigold/components@0.6.0
diff --git a/themes/theme-b2b/src/component.ts b/themes/theme-b2b/src/component.ts
new file mode 100644
index 000000000..5860928f2
--- /dev/null
+++ b/themes/theme-b2b/src/component.ts
@@ -0,0 +1,604 @@
+import { colors } from "./colors";
+import { text } from "./typography";
+
+/**
+ * Select component
+ */
+const selectButton = {
+  appearance: "none",
+  position: "relative",
+  display: "inline-flex",
+  alignItems: "center",
+  justifyContent: "space-between",
+  overflow: "hidden",
+  outline: "none",
+  width: "100%",
+  lineHeight: "large",
+  px: "xsmall",
+  color: "inherit",
+  bg: "transparent",
+} as const;
+
+const selectOption = {
+  fontFamily: "body",
+  fontSize: "xsmall",
+  fontWeight: "body",
+  lineHeight: "large",
+  outline: "none",
+  cursor: "pointer",
+  color: "text",
+  px: "xsmall",
+  listStyle: "none",
+} as const;
+
+const select = {
+  __default: {
+    fontFamily: "body",
+    fontSize: "xsmall",
+    fontWeight: "body",
+    lineHeight: "large",
+    color: "text",
+  },
+  disabled: {
+    fontFamily: "body",
+    fontSize: "xsmall",
+    fontWeight: "body",
+    lineHeight: "large",
+    color: "disabled",
+    cursor: "not-allowed",
+  },
+  listbox: {
+    __default: {
+      bg: "gray00",
+      border: "1px solid transparent",
+      borderLeftColor: "gray40",
+      borderRightColor: "gray40",
+      borderBottomColor: "gray40",
+      borderBottomRightRadius: "small",
+      borderBottomLeftRadius: "small",
+    },
+    error: {
+      bg: "gray00",
+      border: "1px solid transparent",
+      borderLeftColor: "error",
+      borderRightColor: "error",
+      borderBottomColor: "error",
+      borderBottomLeftRadius: "small",
+      borderBottomRightRadius: "small",
+    },
+  },
+  section: {
+    fontFamily: "body",
+    fontSize: "xsmall",
+    fontWeight: "body",
+    lineHeight: "large",
+    px: "xxsmall",
+    color: "gray50",
+  },
+  option: {
+    __default: {
+      ...selectOption,
+      ":focus": {
+        bg: "blue20",
+      },
+    },
+    selected: {
+      ...selectOption,
+      color: "gray00",
+      bg: "blue60",
+    },
+    disabled: {
+      ...selectOption,
+      cursor: "not-allowed",
+      color: "gray40",
+    },
+  },
+} as const;
+
+/**
+ * Button component
+ */
+const button = {
+  root: {
+    position: "relative",
+    fontFamily: "body",
+    fontSize: "xsmall",
+    fontWeight: "body",
+    border: "none",
+    borderRadius: "small",
+    display: "inline-flex",
+  },
+  primary: {
+    color: "background",
+    bg: "primary",
+    ":hover": {
+      color: "background",
+      bg: "orange40",
+      cursor: "pointer",
+    },
+    ":disabled": {
+      color: "gray40",
+      bg: "gray20",
+      cursor: "not-allowed",
+    },
+  },
+  secondary: {
+    color: "background",
+    bg: "secondary",
+    ":hover": {
+      color: "background",
+      bg: "gray60",
+      cursor: "pointer",
+    },
+    ":disabled": {
+      color: "gray40",
+      bg: "gray20",
+      cursor: "not-allowed",
+    },
+  },
+  ghost: {
+    color: "secondary",
+    border: "1px solid",
+    outlineColor: "gray70",
+    ":hover": {
+      color: "secondary",
+      bg: "gray30",
+      cursor: "pointer",
+    },
+    ":disabled": {
+      color: "disabled",
+      bg: "gray00",
+      outlineColor: "disabled",
+      cursor: "not-allowed",
+    },
+  },
+  text: {
+    color: "secondary",
+    ":hover": {
+      color: "secondary",
+      outlineColor: "gray70",
+      bg: "gray30",
+      cursor: "pointer",
+    },
+    ":disabled": {
+      color: "disabled",
+      bg: "gray00",
+      cursor: "not-allowed",
+      outline: "none",
+    },
+  },
+  menu: {
+    color: "secondary",
+    bg: "background",
+    ":hover": {
+      color: "background",
+      bg: "gray60",
+      cursor: "pointer",
+    },
+    ":disabled": {
+      color: "gray40",
+      bg: "gray20",
+      cursor: "not-allowed",
+    },
+  },
+  select: {
+    ...selectButton,
+    border: "1px solid transparent",
+    borderColor: "gray40",
+    ":hover": {
+      cursor: "pointer",
+    },
+    ":focus": {
+      boxShadow: "0 0 0 2px " + colors.blue60,
+      border: "1px solid transparent",
+    },
+    ":disabled": {
+      bg: "gray20",
+      color: "disabled",
+      cursor: "not-allowed",
+    },
+    error: {
+      ...selectButton,
+      border: "1px solid",
+      borderRadius: "small",
+      borderColor: "error",
+    },
+    open: {
+      ...selectButton,
+      border: "1px solid transparent",
+      borderTopColor: "gray40",
+      borderLeftColor: "gray40",
+      borderRightColor: "gray40",
+      borderTopRightRadius: "small",
+      borderTopLeftRadius: "small",
+    },
+    errorOpened: {
+      ...selectButton,
+      border: "1px solid transparent",
+      borderTopColor: "error",
+      borderLeftColor: "error",
+      borderRightColor: "error",
+      borderTopRightRadius: "small",
+      borderTopLeftRadius: "small",
+    },
+  },
+} as const;
+
+/**
+ * Slider component
+ */
+const sliderThumb = {
+  WebkitAppearance: "none",
+  boxSizing: "border-box",
+  border: "4px solid",
+  borderColor: "gray70",
+  width: 16,
+  height: 16,
+  bg: "gray00",
+  borderRadius: "large",
+  cursor: "pointer",
+  marginTop: -4,
+} as const;
+
+const sliderTrack = {
+  WebkitAppearance: "none",
+  width: "100%",
+  height: 8,
+  bg: "gray30",
+  borderRadius: "large",
+  border: "none",
+} as const;
+
+const slider = {
+  __default: {
+    // styles need to be applied to range inputs in all browsers to override their basic appearance.
+    WebkitAppearance: "none",
+    bg: "transparent",
+    borderColor: "transparent",
+    color: "transparent",
+    ":focus": {
+      outline: "none",
+    },
+    // chrome, safari, opera (theres actually no webkit option to style the progress bar like in firefox)
+    "&::-webkit-slider-thumb": {
+      ...sliderThumb,
+    },
+    "&:focus::-webkit-slider-thumb": {
+      ...sliderThumb,
+      border: "4px solid",
+      borderColor: "primary",
+    },
+    "&:disabled::-webkit-slider-thumb": {
+      ...sliderThumb,
+      border: "4px solid",
+      borderColor: "gray40",
+      bg: "gray40",
+    },
+    "&::-webkit-slider-runnable-track": {
+      ...sliderTrack,
+    },
+    "&:focus::-webkit-slider-runnable-track": {
+      ...sliderTrack,
+      backgroundColor: "primary",
+    },
+    "&:disabled::-webkit-slider-runnable-track": {
+      ...sliderTrack,
+      bg: "gray40",
+    },
+    "&::-webkit-progress-value": {
+      bg: "green60",
+    },
+    // firefox
+    "&::-moz-range-thumb": {
+      ...sliderThumb,
+    },
+    "&:focus::-moz-range-thumb": {
+      ...sliderThumb,
+      border: "4px solid",
+      borderColor: "primary",
+    },
+    "&:disabled::-moz-range-thumb": {
+      ...sliderThumb,
+      border: "4px solid",
+      borderColor: "gray40",
+      bg: "gray40",
+    },
+    "&::-moz-range-track": {
+      ...sliderTrack,
+    },
+    "&::-moz-range-progress": {
+      ...sliderTrack,
+      bg: "gray70",
+    },
+    "&:focus::-moz-range-progress": {
+      ...sliderTrack,
+      backgroundColor: "primary",
+    },
+    "&:disabled::-moz-range-progress": {
+      ...sliderTrack,
+      bg: "gray40",
+    },
+  },
+} as const;
+
+export const component = {
+  button: {
+    __default: {
+      p: 0,
+      border: "none",
+    },
+    large: {
+      lineHeight: "xxlarge",
+      paddingX: "xlarge",
+    },
+    small: {
+      lineHeight: "large",
+      paddingX: "medium",
+    },
+    primary: {
+      ...button.root,
+      ...button.primary,
+    },
+    secondary: {
+      ...button.root,
+      ...button.secondary,
+    },
+    ghost: {
+      ...button.root,
+      ...button.ghost,
+    },
+    text: {
+      ...button.root,
+      ...button.text,
+    },
+    menu: {
+      ...button.root,
+      ...button.menu,
+    },
+    select: {
+      ...button.root,
+      ...button.select,
+    },
+  },
+  card: {
+    __default: {
+      maxWidth: 500,
+      bg: "gray00",
+      p: "small",
+      boxShadow: "0px 4px 4px rgba(165, 165, 165, 0.25)",
+      borderRadius: "large",
+    },
+  },
+  checkbox: {
+    __default: {
+      fill: "gray00",
+      stroke: "gray40",
+    },
+    ":checked": {
+      fill: "primary",
+      stroke: "orange80",
+    },
+    ":disabled": {
+      fill: "gray30",
+    },
+    ":error": {
+      stroke: "error",
+    },
+  },
+  dialog: {
+    __default: {
+      bg: "background",
+      minWidth: 510,
+      minHeight: 240,
+    },
+    backdrop: {
+      bg: "#00000080",
+    },
+  },
+  divider: {
+    __default: {
+      my: "xsmall",
+    },
+    bold: {
+      my: "xsmall",
+      height: "2px",
+    },
+    section: {
+      my: "xxsmall",
+      bg: "gray50",
+    },
+  },
+  helpText: {
+    error: {
+      ...text.root,
+      fontSize: "xxsmall",
+      color: "error",
+    },
+    description: {
+      ...text.root,
+      fontSize: "xxsmall",
+      color: "text",
+    },
+  },
+  image: {
+    fullWidth: {
+      maxWidth: "100%",
+      height: "auto",
+    },
+  },
+  input: {
+    __default: {
+      color: "text",
+      border: "none",
+      borderRadius: "small",
+      boxShadow: "0 0 0 1px " + colors.gray40,
+      outline: "none",
+      padding: "0 8px",
+      lineHeight: "large",
+      ":focus": {
+        boxShadow: "0 0 0 2px " + colors.blue60,
+      },
+      ":disabled": {
+        bg: "gray20",
+        color: "gray40",
+        cursor: "not-allowed",
+      },
+    },
+    error: {
+      color: "text",
+      border: "none",
+      borderRadius: "small",
+      boxShadow: "0 0 0 1px " + colors.red60,
+      outline: "none",
+      padding: "0 8px",
+      lineHeight: "large",
+    },
+  },
+  label: {
+    above: {
+      fontSize: "xxsmall",
+      color: "text",
+    },
+    inline: {
+      fontSize: "xsmall",
+      display: "inline-flex",
+      alignItems: "center",
+    },
+    section: {
+      fontSize: "small",
+      lineHeight: "large",
+      color: "gray50",
+    },
+  },
+  link: {
+    __default: {
+      color: "blue60",
+      ":hover": {
+        textDecoration: "none",
+      },
+    },
+    menuItemLink: {
+      color: "text",
+      textDecoration: "none",
+    },
+  },
+  menuItem: {
+    default: {
+      display: "block",
+      fontFamily: "body",
+      fontSize: "xsmall",
+      fontWeight: "body",
+      padding: "xsmall",
+      bg: "background",
+      color: "text",
+      ":hover": {
+        bg: "gray30",
+        cursor: "pointer",
+      },
+    },
+  },
+  message: {
+    warning: {
+      borderStyle: "solid",
+      borderColor: "warning",
+      borderWidth: "2px 2px 2px 16px",
+      pt: "xsmall",
+      pb: "small",
+      px: "small",
+      color: "warning",
+    },
+    error: {
+      borderStyle: "solid",
+      borderColor: "error",
+      borderWidth: "2px 2px 2px 16px",
+      pt: "xsmall",
+      pb: "small",
+      px: "small",
+      color: "error",
+    },
+    info: {
+      borderStyle: "solid",
+      borderColor: "info",
+      borderWidth: "2px 2px 2px 16px",
+      pt: "xsmall",
+      pb: "small",
+      px: "small",
+      color: "info",
+    },
+    title: {
+      mb: "xsmall",
+    },
+  },
+  radio: {
+    __default: {
+      fill: "gray00",
+      stroke: "gray40",
+    },
+    ":disabled": {
+      fill: "gray30",
+    },
+    ":error": {
+      stroke: "error",
+    },
+    ":checked": {
+      fill: "primary",
+      stroke: "orange80",
+    },
+  },
+  select,
+  slider,
+  switch: {
+    __default: {
+      fill: "gray20",
+      stroke: "gray40",
+    },
+    ":checked": {
+      fill: "primary",
+      stroke: "orange80",
+    },
+    ":disabled": {
+      fill: "gray30",
+      stroke: "gray40",
+    },
+  },
+  textArea: {
+    __default: {
+      fontFamily: "body",
+      lineHeight: "medium",
+      py: "xxsmall",
+      px: "xsmall",
+      color: "text",
+      border: "none",
+      borderRadius: "small",
+      outline: "1px solid",
+      outlineColor: "disabled",
+      ":focus": {
+        outline: "2px solid",
+        outlineColor: "blue60",
+      },
+      ":disabled": {
+        cursor: "not-allowed",
+        color: "disabled",
+        bg: "gray20",
+      },
+    },
+  },
+  tooltip: {
+    __default: {
+      p: "xsmall",
+      color: "gray90",
+      fontSize: 14,
+      border: "1px solid",
+      borderColor: "blue70",
+      borderRadius: "large",
+      bg: "blue10",
+    },
+  },
+  validation: {
+    error: {
+      ...text.root,
+      fontSize: "xxsmall",
+      color: "error",
+    },
+  },
+} as const;
diff --git a/themes/theme-b2b/src/components.ts b/themes/theme-b2b/src/components.ts
deleted file mode 100644
index a529fb76e..000000000
--- a/themes/theme-b2b/src/components.ts
+++ /dev/null
@@ -1,619 +0,0 @@
-import { colors } from './colors';
-import { text } from './typography';
-
-/**
- * Select component
- */
-const selectButton = {
-  appearance: 'none',
-  position: 'relative',
-  display: 'inline-flex',
-  alignItems: 'center',
-  justifyContent: 'space-between',
-  overflow: 'hidden',
-  outline: 'none',
-  width: '100%',
-  lineHeight: 'large',
-  px: 'xsmall',
-  color: 'inherit',
-  bg: 'transparent',
-} as const;
-
-const selectOption = {
-  fontFamily: 'body',
-  fontSize: 'xsmall',
-  fontWeight: 'body',
-  lineHeight: 'large',
-  outline: 'none',
-  cursor: 'pointer',
-  color: 'text',
-  px: 'xsmall',
-  listStyle: 'none',
-} as const;
-
-const select = {
-  __default: {
-    fontFamily: 'body',
-    fontSize: 'xsmall',
-    fontWeight: 'body',
-    lineHeight: 'large',
-    color: 'text',
-  },
-  disabled: {
-    fontFamily: 'body',
-    fontSize: 'xsmall',
-    fontWeight: 'body',
-    lineHeight: 'large',
-    color: 'disabled',
-    cursor: 'not-allowed',
-  },
-  listbox: {
-    __default: {
-      bg: 'gray00',
-      border: '1px solid transparent',
-      borderLeftColor: 'gray40',
-      borderRightColor: 'gray40',
-      borderBottomColor: 'gray40',
-      borderBottomRightRadius: 'small',
-      borderBottomLeftRadius: 'small',
-    },
-    error: {
-      bg: 'gray00',
-      border: '1px solid transparent',
-      borderLeftColor: 'error',
-      borderRightColor: 'error',
-      borderBottomColor: 'error',
-      borderBottomLeftRadius: 'small',
-      borderBottomRightRadius: 'small',
-    },
-  },
-  section: {
-    fontFamily: 'body',
-    fontSize: 'xsmall',
-    fontWeight: 'body',
-    lineHeight: 'large',
-    px: 'xxsmall',
-    color: 'gray50',
-  },
-  option: {
-    __default: {
-      ...selectOption,
-      ':focus': {
-        bg: 'blue20',
-      },
-    },
-    selected: {
-      ...selectOption,
-      color: 'gray00',
-      bg: 'blue60',
-    },
-    disabled: {
-      ...selectOption,
-      cursor: 'not-allowed',
-      color: 'gray40',
-    },
-  },
-} as const;
-
-/**
- * Button component
- */
-const button = {
-  root: {
-    position: 'relative',
-    fontFamily: 'body',
-    fontSize: 'xsmall',
-    fontWeight: 'body',
-    border: 'none',
-    borderRadius: 'small',
-    display: 'inline-flex',
-  },
-  primary: {
-    color: 'background',
-    bg: 'primary',
-    ':hover': {
-      color: 'background',
-      bg: 'orange40',
-      cursor: 'pointer',
-    },
-    ':disabled': {
-      color: 'gray40',
-      bg: 'gray20',
-      cursor: 'not-allowed',
-    },
-  },
-  secondary: {
-    color: 'background',
-    bg: 'secondary',
-    ':hover': {
-      color: 'background',
-      bg: 'gray60',
-      cursor: 'pointer',
-    },
-    ':disabled': {
-      color: 'gray40',
-      bg: 'gray20',
-      cursor: 'not-allowed',
-    },
-  },
-  ghost: {
-    color: 'secondary',
-    border: '1px solid',
-    outlineColor: 'gray70',
-    ':hover': {
-      color: 'secondary',
-      bg: 'gray30',
-      cursor: 'pointer',
-    },
-    ':disabled': {
-      color: 'disabled',
-      bg: 'gray00',
-      outlineColor: 'disabled',
-      cursor: 'not-allowed',
-    },
-  },
-  text: {
-    color: 'secondary',
-    ':hover': {
-      color: 'secondary',
-      outlineColor: 'gray70',
-      bg: 'gray30',
-      cursor: 'pointer',
-    },
-    ':disabled': {
-      color: 'disabled',
-      bg: 'gray00',
-      cursor: 'not-allowed',
-      outline: 'none',
-    },
-  },
-  menu: {
-    color: 'secondary',
-    bg: 'background',
-    ':hover': {
-      color: 'background',
-      bg: 'gray60',
-      cursor: 'pointer',
-    },
-    ':disabled': {
-      color: 'gray40',
-      bg: 'gray20',
-      cursor: 'not-allowed',
-    },
-  },
-  select: {
-    ...selectButton,
-    border: '1px solid transparent',
-    borderColor: 'gray40',
-    ':hover': {
-      cursor: 'pointer',
-    },
-    ':focus': {
-      boxShadow: '0 0 0 2px ' + colors.blue60,
-      border: '1px solid transparent',
-    },
-    ':disabled': {
-      bg: 'gray20',
-      color: 'disabled',
-      cursor: 'not-allowed',
-    },
-    error: {
-      ...selectButton,
-      border: '1px solid',
-      borderRadius: 'small',
-      borderColor: 'error',
-    },
-    open: {
-      ...selectButton,
-      border: '1px solid transparent',
-      borderTopColor: 'gray40',
-      borderLeftColor: 'gray40',
-      borderRightColor: 'gray40',
-      borderTopRightRadius: 'small',
-      borderTopLeftRadius: 'small',
-    },
-    errorOpened: {
-      ...selectButton,
-      border: '1px solid transparent',
-      borderTopColor: 'error',
-      borderLeftColor: 'error',
-      borderRightColor: 'error',
-      borderTopRightRadius: 'small',
-      borderTopLeftRadius: 'small',
-    },
-  },
-} as const;
-
-/**
- * Slider component
- */
-const sliderThumb = {
-  WebkitAppearance: 'none',
-  boxSizing: 'border-box',
-  border: '4px solid',
-  borderColor: 'gray70',
-  width: 16,
-  height: 16,
-  bg: 'gray00',
-  borderRadius: 'large',
-  cursor: 'pointer',
-  marginTop: -4,
-} as const;
-
-const sliderTrack = {
-  WebkitAppearance: 'none',
-  width: '100%',
-  height: 8,
-  bg: 'gray30',
-  borderRadius: 'large',
-  border: 'none',
-} as const;
-
-const slider = {
-  __default: {
-    // styles need to be applied to range inputs in all browsers to override their basic appearance.
-    WebkitAppearance: 'none',
-    bg: 'transparent',
-    borderColor: 'transparent',
-    color: 'transparent',
-    ':focus': {
-      outline: 'none',
-    },
-    // chrome, safari, opera (theres actually no webkit option to style the progress bar like in firefox)
-    '&::-webkit-slider-thumb': {
-      ...sliderThumb,
-    },
-    '&:focus::-webkit-slider-thumb': {
-      ...sliderThumb,
-      border: '4px solid',
-      borderColor: 'primary',
-    },
-    '&:disabled::-webkit-slider-thumb': {
-      ...sliderThumb,
-      border: '4px solid',
-      borderColor: 'gray40',
-      bg: 'gray40',
-    },
-    '&::-webkit-slider-runnable-track': {
-      ...sliderTrack,
-    },
-    '&:focus::-webkit-slider-runnable-track': {
-      ...sliderTrack,
-      backgroundColor: 'primary',
-    },
-    '&:disabled::-webkit-slider-runnable-track': {
-      ...sliderTrack,
-      bg: 'gray40',
-    },
-    '&::-webkit-progress-value': {
-      bg: 'green60',
-    },
-    // firefox
-    '&::-moz-range-thumb': {
-      ...sliderThumb,
-    },
-    '&:focus::-moz-range-thumb': {
-      ...sliderThumb,
-      border: '4px solid',
-      borderColor: 'primary',
-    },
-    '&:disabled::-moz-range-thumb': {
-      ...sliderThumb,
-      border: '4px solid',
-      borderColor: 'gray40',
-      bg: 'gray40',
-    },
-    '&::-moz-range-track': {
-      ...sliderTrack,
-    },
-    '&::-moz-range-progress': {
-      ...sliderTrack,
-      bg: 'gray70',
-    },
-    '&:focus::-moz-range-progress': {
-      ...sliderTrack,
-      backgroundColor: 'primary',
-    },
-    '&:disabled::-moz-range-progress': {
-      ...sliderTrack,
-      bg: 'gray40',
-    },
-  },
-} as const;
-
-export const components = {
-  badge: {
-    __default: {
-      display: 'inline-flex',
-      alignItems: 'center',
-      fontFamily: 'body',
-      fontSize: 'xsmall',
-      fontWeight: 'body',
-      borderRadius: 'large',
-      border: '2px solid transparent',
-      whiteSpace: 'nowrap',
-      py: 4,
-      px: 12,
-      mx: 8,
-    },
-  },
-  button: {
-    __default: {
-      p: 0,
-      border: 'none',
-    },
-    large: {
-      lineHeight: 'xxlarge',
-      paddingX: 'xlarge',
-    },
-    small: {
-      lineHeight: 'large',
-      paddingX: 'medium',
-    },
-    primary: {
-      ...button.root,
-      ...button.primary,
-    },
-    secondary: {
-      ...button.root,
-      ...button.secondary,
-    },
-    ghost: {
-      ...button.root,
-      ...button.ghost,
-    },
-    text: {
-      ...button.root,
-      ...button.text,
-    },
-    menu: {
-      ...button.root,
-      ...button.menu,
-    },
-    select: {
-      ...button.root,
-      ...button.select,
-    },
-  },
-  card: {
-    __default: {
-      maxWidth: 500,
-      bg: 'gray00',
-      p: 'small',
-      boxShadow: '0px 4px 4px rgba(165, 165, 165, 0.25)',
-      borderRadius: 'large',
-    },
-  },
-  checkbox: {
-    __default: {
-      fill: 'gray00',
-      stroke: 'gray40',
-    },
-    ':checked': {
-      fill: 'primary',
-      stroke: 'orange80',
-    },
-    ':disabled': {
-      fill: 'gray30',
-    },
-    ':error': {
-      stroke: 'error',
-    },
-  },
-  dialog: {
-    __default: {
-      bg: 'background',
-      minWidth: 510,
-      minHeight: 240,
-    },
-    backdrop: {
-      bg: '#00000080',
-    },
-  },
-  divider: {
-    __default: {
-      my: 'xsmall',
-    },
-    bold: {
-      my: 'xsmall',
-      height: '2px',
-    },
-    section: {
-      my: 'xxsmall',
-      bg: 'gray50',
-    },
-  },
-  helpText: {
-    error: {
-      ...text.root,
-      fontSize: 'xxsmall',
-      color: 'error',
-    },
-    description: {
-      ...text.root,
-      fontSize: 'xxsmall',
-      color: 'text',
-    },
-  },
-  image: {
-    fullWidth: {
-      maxWidth: '100%',
-      height: 'auto',
-    },
-  },
-  input: {
-    __default: {
-      color: 'text',
-      border: 'none',
-      borderRadius: 'small',
-      boxShadow: '0 0 0 1px ' + colors.gray40,
-      outline: 'none',
-      padding: '0 8px',
-      lineHeight: 'large',
-      ':focus': {
-        boxShadow: '0 0 0 2px ' + colors.blue60,
-      },
-      ':disabled': {
-        bg: 'gray20',
-        color: 'gray40',
-        cursor: 'not-allowed',
-      },
-    },
-    error: {
-      color: 'text',
-      border: 'none',
-      borderRadius: 'small',
-      boxShadow: '0 0 0 1px ' + colors.red60,
-      outline: 'none',
-      padding: '0 8px',
-      lineHeight: 'large',
-    },
-  },
-  label: {
-    above: {
-      fontSize: 'xxsmall',
-      color: 'text',
-    },
-    inline: {
-      fontSize: 'xsmall',
-      display: 'inline-flex',
-      alignItems: 'center',
-    },
-    section: {
-      fontSize: 'small',
-      lineHeight: 'large',
-      color: 'gray50',
-    },
-  },
-  link: {
-    __default: {
-      color: 'blue60',
-      ':hover': {
-        textDecoration: 'none',
-      },
-    },
-    menuItemLink: {
-      color: 'text',
-      textDecoration: 'none',
-    },
-  },
-  menuItem: {
-    default: {
-      display: 'block',
-      fontFamily: 'body',
-      fontSize: 'xsmall',
-      fontWeight: 'body',
-      padding: 'xsmall',
-      bg: 'background',
-      color: 'text',
-      ':hover': {
-        bg: 'gray30',
-        cursor: 'pointer',
-      },
-    },
-  },
-  message: {
-    warning: {
-      borderStyle: 'solid',
-      borderColor: 'warning',
-      borderWidth: '2px 2px 2px 16px',
-      pt: 'xsmall',
-      pb: 'small',
-      px: 'small',
-      color: 'warning',
-    },
-    error: {
-      borderStyle: 'solid',
-      borderColor: 'error',
-      borderWidth: '2px 2px 2px 16px',
-      pt: 'xsmall',
-      pb: 'small',
-      px: 'small',
-      color: 'error',
-    },
-    info: {
-      borderStyle: 'solid',
-      borderColor: 'info',
-      borderWidth: '2px 2px 2px 16px',
-      pt: 'xsmall',
-      pb: 'small',
-      px: 'small',
-      color: 'info',
-    },
-    title: {
-      mb: 'xsmall',
-    },
-  },
-  radio: {
-    __default: {
-      fill: 'gray00',
-      stroke: 'gray40',
-    },
-    ':disabled': {
-      fill: 'gray30',
-    },
-    ':error': {
-      stroke: 'error',
-    },
-    ':checked': {
-      fill: 'primary',
-      stroke: 'orange80',
-    },
-  },
-  select,
-  slider,
-  switch: {
-    __default: {
-      fill: 'gray20',
-      stroke: 'gray40',
-    },
-    ':checked': {
-      fill: 'primary',
-      stroke: 'orange80',
-    },
-    ':disabled': {
-      fill: 'gray30',
-      stroke: 'gray40',
-    },
-  },
-  textArea: {
-    __default: {
-      fontFamily: 'body',
-      lineHeight: 'medium',
-      py: 'xxsmall',
-      px: 'xsmall',
-      color: 'text',
-      border: 'none',
-      borderRadius: 'small',
-      outline: '1px solid',
-      outlineColor: 'disabled',
-      ':focus': {
-        outline: '2px solid',
-        outlineColor: 'blue60',
-      },
-      ':disabled': {
-        cursor: 'not-allowed',
-        color: 'disabled',
-        bg: 'gray20',
-      },
-    },
-  },
-  tooltip: {
-    __default: {
-      p: 'xsmall',
-      color: 'gray90',
-      fontSize: 14,
-      border: '1px solid',
-      borderColor: 'blue70',
-      borderRadius: 'large',
-      bg: 'blue10',
-    },
-  },
-  validation: {
-    error: {
-      ...text.root,
-      fontSize: 'xxsmall',
-      color: 'error',
-    },
-  },
-} as const;
diff --git a/themes/theme-b2b/src/components/Badge.style.ts b/themes/theme-b2b/src/components/Badge.style.ts
new file mode 100644
index 000000000..0b4efb625
--- /dev/null
+++ b/themes/theme-b2b/src/components/Badge.style.ts
@@ -0,0 +1,21 @@
+import type { Theme } from '@marigold/components';
+
+export const Badge: Theme['components']['Badge'] = {
+  base: {
+    display: 'inline-flex',
+    alignItems: 'center',
+    fontSize: 'xxsmall',
+    borderRadius: 'large',
+    border: '2px solid transparent',
+    whiteSpace: 'nowrap',
+    py: 4,
+    px: 12,
+  },
+  variant: {
+    info: {
+      color: 'blue10',
+      borderColor: 'blue80',
+      bg: 'blue70',
+    },
+  },
+};
diff --git a/themes/theme-b2b/src/components/index.ts b/themes/theme-b2b/src/components/index.ts
new file mode 100644
index 000000000..bd03d3b47
--- /dev/null
+++ b/themes/theme-b2b/src/components/index.ts
@@ -0,0 +1,1 @@
+export * from './Badge.style';
\ No newline at end of file
diff --git a/themes/theme-b2b/src/index.ts b/themes/theme-b2b/src/index.ts
index 168629c06..903989bf6 100644
--- a/themes/theme-b2b/src/index.ts
+++ b/themes/theme-b2b/src/index.ts
@@ -1,10 +1,13 @@
-import { type Theme } from '@marigold/components';
-import { colors } from './colors';
-import { components } from './components';
-import { typography } from './typography';
+import { type Theme } from "@marigold/components";
+import { colors } from "./colors";
+import { component } from "./component";
+import { typography } from "./typography";
+
+// NEW STYLES!
+import * as components from "./components";
 
 export const webFontUrl = [
-  'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
+  "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap",
 ] as const;
 
 const theme: Theme = {
@@ -46,7 +49,7 @@ const theme: Theme = {
     body: {
       margin: 0,
       padding: 0,
-      fontFamily: 'body',
+      fontFamily: "body",
     },
   },
   radii: {
@@ -56,7 +59,10 @@ const theme: Theme = {
     large: 8,
   },
   ...typography,
-  ...components,
+  ...component,
+
+  // NEW STYLES!!!
+  components,
 } as const;
 
 export default theme;
diff --git a/themes/theme-core/src/components.ts b/themes/theme-core/src/component.ts
similarity index 97%
rename from themes/theme-core/src/components.ts
rename to themes/theme-core/src/component.ts
index fdd8009de..f6299d6f2 100644
--- a/themes/theme-core/src/components.ts
+++ b/themes/theme-core/src/component.ts
@@ -327,21 +327,7 @@ const slider = {
   },
 } as const;
 
-export const components = {
-  badge: {
-    __default: {
-      display: 'inline-flex',
-      alignItems: 'center',
-      fontFamily: 'body',
-      fontSize: 'xsmall',
-      fontWeight: 'body',
-      borderRadius: 'large',
-      border: '2px solid transparent',
-      whiteSpace: 'nowrap',
-      padding: '0.25rem 0.75rem',
-      mx: '0.5rem',
-    },
-  },
+export const component = {
   button: {
     __default: {
       p: 0,
diff --git a/themes/theme-core/src/components/Badge.style.ts b/themes/theme-core/src/components/Badge.style.ts
new file mode 100644
index 000000000..0cbb59bb1
--- /dev/null
+++ b/themes/theme-core/src/components/Badge.style.ts
@@ -0,0 +1,16 @@
+import type { Theme } from '@marigold/components';
+
+export const Badge: Theme['components']['Badge'] = {
+  base: {
+    display: 'inline-flex',
+    alignItems: 'center',
+    fontFamily: 'body',
+    fontSize: 'xsmall',
+    fontWeight: 'body',
+    borderRadius: 'large',
+    border: '2px solid transparent',
+    whiteSpace: 'nowrap',
+    padding: '0.25rem 0.75rem',
+    mx: '0.5rem',
+  },
+};
diff --git a/themes/theme-core/src/components/index.ts b/themes/theme-core/src/components/index.ts
new file mode 100644
index 000000000..bd03d3b47
--- /dev/null
+++ b/themes/theme-core/src/components/index.ts
@@ -0,0 +1,1 @@
+export * from './Badge.style';
\ No newline at end of file
diff --git a/themes/theme-core/src/index.ts b/themes/theme-core/src/index.ts
index 72f6b6c09..72859bfbd 100644
--- a/themes/theme-core/src/index.ts
+++ b/themes/theme-core/src/index.ts
@@ -1,8 +1,11 @@
 import { type Theme } from '@marigold/components';
 import { colors } from './colors';
-import { components } from './components';
+import { component } from './component';
 import { typography } from './typography';
 
+// NEW STYLES!
+import * as components from './components';
+
 const theme: Theme = {
   space: {
     none: 0,
@@ -53,7 +56,9 @@ const theme: Theme = {
     xlarge: 10,
   },
   ...typography,
-  ...components,
+  ...component,
+  // NEW STYLES!!!
+  components,
 } as const;
 
 export default theme;
diff --git a/themes/theme-unicorn/src/colors.ts b/themes/theme-unicorn/src/colors.ts
index 493208459..82fd8f864 100644
--- a/themes/theme-unicorn/src/colors.ts
+++ b/themes/theme-unicorn/src/colors.ts
@@ -33,4 +33,10 @@ export const colors = {
   red60: '#f38a8a',
   red70: '#dd4142',
   red80: '#dd4142',
+  purple10: '#ede7f6',
+  purple20: '#d1c4e9',
+  purple40: '#9575cd',
+  purple60: '#673ab7',
+  purple70: '#512da8',
+  purple80: '#311b92',
 } as const;
diff --git a/themes/theme-unicorn/src/components.ts b/themes/theme-unicorn/src/component.ts
similarity index 96%
rename from themes/theme-unicorn/src/components.ts
rename to themes/theme-unicorn/src/component.ts
index a042168f1..91dca5b85 100644
--- a/themes/theme-unicorn/src/components.ts
+++ b/themes/theme-unicorn/src/component.ts
@@ -317,21 +317,7 @@ const slider = {
   },
 } as const;
 
-export const components = {
-  badge: {
-    __default: {
-      display: 'inline-flex',
-      alignItems: 'center',
-      fontFamily: 'body',
-      fontSize: 'xsmall',
-      fontWeight: 'body',
-      borderRadius: '10px',
-      border: '2px solid transparent',
-      whiteSpace: 'nowrap',
-      padding: '0.25rem 0.75rem',
-      mx: '0.5rem',
-    },
-  },
+export const component = {
   button: {
     __default: {
       p: 0,
diff --git a/themes/theme-unicorn/src/components/Badge.style.ts b/themes/theme-unicorn/src/components/Badge.style.ts
new file mode 100644
index 000000000..372891ef7
--- /dev/null
+++ b/themes/theme-unicorn/src/components/Badge.style.ts
@@ -0,0 +1,21 @@
+import type { Theme } from '@marigold/components';
+
+export const Badge: Theme['components']['Badge'] = {
+  base: {
+    display: 'inline-flex',
+    alignItems: 'center',
+    fontSize: 'xxsmall',
+    borderRadius: 'large',
+    border: '2px solid transparent',
+    whiteSpace: 'nowrap',
+    py: 4,
+    px: 12,
+  },
+  variant: {
+    info: {
+      color: 'purple10',
+      borderColor: 'purple80',
+      bg: 'purple70',
+    },
+  },
+};
diff --git a/themes/theme-unicorn/src/components/index.ts b/themes/theme-unicorn/src/components/index.ts
new file mode 100644
index 000000000..bd03d3b47
--- /dev/null
+++ b/themes/theme-unicorn/src/components/index.ts
@@ -0,0 +1,1 @@
+export * from './Badge.style';
\ No newline at end of file
diff --git a/themes/theme-unicorn/src/index.ts b/themes/theme-unicorn/src/index.ts
index 0ab175110..ab79482ea 100644
--- a/themes/theme-unicorn/src/index.ts
+++ b/themes/theme-unicorn/src/index.ts
@@ -1,8 +1,11 @@
 import { type Theme } from '@marigold/components';
 import { colors } from './colors';
-import { components } from './components';
+import { component } from './component';
 import { typography } from './typography';
 
+// NEW STYLES!
+import * as components from './components';
+
 export const webFontUrl = [
   'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
 ] as const;
@@ -56,7 +59,10 @@ const theme: Theme = {
     large: 8,
   },
   ...typography,
-  ...components,
+  ...component,
+
+  // NEW STYLES!!!
+  components,
 } as const;
 
 export default theme;


Test patch:
diff --git a/packages/components/src/Badge/Badge.test.tsx b/packages/components/src/Badge/Badge.test.tsx
index eb87f4d37..dbe003274 100644
--- a/packages/components/src/Badge/Badge.test.tsx
+++ b/packages/components/src/Badge/Badge.test.tsx
@@ -6,60 +6,67 @@ import { Badge } from './Badge';
 const theme = {
   radii: {
     none: 0,
-    larger: 8,
+    'small-1': 8,
+    'medium-1': 16,
   },
-  badge: {
-    __default: {
-      borderRadius: '8px',
-    },
-    fatBadge: {
-      borderRadius: '12px',
+  space: {
+    none: 0,
+    'small-1': 4,
+  },
+  components: {
+    Badge: {
+      base: {
+        padding: 'small-1',
+      },
+      variant: {
+        one: {
+          borderRadius: 'small-1',
+        },
+        two: {
+          borderRadius: 'medium-1',
+        },
+      },
     },
   },
 };
 
-test('supports default variant and themeSection', () => {
+test('renders as a "div" element', () => {
   render(
     <ThemeProvider theme={theme}>
-      <Badge title="badge" />
+      <Badge data-testid="badge" />
     </ThemeProvider>
   );
-  const badge = screen.getByTitle(/badge/);
 
-  expect(badge).toHaveStyle(`border-radius: 8px;`);
+  const badge = screen.getByTestId('badge');
+  expect(badge instanceof HTMLDivElement).toBeTruthy();
 });
 
-test('renders correct HTML element', () => {
+test('uses base styling form "Badge" in theme', () => {
   render(
     <ThemeProvider theme={theme}>
-      <Badge title="badge" />
+      <Badge data-testid="badge" />
     </ThemeProvider>
   );
-  const badge = screen.getByTitle(/badge/);
-
-  expect(badge instanceof HTMLDivElement).toBeTruthy();
+  const badge = screen.getByTestId('badge');
+  expect(badge).toHaveStyle(`padding: ${theme.space['small-1']}px`);
 });
 
-test('supports other variant than default', () => {
-  render(
+test('supports "Badge" variants from theme', () => {
+  const { rerender } = render(
     <ThemeProvider theme={theme}>
-      <Badge variant="fatBadge" title="badge" />
+      <Badge variant="one" data-testid="badge" />
     </ThemeProvider>
   );
-  const badge = screen.getByTitle(/badge/);
 
-  expect(badge).toHaveStyle(`border-radius: 12px;`);
-});
+  let badge = screen.getByTestId('badge');
+  expect(badge).toHaveStyle(`border-radius: ${theme.radii['small-1']}px`);
 
-test('accepts custom styles prop className', () => {
-  render(
+  rerender(
     <ThemeProvider theme={theme}>
-      <Badge className="custom-class-name" title="badge">
-        badge
-      </Badge>
+      <Badge variant="two" data-testid="badge" />
     </ThemeProvider>
   );
-  const badge = screen.getByTitle(/badge/);
 
-  expect(badge.className).toMatch('custom-class-name');
+  badge = screen.getByTestId('badge');
+  expect(badge).toHaveStyle(`border-radius: ${theme.radii['medium-1']}px`);
 });
diff --git a/packages/create-theme/src/create-theme.test.ts b/packages/create-theme/src/create-theme.test.ts
index 8a1a004a6..9b7bdbaa9 100644
--- a/packages/create-theme/src/create-theme.test.ts
+++ b/packages/create-theme/src/create-theme.test.ts
@@ -238,47 +238,24 @@ test('allows to use fluid dimensions', () => {
 });
 
 test('allows to add component styles', () => {
-  const { link, text, button } = createTheme({
+  const { components } = createTheme({
     colors: ['brand', 'red'],
     components: {
-      link: {
-        color: 'red.80',
-      },
-
-      text: {
-        body: {
-          fontFamily: 'sans',
-        },
-      },
-
-      button: {
-        __default: {
+      Badge: {
+        base: {
           color: 'brand.primary',
         },
-
-        primary: {
-          color: 'red.00',
-        },
       },
     },
   });
 
-  expect({ link, text, button }).toMatchInlineSnapshot(`
+  expect({ components }).toMatchInlineSnapshot(`
     {
-      "button": {
-        "__default": {
-          "color": "brand.primary",
-        },
-        "primary": {
-          "color": "red.00",
-        },
-      },
-      "link": {
-        "color": "red.80",
-      },
-      "text": {
-        "body": {
-          "fontFamily": "sans",
+      "components": {
+        "Badge": {
+          "base": {
+            "color": "brand.primary",
+          },
         },
       },
     }
diff --git a/packages/system/src/components/Box/Box.test.tsx b/packages/system/src/components/Box/Box.test.tsx
index 94790b9b1..adf0aa2f0 100644
--- a/packages/system/src/components/Box/Box.test.tsx
+++ b/packages/system/src/components/Box/Box.test.tsx
@@ -1,9 +1,11 @@
 import React from 'react';
 import { render, screen } from '@testing-library/react';
+
 import { ThemeProvider } from '../../hooks';
-import { Box, StyleProps } from './Box';
 import { normalize } from '../../normalize';
 
+import { Box, StyleProps } from './Box';
+
 const theme = {
   colors: {
     primary: 'black',
@@ -11,6 +13,7 @@ const theme = {
     black: '#000',
     white: '#fff',
     blue: 'cornflowerblue',
+    red: '#c92a2a',
   },
   fontSizes: {
     body: 16,
@@ -306,3 +309,28 @@ test('custom styling overrides normalization, defaults, variants and style props
 
   expect(element).not.toHaveStyle(`color: ${theme.colors.primary}px`); // variant part that is not overriden
 });
+
+test('transforms states selectors', async () => {
+  render(
+    <ThemeProvider theme={theme}>
+      <Box
+        data-hover
+        data-invalid
+        __baseCSS={{ p: 'small', '&:hover': { p: 'medium' } }}
+        css={{
+          fontSize: 'large',
+          m: 'small',
+          bg: 'blue',
+          '&:invalid': {
+            bg: 'red',
+          },
+        }}
+      >
+        Test
+      </Box>
+    </ThemeProvider>
+  );
+  const element = screen.getByText('Test');
+  expect(element).toHaveStyle(`padding: ${theme.space.medium}px`);
+  expect(element).toHaveStyle(`background: ${theme.colors.red}`);
+});
diff --git a/packages/system/src/components/Box/utils.test.ts b/packages/system/src/components/Box/utils.test.ts
new file mode 100644
index 000000000..1588dd13a
--- /dev/null
+++ b/packages/system/src/components/Box/utils.test.ts
@@ -0,0 +1,119 @@
+import { transformPseudos } from './utils';
+
+describe('transform states', () => {
+  test('returns a css object', () => {
+    expect(transformPseudos({})).toEqual({});
+    expect(
+      transformPseudos({
+        color: '#fa5252',
+        padding: 10,
+      })
+    ).toMatchInlineSnapshot(`
+      {
+        "color": "#fa5252",
+        "padding": 10,
+      }
+    `);
+  });
+
+  test.each([
+    {
+      name: ':hover',
+      input: {
+        '&:hover': {},
+      },
+      expected: {
+        '&:hover, &[data-hover]': {},
+      },
+    },
+    {
+      name: ':focus',
+      input: {
+        '&:focus': {},
+      },
+      expected: {
+        '&:focus, &[data-focus]': {},
+      },
+    },
+    {
+      name: ':active',
+      input: {
+        '&:active': {},
+      },
+      expected: {
+        '&:active, &[data-active]': {},
+      },
+    },
+    {
+      name: ':disabled',
+      input: {
+        '&:disabled': {},
+      },
+      expected: {
+        '&[disabled], &[aria-disabled=true], &[data-disabled]': {},
+      },
+    },
+    {
+      name: ':read-only',
+      input: {
+        '&:read-only': {},
+      },
+      expected: {
+        '&[readonly], &[aria-readonly=true], &[data-readonly]': {},
+      },
+    },
+    {
+      name: ':checked',
+      input: {
+        '&:checked': {},
+      },
+      expected: {
+        '&[aria-checked=true], &[data-checked]': {},
+      },
+    },
+    {
+      name: ':indeterminate',
+      input: {
+        '&:indeterminate': {},
+      },
+      expected: {
+        '&:indeterminate, &[aria-checked=mixed], &[data-indeterminate]': {},
+      },
+    },
+    {
+      name: ':invalid',
+      input: {
+        '&:invalid': {},
+      },
+      expected: {
+        '&:invalid, &[aria-invalid=true], &[data-invalid]': {},
+      },
+    },
+  ])('tramsform "$name" to selector', ({ input, expected }) => {
+    expect(transformPseudos(input)).toEqual(expected);
+  });
+
+  test('transform nested pseudos', () => {
+    expect(
+      transformPseudos({
+        padding: 8,
+        '&:hover': {
+          color: '#1864ab',
+          '&:focus': {
+            outline: '1px solid #228be6',
+          },
+        },
+      })
+    ).toMatchInlineSnapshot(`
+      {
+        "&:hover, &[data-hover]": {
+          "&:focus, &[data-focus]": {
+            "outline": "1px solid #228be6",
+          },
+          "color": "#1864ab",
+        },
+        "padding": 8,
+      }
+    `);
+  });
+});
diff --git a/packages/system/src/hooks/useComponentStyles.test.tsx b/packages/system/src/hooks/useComponentStyles.test.tsx
index a7ea6f367..9d1085695 100644
--- a/packages/system/src/hooks/useComponentStyles.test.tsx
+++ b/packages/system/src/hooks/useComponentStyles.test.tsx
@@ -67,22 +67,6 @@ const theme = {
           color: 'secondary',
         },
       },
-      state: {
-        hover: {
-          bg: 'blue',
-        },
-        error: {
-          bg: 'red',
-        },
-        focus: {
-          outline: '2px solid',
-          outlineColor: 'blue',
-        },
-        disabled: {
-          cursor: 'not-allowed',
-          bg: 'grey',
-        },
-      },
     },
     // Component with multiple parts
     Checkbox: {
@@ -101,31 +85,6 @@ const theme = {
           fontSize: 'small-2',
         },
       },
-      state: {
-        checked: {
-          icon: {
-            opacity: 1,
-            bg: 'blue',
-          },
-        },
-        unchecked: {
-          icon: {
-            opacity: 0,
-          },
-        },
-        error: {
-          container: {
-            border: '1px solid',
-            borderColor: 'red',
-          },
-          icon: {
-            fill: 'red',
-          },
-          label: {
-            color: 'red',
-          },
-        },
-      },
       size: {
         small: {
           container: {
@@ -325,104 +284,6 @@ describe('useComponentStyles (simple)', () => {
       bg: 'white',
     });
   });
-
-  test('get state styles for a component', () => {
-    let view = renderHook(
-      () => useComponentStyles('Button', { states: { hover: true } }),
-      {
-        wrapper,
-      }
-    );
-    expect(view.result.current).toMatchInlineSnapshot(`
-      {
-        "appearance": "none",
-        "bg": "blue",
-      }
-    `);
-
-    view = renderHook(
-      () => useComponentStyles('Button', { states: { error: true } }),
-      {
-        wrapper,
-      }
-    );
-    expect(view.result.current).toMatchInlineSnapshot(`
-      {
-        "appearance": "none",
-        "bg": "red",
-      }
-    `);
-  });
-
-  test('get multiple states (disabled overrides other states)', () => {
-    let view = renderHook(
-      () =>
-        useComponentStyles('Button', {
-          states: { hover: true, focus: true },
-        }),
-      {
-        wrapper,
-      }
-    );
-    expect(view.result.current).toMatchInlineSnapshot(`
-      {
-        "appearance": "none",
-        "bg": "blue",
-        "outline": "2px solid",
-        "outlineColor": "blue",
-      }
-    `);
-
-    view = renderHook(
-      () =>
-        useComponentStyles('Button', {
-          states: { hover: true, disabled: true },
-        }),
-      {
-        wrapper,
-      }
-    );
-    expect(view.result.current).toMatchInlineSnapshot(`
-      {
-        "appearance": "none",
-        "bg": "grey",
-        "cursor": "not-allowed",
-      }
-    `);
-  });
-
-  test('works if state does not exist', () => {
-    const { result } = renderHook(
-      () => useComponentStyles('Button', { states: { visited: true } }),
-      {
-        wrapper,
-      }
-    );
-    expect(result.current).toMatchInlineSnapshot(`
-      {
-        "appearance": "none",
-        "bg": "white",
-      }
-    `);
-  });
-
-  test('only applies set styles', () => {
-    const { result } = renderHook(
-      () =>
-        useComponentStyles('Button', {
-          states: { hover: true, disabled: false },
-        }),
-      {
-        wrapper,
-      }
-    );
-    expect(result.current).toMatchInlineSnapshot(`
-      {
-        "appearance": "none",
-        "bg": "blue",
-      }
-    `);
-  });
 });
 
 describe('useComponentStyles (complex)', () => {
@@ -520,31 +381,30 @@ describe('useComponentStyles (complex)', () => {
     `);
   });
 
-  test('get state styles for a component (with parts)', () => {
+  test('returns empty objects if part does not exist', () => {
     const { result } = renderHook(
       () =>
         useComponentStyles(
           'Checkbox',
-          { states: { checked: true } },
-          { parts: ['container', 'icon', 'label'] }
+          {},
+          { parts: ['container', 'non-existing-part'] }
         ),
       {
         wrapper,
       }
     );
+    expect(result.current['non-existing-part']).toMatchInlineSnapshot(`{}`);
+  });
+
+  test('returns only requested parts', () => {
+    const { result } = renderHook(
+      () => useComponentStyles('Checkbox', {}, { parts: ['label'] }),
+      {
+        wrapper,
+      }
+    );
     expect(result.current).toMatchInlineSnapshot(`
       {
-        "container": {
-          "alignItems": "center",
-          "display": "flex",
-          "gap": "small-1",
-        },
-        "icon": {
-          "bg": "blue",
-          "height": "small-1",
-          "opacity": 1,
-          "width": "small-1",
-        },
         "label": {
           "color": "black",
           "fontSize": "small-2",
@@ -552,31 +412,15 @@ describe('useComponentStyles (complex)', () => {
       }
     `);
   });
-
-  test('returns empty objects if part does not exist', () => {
-    const { result } = renderHook(
-      () =>
-        useComponentStyles(
-          'Checkbox',
-          {},
-          { parts: ['container', 'non-existing-part'] }
-        ),
-      {
-        wrapper,
-      }
-    );
-    expect(result.current['non-existing-part']).toMatchInlineSnapshot(`{}`);
-  });
 });
 
 describe('style superiority', () => {
-  test('override order: base < size < state < variant', () => {
+  test('override order: base < size < variant', () => {
     const { result } = renderHook(
       () =>
         useComponentStyles('Button', {
           size: 'small',
           variant: 'pink',
-          states: { disabled: true },
         }),
       {
         wrapper,
@@ -585,14 +429,13 @@ describe('style superiority', () => {
     expect(result.current).toMatchInlineSnapshot(`
       {
         "appearance": "none",
-        "bg": "grey",
-        "cursor": "not-allowed",
+        "bg": "white",
         "height": "small-1",
       }
     `);
   });
 
-  test('override order: base < size < state < variant (with parts)', () => {
+  test('override order: base < size < variant (with parts)', () => {
     const { result } = renderHook(
       () =>
         useComponentStyles(
@@ -600,7 +443,6 @@ describe('style superiority', () => {
           {
             size: 'small',
             variant: 'pink',
-            states: { error: true },
           },
           { parts: ['container', 'icon', 'label'] }
         ),
@@ -612,14 +454,11 @@ describe('style superiority', () => {
       {
         "container": {
           "alignItems": "center",
-          "border": "1px solid",
-          "borderColor": "red",
           "display": "flex",
           "gap": "small-1",
           "p": "small-1",
         },
         "icon": {
-          "fill": "red",
           "height": "small-1",
           "width": "small-1",
         },
@@ -639,20 +478,18 @@ describe('style usage', () => {
         useComponentStyles('Button', {
           size: 'small',
           variant: 'pink',
-          states: { disabled: true },
         }),
       {
         wrapper,
       }
     );
     expect(result.current).toMatchInlineSnapshot(`
-    {
-      "appearance": "none",
-      "bg": "grey",
-      "cursor": "not-allowed",
-      "height": "small-1",
-    }
-  `);
+      {
+        "appearance": "none",
+        "bg": "white",
+        "height": "small-1",
+      }
+    `);
   });
 
   test('usage with <Box>', () => {
diff --git a/packages/system/src/hooks/useStateProps.test.tsx b/packages/system/src/hooks/useStateProps.test.tsx
new file mode 100644
index 000000000..0ffa72578
--- /dev/null
+++ b/packages/system/src/hooks/useStateProps.test.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { renderHook } from '@testing-library/react-hooks';
+
+import { useStateProps } from './useStateProps';
+
+test('returns state attributes', () => {
+  const { result } = renderHook(() =>
+    useStateProps({
+      hover: true,
+      focus: true,
+      active: true,
+      visited: true,
+      disabled: true,
+      readOnly: true,
+      checked: true,
+      indeterminate: true,
+      invalid: true,
+    })
+  );
+
+  expect(result.current).toMatchInlineSnapshot(`
+    {
+      "data-active": "",
+      "data-checked": "",
+      "data-disabled": "",
+      "data-focus": "",
+      "data-hover": "",
+      "data-indeterminate": "",
+      "data-invalid": "",
+      "data-readonly": "",
+      "data-visited": "",
+    }
+  `);
+});
+
+test('return only set states', () => {
+  const { result } = renderHook(() =>
+    useStateProps({
+      hover: false,
+      focus: true,
+      active: false,
+      visited: true,
+      disabled: false,
+    })
+  );
+
+  expect(result.current).toMatchInlineSnapshot(`
+    {
+      "data-focus": "",
+      "data-visited": "",
+    }
+  `);
+});
+
+test('works with empty object', () => {
+  const { result } = renderHook(() => useStateProps({}));
+
+  expect(result.current).toMatchInlineSnapshot(`{}`);
+});
+
+test('works with components', () => {
+  const Component = ({ disabled }: { disabled?: boolean }) => {
+    const stateProps = useStateProps({ disabled });
+
+    return <div data-testid="component" {...stateProps} />;
+  };
+
+  const { rerender } = render(<Component />);
+
+  let element = screen.getByTestId('component');
+  expect(element).not.toHaveAttribute('data-disabled', '');
+
+  rerender(<Component disabled />);
+  expect(element).toHaveAttribute('data-disabled', '');
+});