diff --git a/.changeset/nice-days-jog.md b/.changeset/nice-days-jog.md
new file mode 100644
index 0000000000..fc39db461f
--- /dev/null
+++ b/.changeset/nice-days-jog.md
@@ -0,0 +1,7 @@
+---
+"@primer/css": major
+---
+
+UnderlineNav `:focus` styles
+Refactor selected state and spacing
+Add selected bold state override from github/github
diff --git a/.changeset/orange-ties-sin.md b/.changeset/orange-ties-sin.md
new file mode 100644
index 0000000000..fbb368a808
--- /dev/null
+++ b/.changeset/orange-ties-sin.md
@@ -0,0 +1,5 @@
+---
+"@primer/css": patch
+---
+
+Use `counter-border` for LHC
diff --git a/docs/src/stories/components/Navigation/UnderlineNav.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNav.stories.jsx
new file mode 100644
index 0000000000..a1e5a76dcb
--- /dev/null
+++ b/docs/src/stories/components/Navigation/UnderlineNav.stories.jsx
@@ -0,0 +1,73 @@
+import React from 'react'
+import clsx from 'clsx'
+import {UnderlineNavItemTemplate} from './UnderlineNavItem.stories'
+
+export default {
+ title: 'Components/Navigation/UnderlineNav',
+ excludeStories: ['UnderlineNavTemplate'],
+ layout: 'padded',
+ argTypes: {
+ variant: {
+ options: [0, 1, 2], // iterator
+ mapping: ['', 'UnderlineNav--right', 'UnderlineNav--full'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['default', 'align-right', 'fullwidth']
+ },
+ description: 'nav positioning options',
+ table: {
+ category: 'CSS'
+ }
+ },
+ children: {
+ description: 'creates a slot for children',
+ table: {
+ category: 'HTML'
+ }
+ },
+ actionStart: {
+ description: 'action to left of nav',
+ table: {
+ category: 'HTML'
+ }
+ },
+ actionEnd: {
+ description: 'action to right of nav',
+ table: {
+ category: 'HTML'
+ }
+ }
+ }
+}
+
+export const UnderlineNavTemplate = ({variant, children, actionStart, actionEnd}) => (
+ <>
+
+ >
+)
+
+export const Playground = UnderlineNavTemplate.bind({})
+Playground.args = {
+ variant: 0,
+ children: (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/docs/src/stories/components/Navigation/UnderlineNavAction.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNavAction.stories.jsx
new file mode 100644
index 0000000000..7e43dd03f8
--- /dev/null
+++ b/docs/src/stories/components/Navigation/UnderlineNavAction.stories.jsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import clsx from 'clsx'
+import {ButtonTemplate} from '../Button/Button.stories'
+import {LinkTemplate} from '../Link/Link.stories'
+
+export default {
+ title: 'Components/Navigation/UnderlineNav/Action',
+ excludeStories: ['UnderlineNavActionTemplate'],
+ layout: 'padded',
+ argTypes: {
+ semanticItemType: {
+ options: ['button', 'link'],
+ control: {
+ type: 'inline-radio'
+ },
+ description: 'item can be a button or a link',
+ table: {
+ category: 'HTML'
+ }
+ },
+ label: {
+ name: 'label',
+ type: 'string',
+ description: 'Item label text',
+ table: {
+ category: 'HTML'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set manual focus on item',
+ table: {
+ category: 'Interactive'
+ }
+ }
+ }
+}
+
+export const UnderlineNavActionTemplate = ({semanticItemType, label, focusElement}) => {
+ return (
+
+ {semanticItemType === 'button' ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export const Playground = UnderlineNavActionTemplate.bind({})
+Playground.args = {
+ semanticItemType: 'button',
+ label: 'Action',
+ focusElement: false
+}
diff --git a/docs/src/stories/components/Navigation/UnderlineNavItem.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNavItem.stories.jsx
new file mode 100644
index 0000000000..da27a1227f
--- /dev/null
+++ b/docs/src/stories/components/Navigation/UnderlineNavItem.stories.jsx
@@ -0,0 +1,140 @@
+import React from 'react'
+import clsx from 'clsx'
+import useToggle from '../../helpers/useToggle.jsx'
+
+export default {
+ title: 'Components/Navigation/UnderlineNav/Item',
+ excludeStories: ['UnderlineNavItemTemplate'],
+ layout: 'padded',
+ argTypes: {
+ selected: {
+ control: {type: 'boolean'},
+ description: 'active nav item',
+ table: {
+ category: 'CSS'
+ }
+ },
+ usesDataContent: {
+ control: {type: 'boolean'},
+ description: 'creates a hidden label to allow for bold text without layout shift',
+ table: {
+ category: 'CSS'
+ }
+ },
+ semanticItemType: {
+ options: ['button', 'link'],
+ control: {
+ type: 'inline-radio'
+ },
+ description: 'item can be a button or a link',
+ table: {
+ category: 'HTML'
+ }
+ },
+ label: {
+ name: 'label',
+ type: 'string',
+ description: 'Item label text',
+ table: {
+ category: 'HTML'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set manual focus on tab item',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ icon: {
+ control: {type: 'boolean'},
+ description: 'show icon',
+ table: {
+ category: 'CSS'
+ }
+ },
+ counter: {
+ control: {type: 'boolean'},
+ description: 'show counter',
+ table: {
+ category: 'CSS'
+ }
+ }
+ }
+}
+
+export const UnderlineNavItemTemplate = ({
+ semanticItemType,
+ label,
+ selected,
+ focusElement,
+ icon,
+ counter,
+ usesDataContent
+}) => {
+ const [isSelected, itemisSelected] = useToggle()
+ return (
+
+ {semanticItemType === 'button' ? (
+
+ ) : (
+
+ {icon && (
+
+ )}
+ {label}
+ {counter && 10}
+
+ )}
+
+ )
+}
+
+export const Playground = UnderlineNavItemTemplate.bind({})
+Playground.args = {
+ semanticItemType: 'button',
+ label: 'Item',
+ selected: false,
+ focusElement: false,
+ icon: false,
+ counter: false,
+ usesDataContent: true
+}
diff --git a/docs/src/stories/components/Navigation/UnderlineNavPatterns.stories.jsx b/docs/src/stories/components/Navigation/UnderlineNavPatterns.stories.jsx
new file mode 100644
index 0000000000..f5b152b6b0
--- /dev/null
+++ b/docs/src/stories/components/Navigation/UnderlineNavPatterns.stories.jsx
@@ -0,0 +1,111 @@
+import React from 'react'
+import clsx from 'clsx'
+import {UnderlineNavTemplate} from './UnderlineNav.stories'
+import {UnderlineNavItemTemplate} from './UnderlineNavItem.stories'
+import {UnderlineNavActionTemplate} from './UnderlineNavAction.stories'
+
+export default {
+ title: 'Components/Navigation/UnderlineNav/Features',
+ layout: 'padded'
+}
+
+export const LinkItems = UnderlineNavTemplate.bind({})
+LinkItems.args = {
+ children: (
+ <>
+
+
+
+ >
+ )
+}
+
+export const ButtonItems = UnderlineNavTemplate.bind({})
+ButtonItems.args = {
+ children: (
+ <>
+
+
+
+ >
+ )
+}
+
+export const NavRight = UnderlineNavTemplate.bind({})
+NavRight.args = {
+ variant: 'UnderlineNav--right',
+ children: (
+ <>
+
+
+
+ >
+ )
+}
+
+export const NavFullWidth = UnderlineNavTemplate.bind({})
+NavFullWidth.args = {
+ variant: 'UnderlineNav--full',
+ children: (
+ <>
+
+
+
+ >
+ )
+}
+
+export const ActionRight = UnderlineNavTemplate.bind({})
+ActionRight.args = {
+ children: (
+ <>
+
+
+
+ >
+ ),
+ actionEnd:
+}
+
+export const ActionLeft = UnderlineNavTemplate.bind({})
+ActionLeft.args = {
+ children: (
+ <>
+
+
+
+ >
+ ),
+ actionStart:
+}
+
+export const Overflow = UnderlineNavTemplate.bind({})
+Overflow.args = {
+ children: (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export const Icons = UnderlineNavTemplate.bind({})
+Icons.args = {
+ children: (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/labels/counters.scss b/src/labels/counters.scss
index ed71943623..d170b91a53 100644
--- a/src/labels/counters.scss
+++ b/src/labels/counters.scss
@@ -11,7 +11,7 @@
color: var(--color-fg-default);
text-align: center;
background-color: var(--color-neutral-muted);
- border: $border-width $border-style transparent; // Support Firefox custom colors
+ border: $border-width $border-style var(--color-counter-border);
// stylelint-disable-next-line primer/borders
border-radius: 2em;
diff --git a/src/navigation/underline-nav.scss b/src/navigation/underline-nav.scss
index bac88d8aa4..c6cb37cd18 100644
--- a/src/navigation/underline-nav.scss
+++ b/src/navigation/underline-nav.scss
@@ -1,50 +1,99 @@
+$nav-height: $spacer-3 * 3 !default; // 48px
+
.UnderlineNav {
display: flex;
+ min-height: $nav-height;
overflow-x: auto;
overflow-y: hidden;
// stylelint-disable-next-line primer/box-shadow
box-shadow: inset 0 -1px 0 var(--color-border-muted);
+ -webkit-overflow-scrolling: auto;
justify-content: space-between;
}
.UnderlineNav-body {
display: flex;
+ align-items: center;
+ gap: $spacer-2;
+ list-style: none;
}
.UnderlineNav-item {
- padding: $spacer-2 $spacer-3;
+ position: relative;
+ display: flex;
+ padding: 0 $spacer-2;
font-size: $body-font-size;
// stylelint-disable-next-line primer/typography
line-height: 30px;
color: var(--color-fg-default);
text-align: center;
white-space: nowrap;
+ cursor: pointer;
background-color: transparent;
border: 0;
- // stylelint-disable-next-line primer/borders
- border-bottom: 2px $border-style transparent;
+ border-radius: $border-radius;
+ align-items: center;
- &:hover,
- &:focus {
- color: var(--color-fg-default);
- text-decoration: none;
- border-bottom-color: var(--color-neutral-muted);
- outline: 1px dotted transparent; // Support Firefox custom colors
- outline-offset: -1px;
- transition: border-bottom-color 0.12s ease-out;
+ // renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected
+ [data-content]::before {
+ display: block;
+ height: 0;
+ font-weight: $font-weight-bold;
+ visibility: hidden;
+ content: attr(data-content);
+ }
+
+ // increase touch target area
+ &::before {
+ @include minTouchTarget($min-height: $nav-height);
+ }
+
+ // hover state was "sticking" on mobile after click
+ @media (pointer: fine) {
+ &:hover {
+ color: var(--color-fg-default);
+ text-decoration: none;
+ background: var(--color-action-list-item-default-hover-bg);
+ transition: background 0.12s ease-out;
+ }
}
&.selected,
- &[role=tab][aria-selected=true],
- &[aria-current]:not([aria-current=false]) {
+ &[role='tab'][aria-selected='true'],
+ &[aria-current]:not([aria-current='false']) {
font-weight: $font-weight-bold;
color: var(--color-fg-default);
- border-bottom-color: var(--color-primer-border-active);
- outline: 1px dotted transparent; // Support Firefox custom colors
- outline-offset: -1px;
- .UnderlineNav-octicon {
- color: var(--color-fg-muted);
+ // current/selected underline
+ &::after {
+ position: absolute;
+ right: 50%;
+ // 48px total height / 2 (24px) + 1px
+ bottom: calc(50% - 25px);
+ width: 100%;
+ height: 2px;
+ content: '';
+ background: var(--color-primer-border-active);
+ border-radius: $border-radius;
+ transform: translate(50%, -50%);
+ }
+ }
+
+ // remove when global focus state is merged
+ &.focus,
+ &:focus {
+ @include focusOutline;
+ outline-offset: -2px;
+ }
+
+ .Counter {
+ margin-left: $spacer-2;
+ color: var(--color-fg-default);
+ background-color: var(--color-neutral-muted);
+
+ &--primary {
+ color: var(--color-fg-on-emphasis);
+ background-color: var(--color-neutral-emphasis);
}
}
}
@@ -63,22 +112,18 @@
.UnderlineNav--full {
display: block;
-}
-.UnderlineNav-octicon {
- margin-right: $spacer-1;
- color: var(--color-fg-subtle);
+ // required for underline to align with additional wrapper element
+ .UnderlineNav-body {
+ min-height: $nav-height;
+ }
}
-.UnderlineNav .Counter {
- margin-left: $spacer-1;
- color: var(--color-fg-default);
- background-color: var(--color-neutral-muted);
-
- &--primary {
- color: var(--color-fg-on-emphasis);
- background-color: var(--color-neutral-emphasis);
- }
+.UnderlineNav-octicon {
+ display: inline !important;
+ margin-right: $spacer-2;
+ color: var(--color-fg-muted);
+ fill: var(--color-fg-muted);
}
.UnderlineNav-container {
diff --git a/src/support/mixins/misc.scss b/src/support/mixins/misc.scss
index c27ac96a75..8c7f4c77b8 100644
--- a/src/support/mixins/misc.scss
+++ b/src/support/mixins/misc.scss
@@ -24,3 +24,27 @@
background-color: $border;
}
}
+
+// global focus styles
+
+@mixin focusOutline {
+ z-index: 1;
+ outline: 2px solid var(--color-accent-fg);
+ outline-offset: 2px;
+}
+
+// if min-width is undefined, return only min-height
+@mixin minTouchTarget($min-height: 32px, $min-width: '') {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 100%;
+ height: 100%;
+ min-height: $min-height;
+ content: '';
+ transform: translateX(-50%) translateY(-50%);
+
+ @if $min-width != '' {
+ min-width: $min-width;
+ }
+}