diff --git a/src/static/icons/index.ts b/src/static/icons/index.ts
index 5f1ac341..65413845 100644
--- a/src/static/icons/index.ts
+++ b/src/static/icons/index.ts
@@ -11,3 +11,4 @@ export { default as IconPlus } from './plus.svg';
export { default as IconSearch } from './search.svg';
export { default as IconFilledUnchecked } from './check-filled-unchecked.svg';
export { default as IconSpinner } from './spinner.svg';
+export { default as IconProfile } from './profile.svg';
diff --git a/src/static/icons/profile.svg b/src/static/icons/profile.svg
new file mode 100644
index 00000000..484b6645
--- /dev/null
+++ b/src/static/icons/profile.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/woly/atoms/avatar/index.tsx b/src/woly/atoms/avatar/index.tsx
new file mode 100644
index 00000000..5735e3a8
--- /dev/null
+++ b/src/woly/atoms/avatar/index.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import styled, { StyledComponent } from 'styled-components';
+import { IconProfile } from 'static/icons';
+
+import { useImageLoad } from './use-image-load';
+
+interface AvatarProps {
+ alt?: string;
+ src?: string;
+ srcSet?: string;
+ children?: React.ReactNode;
+}
+
+const AvatarBase: React.FC = ({
+ alt,
+ children: childrenProp,
+ src,
+ srcSet,
+ ...props
+}) => {
+ const loadFailed = useImageLoad({ src, srcSet });
+ const hasImg = src || srcSet;
+ let children = null;
+
+ if (hasImg && !loadFailed) {
+ children =
;
+ } else if (childrenProp) {
+ children = childrenProp;
+ } else {
+ // render fallback if image loading failed or no src attributes / children provided
+ children = ;
+ }
+
+ return {children}
;
+};
+
+export const Avatar = styled(AvatarBase)`
+ --local-size: calc((var(--woly-component-level) + 2) * 2 * var(--woly-const-m));
+
+ width: var(--local-size);
+ height: var(--local-size);
+
+ & > * {
+ width: 100%;
+ height: 100%;
+
+ border-radius: 50%;
+ }
+` as StyledComponent<'div', Record, AvatarProps>;
diff --git a/src/woly/atoms/avatar/usage.mdx b/src/woly/atoms/avatar/usage.mdx
new file mode 100644
index 00000000..b5c08468
--- /dev/null
+++ b/src/woly/atoms/avatar/usage.mdx
@@ -0,0 +1,67 @@
+import {Avatar} from 'ui'
+import {Playground, block} from 'lib/playground'
+
+`Avatar` shows user avatar
+
+### Example
+
+
+
+
+
+### Sizes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Fallback
+
+
+
+
+
+### Custom child component
+
+
+
+
+ RC
+
+
+
+
+### Props
+
+| Name | Type | Default | Description |
+| ---------- | ----------------- | ----------- | ---------------------------------------- |
+| `alt` | `string` | `''` | text description of the image |
+| `children` | `React.ReactNode` | `undefined` | use if no src attributes provided |
+| `src` | `string` | `''` | avatar src |
+| `srcSet` | `string` | `''` | avatar src set for multiple screen sizes |
diff --git a/src/woly/atoms/avatar/use-image-load.ts b/src/woly/atoms/avatar/use-image-load.ts
new file mode 100644
index 00000000..fefbfe56
--- /dev/null
+++ b/src/woly/atoms/avatar/use-image-load.ts
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react';
+
+export function useImageLoad({ src, srcSet }: { src?: string; srcSet?: string }) {
+ const [failed, setFailed] = useState(false);
+
+ useEffect(() => {
+ if (!src && !srcSet) {
+ return;
+ }
+
+ setFailed(false);
+
+ const image = new Image();
+ image.src = src ?? '';
+ image.srcset = srcSet ?? '';
+
+ const onLoad = () => {
+ setFailed(false);
+ };
+
+ const onError = () => {
+ setFailed(true);
+ };
+
+ image.addEventListener('load', onLoad);
+ image.addEventListener('error', onError);
+
+ return () => {
+ image.removeEventListener('load', onLoad);
+ image.removeEventListener('error', onError);
+ };
+ }, [src, srcSet]);
+
+ return failed;
+}
diff --git a/src/woly/atoms/index.ts b/src/woly/atoms/index.ts
index 7b484e4f..9f61c84a 100644
--- a/src/woly/atoms/index.ts
+++ b/src/woly/atoms/index.ts
@@ -16,3 +16,4 @@ export { Text } from './text';
export { TextArea } from './text-area';
export { Tooltip } from './tooltip';
export { UploadArea } from './upload-area';
+export { Avatar } from './avatar';