diff --git a/docs/src/stories/explorations/Button.stories.jsx b/docs/src/stories/explorations/Button.stories.jsx
new file mode 100644
index 0000000000..87e92c429c
--- /dev/null
+++ b/docs/src/stories/explorations/Button.stories.jsx
@@ -0,0 +1,207 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Explorations/Button',
+ parameters: {
+ layout: 'padded'
+ },
+
+ excludeStories: ['ButtonTemplate'],
+ argTypes: {
+ variant: {
+ options: [0, 1, 2, 3], // iterator
+ mapping: ['Button--secondary', 'Button--primary', 'Button--invisible', 'Button--danger'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['secondary', 'primary', 'invisible', 'danger']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls button color',
+ defaultValue: 'Button--secondary'
+ },
+ size: {
+ options: [0, 1, 2], // iterator
+ mapping: [null, 'Button--small', 'Button--large'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['default [32px]', 'small [28px]', 'large [40px]']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls button height',
+ defaultValue: 0
+ },
+ alignContent: {
+ options: [0, 1], // iterator
+ mapping: [null, 'Button-content--alignStart'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['center [default]', 'start']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description:
+ 'Align button label + visuals to the center (default for CTA buttons) or start for select/dropdown button scenarios',
+ defaultValue: 0
+ },
+ label: {
+ defaultValue: 'Button',
+ type: 'string',
+ name: 'label',
+ description: 'Visible button label',
+ table: {
+ category: 'Slot'
+ }
+ },
+ ariaLabel: {
+ defaultValue: '',
+ type: 'string',
+ name: 'ariaLabel',
+ description: 'Hidden button label (in addition to visible label). Not required in all cases.',
+ table: {
+ category: 'Slot'
+ }
+ },
+ disabled: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ table: {
+ category: 'State'
+ }
+ },
+ fullWidth: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ description: 'Allow button to stretch and fill container',
+ table: {
+ category: 'CSS'
+ }
+ },
+ leadingVisual: {
+ name: 'leadingVisual',
+ control: {type: 'boolean'},
+ description: 'Slot for SVG icon or emoji (boolean only for testing purposes)',
+ defaultValue: false,
+ table: {
+ category: 'Slot'
+ }
+ },
+ trailingVisual: {
+ name: 'trailingVisual',
+ control: {type: 'boolean'},
+ description: 'Slot for SVG icon or emoji (boolean only for testing purposes)',
+ table: {
+ category: 'Slot'
+ },
+ defaultValue: false
+ },
+ trailingAction: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ description:
+ 'Slot for SVG icon that indicates an action. Primarily used by other Primer components, like a DropdownMenu or overlay trigger (boolean only for testing purposes)',
+ table: {
+ category: 'Slot'
+ }
+ },
+ pressed: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ table: {
+ category: 'State'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on one element',
+ table: {
+ category: 'State'
+ }
+ },
+ active: {
+ control: {type: 'boolean'},
+ description: 'set button to active state',
+ table: {
+ category: 'State'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var button = document.getElementsByTagName('button')[0]
+ // set focus on element
+ button.focus()
+}
+
+const star = (
+
+)
+
+const caret = (
+
+)
+
+export const ButtonTemplate = ({
+ label,
+ variant,
+ disabled,
+ size,
+ fullWidth,
+ leadingVisual,
+ trailingVisual,
+ trailingAction,
+ pressed,
+ focusElement,
+ active,
+ className,
+ ariaLabel,
+ alignContent
+}) => (
+ <>
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = ButtonTemplate.bind({})
+Playground.args = {
+ focusElement: false,
+ active: false,
+ variant: 'Button--secondary',
+ leadingVisual: false,
+ trailingAction: false,
+ trailingVisual: false
+}
diff --git a/docs/src/stories/explorations/ButtonGroup.stories.jsx b/docs/src/stories/explorations/ButtonGroup.stories.jsx
new file mode 100644
index 0000000000..b91ae511cd
--- /dev/null
+++ b/docs/src/stories/explorations/ButtonGroup.stories.jsx
@@ -0,0 +1,24 @@
+import React from 'react'
+import clsx from 'clsx'
+import {ButtonTemplate} from './Button.stories'
+import {IconButtonTemplate} from './IconButton.stories'
+
+export default {
+ title: 'Explorations/ButtonGroup',
+ excludeStories: ['ButtonGroupTemplate'],
+ layout: 'padded',
+ argTypes: {}
+}
+
+// build every component case here in the template (private api)
+export const ButtonGroupTemplate = ({}) => (
+
+
+
+
+
+
+)
+
+export const Playground = ButtonGroupTemplate.bind({})
+Playground.args = {}
diff --git a/docs/src/stories/explorations/ExampleSheet.stories.jsx b/docs/src/stories/explorations/ExampleSheet.stories.jsx
new file mode 100644
index 0000000000..2f539aa70f
--- /dev/null
+++ b/docs/src/stories/explorations/ExampleSheet.stories.jsx
@@ -0,0 +1,986 @@
+import React from 'react'
+import clsx from 'clsx'
+import {ButtonTemplate} from './Button.stories'
+import {IconButtonTemplate} from './IconButton.stories'
+import {LinkTemplate} from './Link.stories'
+import {LinkStyledAsButtonTemplate} from './LinkStyledAsButton.stories'
+import {ButtonGroupTemplate} from './ButtonGroup.stories'
+
+export default {
+ title: 'Explorations',
+ layout: 'padded',
+ argTypes: {}
+}
+
+const gridStyle = {
+ display: 'grid',
+ gridAutoFlow: 'column dense',
+ gap: '16px',
+ justifyItems: 'start'
+}
+
+const gridStyleStretch = {
+ display: 'grid',
+ gridAutoFlow: 'column',
+ gap: '16px',
+ justifyItems: 'start',
+ width: '100%'
+}
+
+export const ExampleSheet = ({}) => (
+
+
Standard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading visual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trailing visual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading + Trailing visual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trailing action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Fullwidth (all visual scenarios, one button size)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Fullwidth (all visual scenarios, one button size) visualPosition fixed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading visual + action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trailing visual + action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading + Trailing visual + action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
IconButton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
LinkStyledAsButton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading visual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trailing visual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading + Trailing visual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trailing action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading visual + action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Trailing visual + action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leading + Trailing visual + action
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Link
+
+
+
+
+
+
+
+
+
+)
+
+// export const Playground = ExampleSheetTemplate.bind({})
+// Playground.args = {}
diff --git a/docs/src/stories/explorations/IconButton.stories.jsx b/docs/src/stories/explorations/IconButton.stories.jsx
new file mode 100644
index 0000000000..83acd9c942
--- /dev/null
+++ b/docs/src/stories/explorations/IconButton.stories.jsx
@@ -0,0 +1,130 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Explorations/IconButton',
+ parameters: {
+ // design: {
+ // type: 'figma',
+ // url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=4371%3A7128'
+ // },
+ layout: 'padded'
+ },
+
+ excludeStories: ['IconButtonTemplate'],
+ argTypes: {
+ variant: {
+ options: [0, 1, 2, 3], // iterator
+ mapping: ['Button--secondary', 'Button--primary', 'Button--invisible', 'Button--danger'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['default', 'primary', 'outline', 'danger']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls button color',
+ defaultValue: 'Button--secondary'
+ },
+ size: {
+ options: [0, 1, 2], // iterator
+ mapping: [null, 'Button--small', 'Button--large'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['default [32px]', 'small [28px]', 'large [40px]']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls button height and width',
+ defaultValue: 0
+ },
+ ariaLabel: {
+ defaultValue: '',
+ type: 'string',
+ name: 'ariaLabel',
+ description: 'Hidden button label (required)',
+ table: {
+ category: 'Slot'
+ }
+ },
+ disabled: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ table: {
+ category: 'State'
+ }
+ },
+ visual: {
+ name: 'visual',
+ control: {type: 'boolean'},
+ description: 'Slot for SVG icon (boolean only for testing purposes)',
+ defaultValue: true,
+ table: {
+ category: 'Slot'
+ }
+ },
+ pressed: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ table: {
+ category: 'State'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on one element',
+ table: {
+ category: 'State'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var button = document.getElementsByTagName('button')[0]
+ // set focus on element
+ button.focus()
+}
+
+const star = (
+
+)
+
+const caret = (
+
+)
+
+export const IconButtonTemplate = ({variant, disabled, size, visual, pressed, focusElement, className, ariaLabel}) => (
+ <>
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = IconButtonTemplate.bind({})
+Playground.args = {
+ focusElement: false,
+ ariaLabel: 'Button description'
+}
diff --git a/docs/src/stories/explorations/Link.stories.jsx b/docs/src/stories/explorations/Link.stories.jsx
new file mode 100644
index 0000000000..7379937b1f
--- /dev/null
+++ b/docs/src/stories/explorations/Link.stories.jsx
@@ -0,0 +1,130 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Explorations/Link',
+ parameters: {
+ // design: {
+ // type: 'figma',
+ // url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=4371%3A7128'
+ // },
+ layout: 'padded'
+ },
+
+ excludeStories: ['LinkTemplate'],
+ argTypes: {
+ variant: {
+ options: [0, 1], // iterator
+ mapping: [null, 'Link--muted'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['default', 'muted']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls link color',
+ defaultValue: 0
+ },
+ label: {
+ defaultValue: 'Link',
+ type: 'string',
+ name: 'label',
+ description: 'Link label',
+ table: {
+ category: 'Slot'
+ }
+ },
+ target: {
+ defaultValue: '',
+ type: 'string',
+ name: 'target',
+ description: '_blank for new tab',
+ table: {
+ category: 'Slot'
+ }
+ },
+ href: {
+ defaultValue: '',
+ type: 'string',
+ name: 'href',
+ description: '',
+ table: {
+ category: 'Slot'
+ }
+ },
+ showTrailingAction: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ description: '',
+ table: {
+ category: 'CSS'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on one element',
+ table: {
+ category: 'State'
+ }
+ },
+ focusAllElements: {
+ control: {type: 'boolean'},
+ description: 'set focus on all elements for viewing in all themes',
+ table: {
+ category: 'State'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var button = document.getElementsByTagName('button')[0]
+ // set focus on element
+ button.focus()
+}
+
+const arrow = (
+
+)
+
+export const LinkTemplate = ({
+ label,
+ variant,
+ focusElement,
+ focusAllElements,
+ className,
+ href,
+ showTrailingAction,
+ target
+}) => (
+ <>
+
+ {showTrailingAction ? (
+
+ {label}
+ {arrow}
+
+ ) : (
+ label
+ )}
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = LinkTemplate.bind({})
+Playground.args = {
+ focusElement: false,
+ focusAllElements: false
+}
diff --git a/docs/src/stories/explorations/LinkStyledAsButton.stories.jsx b/docs/src/stories/explorations/LinkStyledAsButton.stories.jsx
new file mode 100644
index 0000000000..0efd391a4e
--- /dev/null
+++ b/docs/src/stories/explorations/LinkStyledAsButton.stories.jsx
@@ -0,0 +1,228 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Explorations/LinkStyledAsButton',
+ parameters: {
+ // design: {
+ // type: 'figma',
+ // url: 'https://www.figma.com/file/GCvY3Qv8czRgZgvl1dG6lp/Primer-Web?node-id=4371%3A7128'
+ // },
+ layout: 'padded'
+ },
+
+ excludeStories: ['LinkStyledAsButtonTemplate'],
+ argTypes: {
+ variant: {
+ options: [0, 1, 2, 3], // iterator
+ mapping: [
+ 'LinkStyledAsButton--secondary',
+ 'LinkStyledAsButton--primary',
+ 'LinkStyledAsButton--invisible',
+ 'LinkStyledAsButton--danger'
+ ], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['secondary', 'primary', 'invisible', 'danger']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls button color',
+ defaultValue: 'LinkStyledAsButton--secondary'
+ },
+ size: {
+ options: [0, 1, 2], // iterator
+ mapping: [null, 'LinkStyledAsButton--small', 'LinkStyledAsButton--large'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['default [32px]', 'small [28px]', 'large [40px]']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description: 'Controls button height',
+ defaultValue: 0
+ },
+ alignContent: {
+ options: [0, 1], // iterator
+ mapping: [null, 'Button-content--alignStart'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['center [default]', 'start']
+ },
+ table: {
+ category: 'CSS'
+ },
+ description:
+ 'Align button label + visuals to the center (default for CTA buttons) or start for select/dropdown button scenarios',
+ defaultValue: 0
+ },
+ label: {
+ defaultValue: 'Link',
+ type: 'string',
+ name: 'label',
+ description: 'Visible button label',
+ table: {
+ category: 'Slot'
+ }
+ },
+ target: {
+ defaultValue: '',
+ type: 'string',
+ name: 'target',
+ description: '_blank for new tab',
+ table: {
+ category: 'Slot'
+ }
+ },
+ href: {
+ defaultValue: '',
+ type: 'string',
+ name: 'href',
+ description: '',
+ table: {
+ category: 'Slot'
+ }
+ },
+ disabled: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ table: {
+ category: 'State'
+ }
+ },
+ fullWidth: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ description: 'Allow button to stretch and fill container',
+ table: {
+ category: 'CSS'
+ }
+ },
+ leadingVisual: {
+ name: 'leadingVisual',
+ control: {type: 'boolean'},
+ description: 'Slot for SVG icon or emoji (boolean only for testing purposes)',
+ defaultValue: false,
+ table: {
+ category: 'Slot'
+ }
+ },
+ trailingVisual: {
+ name: 'trailingVisual',
+ control: {type: 'boolean'},
+ description: 'Slot for SVG icon or emoji (boolean only for testing purposes)',
+ table: {
+ category: 'Slot'
+ },
+ defaultValue: false
+ },
+ showTrailingAction: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ description:
+ 'Slot for SVG icon that indicates an action. Primarily used by other Primer components, like a DropdownMenu or overlay trigger (boolean only for testing purposes)',
+ table: {
+ category: 'Slot'
+ }
+ },
+ pressed: {
+ defaultValue: false,
+ control: {type: 'boolean'},
+ table: {
+ category: 'State'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on one element',
+ table: {
+ category: 'State'
+ }
+ },
+ active: {
+ control: {type: 'boolean'},
+ description: 'set button to active state',
+ table: {
+ category: 'State'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var button = document.getElementsByTagName('button')[0]
+ // set focus on element
+ button.focus()
+}
+
+const star = (
+
+)
+
+const arrow = (
+
+)
+
+export const LinkStyledAsButtonTemplate = ({
+ label,
+ variant,
+ size,
+ fullWidth,
+ leadingVisual,
+ trailingVisual,
+ showTrailingAction,
+ focusElement,
+ active,
+ alignContent,
+ className,
+ href,
+ target
+}) => (
+ <>
+
+
+ {leadingVisual && {star}}
+ {label}
+ {trailingVisual && {star}}
+
+ {showTrailingAction && (
+ {arrow}
+ )}
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = LinkStyledAsButtonTemplate.bind({})
+Playground.args = {
+ focusElement: false,
+ active: false,
+ variant: 'LinkStyledAsButton--secondary',
+ leadingVisual: false,
+ showTrailingAction: false,
+ trailingVisual: false
+}
diff --git a/src/button-refactor/ButtonsLinksAPI.md b/src/button-refactor/ButtonsLinksAPI.md
new file mode 100644
index 0000000000..ac24cf474d
--- /dev/null
+++ b/src/button-refactor/ButtonsLinksAPI.md
@@ -0,0 +1,116 @@
+# Button and Link component APIs
+Button and Link has been discussed in a number of issues and discussions over past few years. I went through the list below, took note of their purpose and started compiling a new API spec. This doc is is meant to be collaborative and accompany Storybook docs while we work through a final API recommendation. Once we have a solid plan, this can move into an issue and be delegated across frameworks.
+
+Existing issues
+- https://github.com/github/primer/issues/141
+ -- Accessory button component, should reconcile with reaction button (sister component to Button)
+- https://github.com/github/primer/issues/220
+ -- Tracking issue, mentions Button in the context of API consistency
+- https://github.com/github/primer/issues/253
+ -- Button audit, anatomy spec, design considerations, some button link discussion, icon buttons (this issue is very informative!)
+- https://github.com/github/primer/issues/263
+ -- More design discussion, particularly focused on outline button
+- https://github.com/github/primer/issues/272
+ -- Make icon only buttons square
+- https://github.com/github/primer/issues/321
+ -- Super specific button use case?
+- https://github.com/github/primer/issues/350
+ -- a11y audit with task list for PVC
+- https://github.com/github/primer/issues/382
+ -- React button refactor
+- https://github.com/github/primer/issues/468
+ -- Visual bugs (colors, state)
+- https://github.com/github/primer/discussions/87
+ -- Discussion about accessory buttons
+- https://github.com/github/primer/discussions/211
+ -- Open ended discussion about primary button as dropdown
+- https://github.com/github/primer/discussions/459
+ -- Allie's thoughts on limiting icon only buttons and working towards encouraging visual labels
+- https://github.com/github/primer/discussions/477
+ -- Buttons styled as links, links styled as buttons
+- https://github.com/github/primer/issues/65
+ -- Segmented control, pressed state discussion
+ https://github.com/primer/design/pull/193
+
+## Specific notes from previous bugs to keep in mind
+- [ ] Check all button variants have shadow present
+- [ ] Ensure icon colors are consistent in hover states
+- [ ] Ensure icon colors are consistent with variants in all states
+- [ ] Ensure disabled colors are consistent across frameworks
+
+## Component list
+A few discussions were about naming and prop drilling (source here). We found that for Button to handle all of its use cases, we would need a large number of props– some conditional and dependent on one another. When that happens, it's typically a sign that the logic should be separated into another component.
+
+| Component | Description |
+| -- | -- |
+| Button | standard `button` with variants, size, visual slots |
+| IconButton | `button` with icon only (square) and required `aria-label` |
+| ReactionButton | `button` snowflake with specific styles/interaction design (round) potentially a variant |
+| ButtonGroup | wrapper to handle grouping buttons |
+| Link | `a` with variants, optional trailing visuals |
+| LinkStyledAsButton | `a` with button variants, required trailing visual |
+
+## Component API breakdown
+
+### Button
+
+| prop | type | options | default | notes |
+| -- | -- | -- | -- | -- |
+| `variant` | one-of string | `primary` `secondary` `danger` `invisible` | `secondary` | |
+| `size` | one-of string | `small` `default` `large` | `default` | |
+| `label` | string | button description | null | |
+| `aria-label` | string | button description for screen readers | null | |
+| `aria-pressed` | boolean | `true/false` | `false` | |
+| `leadingVisual` | children (slot) | octicon | null | |
+| `trailingVisual` | children (slot) | octicon | null | |
+| `trailingAction` | children (slot) | octicon | null | slot for caret to maintain leading/trailing visuals if they exist |
+| `fullWidth` | boolean | `true/false` | `false` | |
+| `visualPosition` | one-of string | `fixed` `some-other-word` | `fixed` | lock icon to the text label or to the button container |
+
+
+### LinkStyledAsButton
+
+| prop | type | options | default | notes |
+| -- | -- | -- | -- | -- |
+| `variant` | one-of string | `primary` `secondary` `danger` `invisible` | `secondary` | |
+| `size` | one-of string | `small` `default` `large` | `default` | |
+| `label` | string | button description | null | |
+| `leadingVisual` | children (slot) | octicon | null | |
+| `trailingVisual` | children (slot) | octicon | null | |
+| `showTrailingAction` | arrow octicon | `true/false` | true? | show trailingVisual icon to indicate linkyness |
+| `fullWidth` | boolean | `true/false` | `false` | |
+| `visualPosition` | one-of string | `fixed` `some-other-word` | `fixed` | lock icon to the text label or to the button container |
+
+### IconButton
+
+| prop | type | options | default | notes |
+| -- | -- | -- | -- | -- |
+| `variant` | one-of string | `primary` `secondary` `danger` `outline`? | `secondary` | |
+| `size` | one-of string | `small` `default` `large` | `default | |
+| `aria-label` | string | button description for screen readers (required) | null | |
+| `visual` | children (slot) | octicon | null | |
+| `aria-pressed` | boolean | `true/false` | `false` | |
+
+### Link
+
+| prop | type | options | default | notes |
+| -- | -- | -- | -- | -- |
+| `variant` | one-of string | ?? we need to work on this (subtle and or muted) | |
+| `label` | string | button description | null | |
+| `showTrailingAction` | boolean | specific octicon for new tab or new page (or anchor?) | null | |
+
+### ButtonGroup
+
+| prop | type | options | default | notes |
+| -- | -- | -- | -- | -- |
+| `children` | child slot | | | |
+
+## Next review session
+- [ ] Help refine transition animations + tokenize
+- [ ] Icon colors- same as button text, or specific?
+- [ ] for `LinkStyledAsButton` should `trailingAction` slot be reserved for the > chevron indicating this is a link and not a button? Or is it more of a `trailingVisual`?
+- [ ] What color should `invisible` button variant be? Blue or grey?
+- [ ] No aria-label for `LinkStyledAsButton`
+- [ ] Do we like `fullWidth` or `block` as a prop for width behavior?
+- [ ] Should `ButtonGroup` offer an option with gaps (more of a layout tool)
+- [ ] Should `Button` proper handle a `pressed` (formerly selected) state, or should we create a new component `SegmentedControl` that uses `Button` and provides specific design (color, shadows)?
diff --git a/src/button-refactor/button-group.scss b/src/button-refactor/button-group.scss
new file mode 100644
index 0000000000..bc27caeae8
--- /dev/null
+++ b/src/button-refactor/button-group.scss
@@ -0,0 +1,27 @@
+.ButtonGroup {
+ display: flex;
+ flex-direction: row;
+}
+
+// reset border-radius
+.ButtonGroup-item {
+ border-right-width: 0;
+ border-radius: 0;
+
+ &:first-of-type {
+ border-top-left-radius: $border-radius;
+ border-bottom-left-radius: $border-radius;
+ }
+
+ &:last-of-type {
+ border-right-width: $border-width;
+ border-top-right-radius: $border-radius;
+ border-bottom-right-radius: $border-radius;
+ }
+
+ // ensure that the focus ring sits above the adjacent buttons
+ &:focus,
+ &:active {
+ z-index: 1;
+ }
+}
diff --git a/src/button-refactor/button.scss b/src/button-refactor/button.scss
new file mode 100644
index 0000000000..8baf610001
--- /dev/null
+++ b/src/button-refactor/button.scss
@@ -0,0 +1,305 @@
+// temporary, pre primitives release
+:root {
+ --primer-duration-fast: 80ms;
+ --primer-easing-easeInOut: cubic-bezier(0.65, 0, 0.35, 1);
+}
+
+// base button
+.Button {
+ position: relative;
+ font-size: var(--primer-text-body-size-medium);
+ font-weight: var(--base-text-weight-medium);
+ cursor: pointer;
+ user-select: none;
+ background-color: transparent;
+ border: var(--primer-borderWidth-thin) solid;
+ border-color: transparent;
+ border-radius: var(--primer-borderRadius-medium);
+ color: var(--color-btn-text);
+ transition: var(--primer-duration-fast) var(--primer-easing-easeInOut);
+ transition-property: color, fill, background-color, border-color;
+ text-align: center;
+ height: var(--primer-control-medium-size);
+ padding: 0 var(--primer-control-medium-paddingInline-normal);
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--primer-control-medium-gap);
+
+ // mobile friendly sizing
+ @media (pointer: course) {
+ &::before {
+ @include minTouchTarget(48px, 48px);
+ }
+ }
+
+ // base states
+
+ &:hover,
+ [open] > & {
+ transition-duration: var(--primer-duration-fast);
+ }
+
+ &:active,
+ &.Button--active {
+ transition: none;
+ }
+
+ &:disabled,
+ &.Button--disabled,
+ &[aria-disabled='true'] {
+ cursor: not-allowed;
+ box-shadow: none;
+ }
+
+ &:focus {
+ @include focusOutline;
+ }
+}
+
+.Button-withTooltip {
+ position: relative;
+ display: inline-block;
+}
+
+a.Button,
+summary.Button {
+ display: inline-flex;
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+// wrap grid content to allow trailingAction to lock-right
+.Button-content {
+ flex: 1 0 auto;
+ display: grid;
+ grid-template-areas: 'leadingVisual text trailingVisual';
+ grid-template-columns: min-content minmax(0, auto) min-content;
+ align-items: center;
+ place-content: center;
+ // padding-bottom: 1px; // optical alignment for firefox
+
+ > :not(:last-child) {
+ margin-right: var(--primer-control-medium-gap);
+ }
+}
+
+// center child elements for fullWidth
+.Button-content--alignStart {
+ justify-content: start;
+}
+
+// button child elements
+
+// align svg
+.Button-visual {
+ display: flex;
+ pointer-events: none; // allow click handler to work, avoiding visuals
+}
+
+.Button-label {
+ grid-area: text;
+ white-space: nowrap;
+ line-height: var(--primer-text-body-lineHeight-medium);
+}
+
+.Button-leadingVisual {
+ grid-area: leadingVisual;
+}
+
+.Button-trailingVisual {
+ grid-area: trailingVisual;
+}
+
+.Button-trailingAction {
+ margin-right: calc(var(--base-size-4) * -1);
+}
+
+// sizes
+
+.Button--small {
+ font-size: var(--primer-text-body-size-small);
+ height: var(--primer-control-small-size);
+ padding: 0 var(--primer-control-small-paddingInline-normal);
+ gap: var(--primer-control-small-gap);
+
+ .Button-label {
+ line-height: var(--primer-text-body-lineHeight-small);
+ }
+
+ .Button-content {
+ > :not(:last-child) {
+ margin-right: var(--primer-control-small-gap);
+ }
+ }
+}
+
+.Button--large {
+ height: var(--primer-control-large-size);
+ padding: 0 var(--primer-control-large-paddingInline-normal);
+ gap: var(--primer-control-large-gap);
+
+ .Button-label {
+ line-height: var(--primer-text-body-lineHeight-large);
+ }
+
+ .Button-content {
+ > :not(:last-child) {
+ margin-right: var(--primer-control-large-gap);
+ }
+ }
+}
+
+.Button--fullWidth {
+ width: 100%;
+}
+
+// variants
+
+// primary
+.Button--primary {
+ color: var(--color-btn-primary-text);
+ fill: var(--color-btn-primary-icon);
+ background-color: var(--color-btn-primary-bg);
+ border-color: var(--color-btn-primary-border);
+ box-shadow: var(--color-btn-primary-shadow), var(--color-btn-primary-inset-shadow);
+
+ &:hover,
+ [open] > & {
+ background-color: var(--color-btn-primary-hover-bg);
+ border-color: var(--color-btn-primary-hover-border);
+ }
+
+ &:active,
+ &[aria-pressed='true'],
+ &.Button--pressed {
+ background-color: var(--color-btn-primary-selected-bg);
+ box-shadow: var(--color-btn-primary-selected-shadow);
+ }
+
+ &:disabled,
+ &.Button--disabled,
+ &[aria-disabled='true'] {
+ color: var(--color-btn-primary-disabled-text);
+ background-color: var(--color-btn-primary-disabled-bg);
+ border-color: var(--color-btn-primary-disabled-border);
+ fill: var(--color-btn-primary-disabled-text);
+ }
+}
+
+// default (secondary)
+.Button--secondary {
+ color: var(--color-btn-text);
+ fill: var(--color-fg-muted); // help this
+ background-color: var(--color-btn-bg);
+ border-color: var(--color-btn-border);
+ box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow);
+
+ &:hover,
+ [open] > & {
+ background-color: var(--color-btn-hover-bg);
+ border-color: var(--color-btn-hover-border);
+ }
+
+ &:active,
+ &.Button--active {
+ background-color: var(--color-btn-active-bg);
+ border-color: var(--color-btn-active-border);
+ }
+
+ &[aria-pressed='true'],
+ &.Button--pressed {
+ background-color: var(--color-btn-selected-bg);
+ box-shadow: var(--color-primer-shadow-inset);
+ }
+
+ &:disabled,
+ &.Button--disabled,
+ &[aria-disabled='true'] {
+ color: var(--color-primer-fg-disabled);
+ background-color: var(--color-btn-bg);
+ border-color: var(--color-btn-border);
+ fill: var(--color-primer-fg-disabled);
+ }
+}
+
+// link color without svg
+.Button--invisible {
+ color: var(--color-fg-default);
+ fill: var(--color-fg-default);
+ border: none;
+
+ &:hover,
+ [open] > & {
+ background-color: var(--color-segmented-control-button-hover-bg);
+ }
+
+ &[aria-pressed='true'],
+ &:active,
+ &.Button--active,
+ &.Button--pressed {
+ background-color: var(--color-segmented-control-button-active-bg);
+ // box-shadow: var(--color-primer-shadow-inset);
+ }
+
+ &:disabled,
+ &.Button--disabled,
+ &[aria-disabled='true'] {
+ color: var(--color-primer-fg-disabled);
+ background-color: var(--color-btn-bg);
+ border-color: var(--color-btn-border);
+ fill: var(--color-primer-fg-disabled);
+ }
+
+ // if visual is present, muted label color
+ .Button-label:not(:only-child) {
+ color: var(--color-btn-text);
+ }
+
+ // if trailingAction is present, muted label color
+ .Button-content:not(:only-child) {
+ .Button-label {
+ color: var(--color-btn-text);
+ }
+ }
+}
+
+// danger
+.Button--danger {
+ color: var(--color-btn-danger-text);
+ fill: var(--color-btn-danger-icon);
+ background-color: var(--color-btn-bg);
+ border-color: var(--color-btn-border);
+ box-shadow: var(--color-btn-shadow), var(--color-btn-inset-shadow);
+
+ &:hover,
+ [open] > & {
+ color: var(--color-btn-danger-hover-text);
+ fill: var(--color-btn-danger-hover-text);
+ background-color: var(--color-btn-danger-hover-bg);
+ border-color: var(--color-btn-danger-hover-border);
+ box-shadow: var(--color-btn-danger-hover-shadow), var(--color-btn-danger-hover-inset-shadow);
+ }
+
+ &:active,
+ &[aria-pressed='true'],
+ &.Button--pressed {
+ color: var(--color-btn-danger-selected-text);
+ fill: var(--color-btn-danger-selected-text);
+ background-color: var(--color-btn-danger-selected-bg);
+ border-color: var(--color-btn-danger-selected-border);
+ box-shadow: var(--color-btn-danger-selected-shadow);
+ }
+
+ &:disabled,
+ &.disabled,
+ &[aria-disabled='true'] {
+ color: var(--color-btn-danger-disabled-text);
+ fill: var(--color-btn-danger-disabled-text);
+ background-color: var(--color-btn-danger-disabled-bg);
+ border-color: var(--color-btn-border);
+ }
+}
diff --git a/src/button-refactor/icon-button.scss b/src/button-refactor/icon-button.scss
new file mode 100644
index 0000000000..f2b39a5a02
--- /dev/null
+++ b/src/button-refactor/icon-button.scss
@@ -0,0 +1,14 @@
+.Button--iconOnly {
+ display: grid;
+ place-content: center;
+ padding: unset;
+ width: var(--primer-control-medium-size);
+
+ &.Button--small {
+ width: var(--primer-control-small-size);
+ }
+
+ &.Button--large {
+ width: var(--primer-control-large-size);
+ }
+}
diff --git a/src/button-refactor/index.scss b/src/button-refactor/index.scss
new file mode 100644
index 0000000000..739c32c617
--- /dev/null
+++ b/src/button-refactor/index.scss
@@ -0,0 +1,6 @@
+@import '../support/index.scss';
+@import './button.scss';
+@import './button-group.scss';
+@import './icon-button.scss';
+@import './link.scss';
+@import './link-styled-as-button.scss';
diff --git a/src/button-refactor/link-styled-as-button.scss b/src/button-refactor/link-styled-as-button.scss
new file mode 100644
index 0000000000..7915374118
--- /dev/null
+++ b/src/button-refactor/link-styled-as-button.scss
@@ -0,0 +1,75 @@
+// base link
+.LinkStyledAsButton {
+ @extend .Button;
+ display: inline-flex;
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.LinkStyledAsButton-content {
+ @extend .Button-content;
+}
+
+.LinkStyledAsButton-content--alignStart {
+ @extend .Button-content--alignStart;
+}
+
+// link child elements
+
+.LinkStyledAsButton-visual {
+ @extend .Button-visual;
+}
+
+.LinkStyledAsButton-label {
+ @extend .Button-label;
+}
+
+.LinkStyledAsButton-leadingVisual {
+ @extend .Button-leadingVisual;
+}
+
+.LinkStyledAsButton-trailingVisual {
+ @extend .Button-trailingVisual;
+}
+
+.LinkStyledAsButton-trailingAction {
+ @extend .Button-trailingAction;
+}
+
+// sizes
+
+.LinkStyledAsButton--small {
+ @extend .Button--small;
+}
+
+.LinkStyledAsButton--large {
+ @extend .Button--large;
+}
+
+.LinkStyledAsButton--fullWidth {
+ @extend .Button--fullWidth;
+}
+
+// variants
+
+// primary
+.LinkStyledAsButton--primary {
+ @extend .Button--primary;
+}
+
+// default (secondary)
+.LinkStyledAsButton--secondary {
+ @extend .Button--secondary;
+}
+
+// invisible
+.LinkStyledAsButton--invisible {
+ @extend .Button--invisible;
+}
+
+// danger
+.LinkStyledAsButton--danger {
+ @extend .Button--danger;
+}
diff --git a/src/button-refactor/link.scss b/src/button-refactor/link.scss
new file mode 100644
index 0000000000..3b69f702b1
--- /dev/null
+++ b/src/button-refactor/link.scss
@@ -0,0 +1,28 @@
+.Link {
+ color: var(--color-accent-fg);
+ fill: var(--color-accent-fg);
+
+ &:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+
+ // variants
+
+ &.Link--muted {
+ color: var(--color-fg-muted);
+ fill: var(--color-fg-muted);
+
+ &:hover {
+ color: var(--color-accent-fg);
+ fill: var(--color-accent-fg);
+ }
+ }
+}
+
+.Link-trailingVisual {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.25em;
+}
diff --git a/src/core/index.scss b/src/core/index.scss
index bc704ee041..ccac65d3ff 100644
--- a/src/core/index.scss
+++ b/src/core/index.scss
@@ -24,7 +24,6 @@
@import '../pagination/index.scss';
@import '../tooltips/index.scss';
@import '../truncate/index.scss';
-@import '../overlay/index.scss';
-
+@import '../button-refactor/index.scss';
// Utilities always go last so that they can override components
@import '../utilities/index.scss';