Repository: marigold-ui/marigold
Pull request #1979 (2022-04-11 06:42:07)

feat: transform state to corresponding css selector and data-attr

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 71ac1ce0d07bbb3b37a6eee50718b4775a1f8653

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/marigold-ui/marigold/new/transform-pseudos?filename=.changeset/cyan-ligers-sin.md&value=---%0A%22%40marigold%2Fsystem%22%3A%20patch%0A---%0A%0Afeat%3A%20transform%20state%20to%20corresponding%20css%20selector%20and%20data-attr%0A)



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/action-group.mdx b/docs/content/components/action-group.mdx
deleted file mode 100644
index 8b5dff651..000000000
--- a/docs/content/components/action-group.mdx
+++ /dev/null
@@ -1,74 +0,0 @@
----
-title: ActionGroup
----
-
-import { FigmaLink } from '../../src/components/FigmaLink';
-
-# ActionGroup
-
-The ActionGroup component adds two or more components together to form a common group.
-
-<MarigoldTheme>
-  <ActionGroup space="xsmall">
-    <Button variant="text" size="small">
-      Cancel
-    </Button>
-    <Button variant="primary" size="small">
-      Confirm
-    </Button>
-  </ActionGroup>
-</MarigoldTheme>
-
-```tsx onlyCode
-import { ActionGroup } from '@marigold/components';
-```
-
-<FigmaLink
-  to={
-    'https://www.figma.com/file/DFKyTGHAoDxOsUBPszLLxP/%F0%9F%8F%B5%EF%B8%8FMarigold?node-id=864%3A1'
-  }
-/>
-
-## Props
-
-| Property                       | Type                           | Default |
-| :----------------------------- | :----------------------------- | :------ |
-| `space` (optional)             | `ResponsiveStyleValue<string>` | `none`  |
-| `verticalAlignment` (optional) | `boolean`                      | `false` |
-
-## Examples
-
-### Space between the elements
-
-```tsx
-<ActionGroup space="small">
-  <Button variant="ghost">Back</Button>
-  <Button variant="secondary">Next</Button>
-</ActionGroup>
-```
-
-### Vertical alignment
-
-```tsx
-<ActionGroup space="small" verticalAlignment>
-  <Button variant="ghost" size="small">
-    Sort <SortUp />
-  </Button>
-  <Button variant="secondary" size="small">
-    Sort <SortDown />
-  </Button>
-</ActionGroup>
-```
-
-### Group two Buttons in a form
-
-```tsx
-<ActionGroup space="xsmall">
-  <Button variant="text" size="small">
-    Cancel
-  </Button>
-  <Button variant="primary" size="small" space="xsmall">
-    Save Settings <Save />
-  </Button>
-</ActionGroup>
-```
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/button.mdx b/docs/content/components/button.mdx
index dd0c43885..d3107adfe 100644
--- a/docs/content/components/button.mdx
+++ b/docs/content/components/button.mdx
@@ -2,8 +2,8 @@
 title: Button
 ---
 
-import { DoAndDont } from '../../src/components/DoAndDont';
-import { FigmaLink } from '../../src/components/FigmaLink';
+import { DoAndDont } from "../../src/components/DoAndDont";
+import { FigmaLink } from "../../src/components/FigmaLink";
 
 # Button
 
@@ -13,21 +13,21 @@ They have multiple styles for various needs, and are ideal for calling attention
 The style variant and size of the button can be added with the variant prop and is composed like `variant.size`.
 
 <MarigoldTheme>
-  <ActionGroup space="small">
+  <Inline space="small">
     <Button>Primary</Button>
     <Button variant="secondary">Secondary</Button>
     <Button variant="ghost">Ghost</Button>
     <Button variant="text">Text Only</Button>
-  </ActionGroup>
+  </Inline>
 </MarigoldTheme>
 
 ```tsx onlyCode
-import { Button } from '@marigold/components';
+import { Button } from "@marigold/components";
 ```
 
 <FigmaLink
   to={
-    'https://www.figma.com/file/DFKyTGHAoDxOsUBPszLLxP/%F0%9F%8F%B5%EF%B8%8FMarigold?node-id=11%3A269'
+    "https://www.figma.com/file/DFKyTGHAoDxOsUBPszLLxP/%F0%9F%8F%B5%EF%B8%8FMarigold?node-id=11%3A269"
   }
 />
 
