diff --git a/.changeset/metal-days-remain.md b/.changeset/metal-days-remain.md
new file mode 100644
index 00000000000..90a1129ef0e
--- /dev/null
+++ b/.changeset/metal-days-remain.md
@@ -0,0 +1,5 @@
+---
+"@primer/components": minor
+---
+
+New `Spinner` Component
diff --git a/docs/content/Spinner.md b/docs/content/Spinner.md
new file mode 100644
index 00000000000..8c8a48d7120
--- /dev/null
+++ b/docs/content/Spinner.md
@@ -0,0 +1,38 @@
+---
+title: Spinner
+status: alpha
+---
+
+Use Spinner to let users know that content is being loaded.
+
+## Examples
+
+### Default (Medium)
+
+```jsx live
+
+```
+
+### Small
+
+```jsx live
+
+```
+
+### Large
+
+```jsx live
+
+```
+
+## System props
+
+Spinner components get `COMMON` system props. Read our [System Props](/system-props) doc page for a full list of available props.
+
+## Component props
+
+StyledOcticon passes all of its props except the common system props down to the [Octicon component](https://github.com/primer/octicons/tree/master/lib/octicons_react#usage), including:
+
+| Name | Type | Default | Description |
+| :--- | :------------------------------------- | :------: | :------------------------------------------------------- |
+| size | 'small' | 'medium' | 'large' | 'medium' | Sets the uniform `width` and `height` of the SVG element |
diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
index 9eacf0a11f9..1f3bad0514e 100644
--- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml
+++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml
@@ -103,6 +103,8 @@
url: /SelectMenu
- title: SideNav
url: /SideNav
+ - title: Spinner
+ url: /Spinner
- title: StateLabel
url: /StateLabel
- title: StyledOcticon
diff --git a/src/Spinner.tsx b/src/Spinner.tsx
new file mode 100644
index 00000000000..13ecb3a59c1
--- /dev/null
+++ b/src/Spinner.tsx
@@ -0,0 +1,58 @@
+import React from 'react'
+import styled from 'styled-components'
+import {COMMON, SystemCommonProps} from './constants'
+import sx, {SxProp} from './sx'
+import {ComponentProps} from './utils/types'
+
+const sizeMap = {
+ small: '16px',
+ medium: '32px',
+ large: '64px'
+}
+
+export interface SpinnerInternalProps {
+ size?: keyof typeof sizeMap
+}
+
+function Spinner({size: sizeKey, ...props}: SpinnerInternalProps) {
+ const size = (sizeKey && sizeMap[sizeKey]) ?? sizeMap.medium
+
+ return (
+
+ )
+}
+
+const StyledSpinner = styled(Spinner)`
+ @keyframes rotate-keyframes {
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
+ animation: rotate-keyframes 1s linear infinite;
+
+ ${COMMON}
+ ${sx}
+`
+
+StyledSpinner.displayName = 'Spinner'
+
+export type SpinnerProps = ComponentProps
+export default StyledSpinner
diff --git a/src/__tests__/Spinner.tsx b/src/__tests__/Spinner.tsx
new file mode 100644
index 00000000000..37074665915
--- /dev/null
+++ b/src/__tests__/Spinner.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import {Spinner, SpinnerProps} from '..'
+import {behavesAsComponent, checkExports} from '../utils/testing'
+import {COMMON} from '../constants'
+import {render as HTMLRender, cleanup} from '@testing-library/react'
+import {axe, toHaveNoViolations} from 'jest-axe'
+import 'babel-polyfill'
+expect.extend(toHaveNoViolations)
+
+describe('Spinner', () => {
+ afterEach(() => {
+ cleanup()
+ })
+
+ behavesAsComponent({
+ Component: Spinner,
+ systemPropArray: [COMMON]
+ })
+
+ checkExports('Spinner', {
+ default: Spinner
+ })
+
+ it('should have no axe violations', async () => {
+ const {container} = HTMLRender()
+ const results = await axe(container)
+ expect(results).toHaveNoViolations()
+ })
+
+ it('should respect size arguments', () => {
+ const expectSize = (input: SpinnerProps['size'] | undefined, expectedSize: string) => {
+ const {container} = HTMLRender()
+ const svg = container.querySelector('svg')!
+ expect(svg.getAttribute('height')).toEqual(expectedSize)
+ expect(svg.getAttribute('width')).toEqual(expectedSize)
+ }
+
+ // default: medium
+ expectSize(undefined, '32px')
+ expectSize('small', '16px')
+ expectSize('medium', '32px')
+ expectSize('large', '64px')
+ })
+})
diff --git a/src/__tests__/__snapshots__/Spinner.tsx.snap b/src/__tests__/__snapshots__/Spinner.tsx.snap
new file mode 100644
index 00000000000..5c7f00dce0c
--- /dev/null
+++ b/src/__tests__/__snapshots__/Spinner.tsx.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Spinner renders consistently 1`] = `
+.c0 {
+ -webkit-animation: rotate-keyframes 1s linear infinite;
+ animation: rotate-keyframes 1s linear infinite;
+}
+
+
+`;
diff --git a/src/index.ts b/src/index.ts
index 8cf553218e6..a631920c65e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -133,6 +133,8 @@ export type {
} from './SelectMenu'
export {default as SideNav} from './SideNav'
export type {SideNavProps, SideNavLinkProps} from './SideNav'
+export {default as Spinner} from './Spinner'
+export type {SpinnerProps} from './Spinner'
export {default as StateLabel} from './StateLabel'
export type {StateLabelProps} from './StateLabel'
export {default as StyledOcticon} from './StyledOcticon'