diff --git a/.changeset/slimy-bikes-wash.md b/.changeset/slimy-bikes-wash.md
new file mode 100644
index 0000000000..94d88263bc
--- /dev/null
+++ b/.changeset/slimy-bikes-wash.md
@@ -0,0 +1,5 @@
+---
+"@marigold/system": patch
+---
+
+create Element component + normalize file
diff --git a/packages/system/src/Element.test.tsx b/packages/system/src/Element.test.tsx
new file mode 100644
index 0000000000..618e7a668c
--- /dev/null
+++ b/packages/system/src/Element.test.tsx
@@ -0,0 +1,203 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { useStyles } from './useStyles';
+import { ThemeProvider } from './useTheme';
+import { Element } from './Element';
+
+const theme = {
+ text: {
+ body: {
+ fontSize: 1,
+ color: 'black',
+ marginTop: '2px',
+ },
+ heading: {
+ fontSize: 3,
+ color: 'primary',
+ },
+ padding: {
+ paddingTop: '2px',
+ },
+ },
+};
+
+test('renders a
by default', () => {
+ render(
Text);
+ const testelem = screen.getByText('Text');
+
+ expect(testelem instanceof HTMLDivElement).toBeTruthy();
+});
+
+test('supports as prop', () => {
+ render(
Text);
+ const testelem = screen.getByText('Text');
+
+ expect(testelem instanceof HTMLParagraphElement).toBeTruthy();
+});
+
+test('supports HTML className attribute', () => {
+ render(
Text);
+ const element = screen.getByText('Text');
+
+ expect(element.getAttribute('class')).toMatch('my-custom-class');
+});
+
+test('passes down HTML attributes', () => {
+ render(
+
+ Text
+
+ );
+ const element = screen.getByText('Text');
+
+ expect(element.getAttribute('id')).toEqual('element-id');
+ expect(element.getAttribute('disabled')).toMatch('');
+ expect(element.getAttribute('class')).toMatch('my-custom-class');
+});
+
+test('forwards ref', () => {
+ const ref = React.createRef
();
+ render(
+
+ button
+
+ );
+
+ expect(ref.current instanceof HTMLButtonElement).toBeTruthy();
+});
+
+test('base styles first', () => {
+ const { getByText } = render(Text);
+ const testelem = getByText('Text');
+ const style = getComputedStyle(testelem);
+
+ expect(style.marginTop).toEqual('0px'); // 0px come from base
+});
+
+test('variant styles second', () => {
+ const TestComponent: React.FC<{ variant?: 'body' }> = ({
+ variant = 'body',
+ children,
+ ...props
+ }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { getByText } = render(
+
+ Text
+
+ );
+ const testelem = getByText('Text');
+ const style = getComputedStyle(testelem);
+
+ expect(style.marginTop).not.toEqual('0px'); // 0px come from base
+ expect(style.marginBottom).toEqual('0px'); // 0px still come from base
+ expect(style.marginTop).toEqual('2px'); // 2px come from variant
+});
+
+test('array of variant styles', () => {
+ const TestComponent: React.FC<{ variant?: 'body' }> = ({
+ variant = 'body',
+ children,
+ ...props
+ }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { getByText } = render(
+
+ Text
+
+ );
+ const testelem = getByText('Text');
+ const style = getComputedStyle(testelem);
+
+ expect(style.marginTop).not.toEqual('0px'); // 0px come from base
+ expect(style.marginBottom).toEqual('0px'); // 0px still come from base
+ expect(style.marginTop).toEqual('2px'); // 2px marginTop come from variant
+ expect(style.paddingTop).toEqual('2px'); // 2px paddingTop come from variant
+});
+
+test('custom styles with css prop third', () => {
+ const TestComponent: React.FC<{ variant?: 'body' }> = ({
+ variant = 'body',
+ children,
+ ...props
+ }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { getByText } = render(
+
+ Text
+
+ );
+ const testelem = getByText('Text');
+ const style = getComputedStyle(testelem);
+
+ expect(style.marginTop).not.toEqual('0px'); // do not apply 0px from base
+ expect(style.marginTop).not.toEqual('2px'); // do not apply 2px from variant
+ expect(style.marginTop).toEqual('4px'); // apply 4px from custom styles
+});
+
+test("don't apply the same reset multiple times", () => {
+ const Button = ({ className }: { className?: string }) => {
+ const classNames = useStyles({ element: 'button', className });
+ return (
+
+ Click me!
+
+ );
+ };
+ const Wrapper = () => ;
+
+ render();
+ const button = screen.getByTitle('button');
+ const classNames = button.className.split(' ').filter(i => i.length);
+
+ // Test if applied classnames are unique
+ expect(classNames.length).toEqual([...new Set(classNames)].length);
+});
+
+test('normalize tag name ', () => {
+ const TestComponent: React.FC<{ variant?: 'body' }> = ({
+ variant = 'body',
+ children,
+ ...props
+ }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { getByText } = render(
+
+ Link
+
+ );
+ const testelem = getByText('Link');
+ const style = getComputedStyle(testelem);
+
+ expect(style.boxSizing).toEqual('border-box'); // from base
+ expect(style.textDecoration).toEqual('none'); // from a
+});
diff --git a/packages/system/src/Element.tsx b/packages/system/src/Element.tsx
new file mode 100644
index 0000000000..90027fb5e3
--- /dev/null
+++ b/packages/system/src/Element.tsx
@@ -0,0 +1,59 @@
+import { jsx } from '@emotion/react';
+import { forwardRef } from 'react';
+import {
+ PolymorphicPropsWithRef,
+ PolymorphicComponentWithRef,
+} from '@marigold/types';
+
+import { getNormalizedStyles } from './normalize';
+import { CSSObject } from './types';
+import { useTheme } from './useTheme';
+
+export type ElementOwnProps = {
+ css?: CSSObject;
+ variant?: string | string[];
+};
+
+export type ElementProps = PolymorphicPropsWithRef;
+
+/**
+ * Function expression to check if there is any falsy value or empty object
+ */
+const isNotEmpty = (val: any) =>
+ !(val && Object.keys(val).length === 0 && val.constructor === Object);
+
+/**
+ * Get the normalized base styles
+ */
+const baseStyles = getNormalizedStyles('base');
+
+export const Element: PolymorphicComponentWithRef =
+ forwardRef(
+ ({ as = 'div', css: styles = {}, variant, children, ...props }, ref) => {
+ const { css } = useTheme();
+
+ /**
+ * Transform variant input for `@theme-ui/css`
+ */
+ const variants = Array.isArray(variant)
+ ? variant.map(v => ({ variant: v }))
+ : [{ variant }];
+
+ return jsx(
+ as,
+ {
+ ...props,
+ ...{
+ css: [
+ baseStyles,
+ getNormalizedStyles(as),
+ ...variants.map(v => css(v)),
+ css(styles),
+ ].filter(isNotEmpty),
+ },
+ ref,
+ },
+ children
+ );
+ }
+ );
diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts
index 3236bf37ae..e040f2deb0 100644
--- a/packages/system/src/index.ts
+++ b/packages/system/src/index.ts
@@ -1,5 +1,6 @@
+export * from './Element';
export * from './cache';
export * from './types';
export * from './useClassname';
export * from './useStyles';
-export * from './useTheme';
\ No newline at end of file
+export * from './useTheme';
diff --git a/packages/system/src/normalize.test.tsx b/packages/system/src/normalize.test.tsx
new file mode 100644
index 0000000000..d75055fffb
--- /dev/null
+++ b/packages/system/src/normalize.test.tsx
@@ -0,0 +1,42 @@
+import { getNormalizedStyles } from './normalize';
+
+test('get base styles', () => {
+ const baseStyles = getNormalizedStyles('base');
+ expect(baseStyles).toEqual({
+ boxSizing: 'border-box',
+ margin: 0,
+ padding: 0,
+ minWidth: 0,
+ fontSize: '100%',
+ fontFamily: 'inherit',
+ verticalAlign: 'baseline',
+ WebkitTapHighlightColor: 'transparent',
+ });
+});
+
+test('get reset style by element', () => {
+ const baseStyles = getNormalizedStyles('a');
+ expect(baseStyles).toEqual({
+ textDecoration: 'none',
+ touchAction: 'manipulation',
+ });
+});
+
+test('getNormalizedStyles returns base if input is not a string', () => {
+ const baseStyles = getNormalizedStyles(undefined);
+ expect(baseStyles).toEqual({
+ boxSizing: 'border-box',
+ margin: 0,
+ padding: 0,
+ minWidth: 0,
+ fontSize: '100%',
+ fontFamily: 'inherit',
+ verticalAlign: 'baseline',
+ WebkitTapHighlightColor: 'transparent',
+ });
+});
+
+test('getNormalizedStyles returns empty object if input is unknown', () => {
+ const baseStyles = getNormalizedStyles('p');
+ expect(baseStyles).toEqual({});
+});
diff --git a/packages/system/src/normalize.ts b/packages/system/src/normalize.ts
new file mode 100644
index 0000000000..8f48e65bcc
--- /dev/null
+++ b/packages/system/src/normalize.ts
@@ -0,0 +1,131 @@
+import { ElementType } from 'react';
+
+const base = {
+ boxSizing: 'border-box',
+ margin: 0,
+ padding: 0,
+ minWidth: 0,
+ fontSize: '100%',
+ fontFamily: 'inherit',
+ verticalAlign: 'baseline',
+ WebkitTapHighlightColor: 'transparent',
+} as const;
+
+// Content
+// ---------------
+const block = {
+ display: 'block',
+} as const;
+
+const list = {
+ // empty
+} as const;
+
+const table = {
+ borderCollapse: 'collapse',
+ borderSpacing: 0,
+} as const;
+
+// Typography
+// ---------------
+const a = {
+ textDecoration: 'none',
+ touchAction: 'manipulation',
+} as const;
+
+const quote = {
+ quotes: 'none',
+ selectors: {
+ '&:before, &:after': {
+ content: "''",
+ },
+ },
+} as const;
+
+// Form Elements
+// ---------------
+const button = {
+ display: 'block',
+ appearance: 'none',
+ background: 'transparent',
+ textAlign: 'center',
+ touchAction: 'manipulation',
+} as const;
+
+const input = {
+ display: 'block',
+ appearance: 'none',
+ selectors: {
+ '&::-ms-clear': {
+ display: 'none',
+ },
+ '&::-webkit-search-cancel-button': {
+ WebkitAppearance: 'none',
+ },
+ },
+} as const;
+
+const select = {
+ display: 'block',
+ appearance: 'none',
+ selectors: {
+ '&::-ms-expand': {
+ display: 'none',
+ },
+ },
+} as const;
+
+const textarea = {
+ display: 'block',
+ appearance: 'none',
+} as const;
+
+// Reset
+// ---------------
+const reset = {
+ article: block,
+ aside: block,
+ details: block,
+ figcaption: block,
+ figure: block,
+ footer: block,
+ header: block,
+ hgroup: block,
+ menu: block,
+ nav: block,
+ section: block,
+ ul: list,
+ ol: list,
+ blockquote: quote,
+ q: quote,
+ a,
+ base,
+ table,
+ select,
+ button,
+ textarea,
+ input,
+} as const;
+
+export type NormalizedElement = keyof typeof reset;
+const isKnownElement = (input: string): input is NormalizedElement =>
+ input in reset;
+
+/**
+ * Helper to conveniently get reset styles.
+ */
+export const getNormalizedStyles = (input?: ElementType): object => {
+ /**
+ * If a React component is given, we don't apply any reset styles
+ * and return the base reset.
+ */
+ if (typeof input !== 'string') {
+ return reset.base;
+ }
+
+ /**
+ * Try to find the reset style for a HTML element. If the element
+ * is not included return empty styles.
+ */
+ return isKnownElement(input) ? reset[input] : {};
+};