@@ -220,36 +220,36 @@ Icons can be added in Order to make clearer what a button does. Buttons should n
   <Box
     display="grid"
     css={{
-      gridGap: 'small',
-      gridTemplateColumns: 'repeat(auto-fit, minmax(min(500px, 100%), 1fr))',
+      gridGap: "small",
+      gridTemplateColumns: "repeat(auto-fit, minmax(min(500px, 100%), 1fr))",
     }}
   >
     <DoAndDont
       preview={
-        <ActionGroup space="small">
+        <Inline space="small">
           <Button variant="ghost">Ghost</Button>
           <Button>Primary</Button>
-        </ActionGroup>
+        </Inline>
       }
     >
       Use a ghost button next to a Primary button.
     </DoAndDont>
     <DoAndDont
       preview={
-        <ActionGroup space="small">
+        <Inline space="small">
           <Button variant="text">Text</Button>
           <Button>Primary</Button>
-        </ActionGroup>
+        </Inline>
       }
     >
       Use a Text button next to a Primary button.
     </DoAndDont>
     <DoAndDont
       preview={
-        <ActionGroup space="small">
+        <Inline space="small">
           <Button variant="text">Text</Button>
           <Button variant="text">Text</Button>
-        </ActionGroup>
+        </Inline>
       }
     >
       Use a Text button next to another text button.
@@ -257,10 +257,10 @@ Icons can be added in Order to make clearer what a button does. Buttons should n
     <DoAndDont
       dont
       preview={
-        <ActionGroup space="small">
+        <Inline space="small">
           <Button>Primary</Button>
           <Button>Primary</Button>
-        </ActionGroup>
+        </Inline>
       }
     >
       Use two Primary buttons on the same page, there should be only one
@@ -269,10 +269,10 @@ Icons can be added in Order to make clearer what a button does. Buttons should n
     <DoAndDont
       dont
       preview={
-        <ActionGroup space="small">
+        <Inline space="small">
           <Button variant="secondary">Secondary</Button>
           <Button variant="secondary">Secondary</Button>
-        </ActionGroup>
+        </Inline>
       }
     >
       Use two Secondary buttons next to each other, it loses hierarchie and is
@@ -281,12 +281,12 @@ Icons can be added in Order to make clearer what a button does. Buttons should n
     <DoAndDont
       dont
       preview={
-        <ActionGroup space="small">
+        <Inline space="small">
           <Button variant="secondary">Secondary</Button>
           <Button variant="secondary" size="small">
             Secondary
           </Button>
-        </ActionGroup>
+        </Inline>
       }
     >
       Mix normal sized buttons with small buttons inmediatly next or below each
diff --git a/docs/content/components/inline.mdx b/docs/content/components/inline.mdx
index 72bf67d1d..2485b97d0 100644
--- a/docs/content/components/inline.mdx
+++ b/docs/content/components/inline.mdx
@@ -14,7 +14,7 @@ Layout component to inline elements with space. This component uses the spaces f
 </MarigoldTheme>
 
 ```tsx onlyCode
-import { Inline } from '@marigold/components';
+import { Inline } from "@marigold/components";
 ```
 
 ## Props
@@ -106,6 +106,5 @@ import { Inline } from '@marigold/components';
 
 ## Use cases
 
-- [ActionGroup](../action-group)
 - [Badge](../badge)
 - [Button](../button)
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/ActionGroup/ActionGroup.stories.tsx b/packages/components/src/ActionGroup/ActionGroup.stories.tsx
deleted file mode 100644
index a253b6106..000000000
--- a/packages/components/src/ActionGroup/ActionGroup.stories.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import type { Meta, ComponentStory } from '@storybook/react';
-
-import { Button } from '../Button';
-import { ActionGroup } from './ActionGroup';
-
-export default {
-  component: ActionGroup,
-  title: 'Components/ActionGroup',
-  argTypes: {
-    space: {
-      control: {
-        type: 'select',
-      },
-      options: [
-        'none',
-        'xxsmall',
-        'xsmall',
-        'small',
-        'medium',
-        'large',
-        'xlarge',
-        'xxlarge',
-      ],
-      defaultValue: 'none',
-      description: 'spacing around the group',
-    },
-    verticalAlignment: {
-      control: {
-        type: 'boolean',
-      },
-      defaultValue: false,
-      description: 'switch alignment',
-    },
-  },
-} as Meta;
-
-export const Basic: ComponentStory<typeof ActionGroup> = args => (
-  <ActionGroup {...args}>
-    <Button variant="text" size="small">
-      Cancel
-    </Button>
-    <Button variant="primary" size="small">
-      Confirm
-    </Button>
-  </ActionGroup>
-);
diff --git a/packages/components/src/ActionGroup/ActionGroup.tsx b/packages/components/src/ActionGroup/ActionGroup.tsx
deleted file mode 100755
index 5552f095e..000000000
--- a/packages/components/src/ActionGroup/ActionGroup.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-
-import { ResponsiveStyleValue } from '@marigold/system';
-import { ComponentProps } from '@marigold/types';
-
-import { Inline } from '../Inline';
-import { Stack } from '../Stack';
-
-// Props
-// ---------------
-export interface ActionGroupProps extends ComponentProps<'div'> {
-  space?: ResponsiveStyleValue<string>;
-  verticalAlignment?: boolean;
-}
-
-// Component
-// ---------------
-export const ActionGroup: React.FC<ActionGroupProps> = ({
-  space = 'none',
-  verticalAlignment = false,
-  children,
-  ...props
-}) =>
-  verticalAlignment ? (
-    <Stack space={space} {...props}>
-      {children}
-    </Stack>
-  ) : (
-    <Inline space={space} {...props}>
-      {children}
-    </Inline>
-  );
diff --git a/packages/components/src/ActionGroup/index.ts b/packages/components/src/ActionGroup/index.ts
deleted file mode 100644
index 522c45686..000000000
--- a/packages/components/src/ActionGroup/index.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export * from './ActionGroup';
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/index.ts b/packages/components/src/index.ts
index a40e7ca6f..97d085cef 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -1,6 +1,5 @@
 export * from './theme';
 
-export * from './ActionGroup';
 export * from './Aside';
 export * from './Aspect';
 export * from './Badge';
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/ActionGroup/ActionGroup.test.tsx b/packages/components/src/ActionGroup/ActionGroup.test.tsx
deleted file mode 100644
index de8d8dc5c..000000000
--- a/packages/components/src/ActionGroup/ActionGroup.test.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { ThemeProvider } from '@marigold/system';
-import { ActionGroup } from './ActionGroup';
-import { Button } from '../Button';
-
-const theme = {
-  space: {
-    none: 0,
-    small: 2,
-  },
-};
-
-test('default space is "none"', () => {
-  render(
-    <ThemeProvider theme={theme}>
-      <ActionGroup title="actionGroup">
-        <Button>Button1</Button>
-        <Button>Button2</Button>
-      </ActionGroup>
-    </ThemeProvider>
-  );
-  const group = screen.getByTitle(/actionGroup/);
-  expect(group).toHaveStyle(`gap: 0`);
-});
-
-test('supports space prop', () => {
-  render(
-    <ThemeProvider theme={theme}>
-      <ActionGroup title="actionGroup" space="8px">
-        <Button>Button1</Button>
-        <Button>Button2</Button>
-      </ActionGroup>
-    </ThemeProvider>
-  );
-  const groupt = screen.getByTitle(/actionGroup/);
-  expect(groupt).toHaveStyle(`gap: 8px`);
-});
-
-test('accepts and uses spacing from theme', () => {
-  render(
-    <ThemeProvider theme={theme}>
-      <ActionGroup title="actionGroup" space="small">
-        <Button title="Button1">Button1</Button>
-        <Button title="Button2">Button2</Button>
-      </ActionGroup>
-    </ThemeProvider>
-  );
-  const group = screen.getByTitle(/actionGroup/);
-  expect(group).toHaveStyle(`gap: 2px`);
-});
-
-test('supports verticalAlignment prop', () => {
-  render(
-    <ThemeProvider theme={theme}>
-      <ActionGroup title="actionGroup" space="small" verticalAlignment>
-        <Button title="Button1">Button1</Button>
-        <Button title="Button2">Button2</Button>
-      </ActionGroup>
-    </ThemeProvider>
-  );
-  const group = screen.getByTitle(/actionGroup/);
-  expect(group).toHaveStyle(`gap: 2px`);
-});
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', '');
+});