fix(Button): hover/cursor affordances + feat(ButtonLink) for navigation#307
Open
runleveldev wants to merge 3 commits into
Open
fix(Button): hover/cursor affordances + feat(ButtonLink) for navigation#307runleveldev wants to merge 3 commits into
runleveldev wants to merge 3 commits into
Conversation
Tailwind v4 preflight resets buttons to cursor: default, so Button lost the clickable affordance (QuickAction already opts back in with cursor-pointer; this brings Button to parity). Ghost's hover tint moves one token step (neutral-100 -> neutral-200, dark neutral-800 -> neutral-700, actives follow): neutral-100 on a white page is a ~4% delta most displays cannot show, so the hover state read as "nothing happens" and users only noticed feedback via the active state on click.
A polymorphic link that reuses buttonVariants, replacing the <Link><Button/></Link> anti-pattern (invalid interactive-inside- interactive HTML, double tab stop, button cursor hiding the link pointer). Renders <a href> by default; SPA consumers pass their router's link via `as` (react-router, Next.js) to keep client-side navigation. Keeps native link affordances: middle-click/Ctrl+click to a new tab, copy link address, correct assistive-technology semantics. Deliberately a separate component rather than an `as`/`asChild` prop on Button: links have no disabled/loading semantics, so each component keeps an honest API.
Tailwind v4 gates the built-in hover variant behind @media (hover: hover). Browsers in VMs, remote desktops, and some touchscreen laptops misreport pointer capability as hover-incapable, silently disabling every hover style in the compiled CSS while :hover itself still matches mouse movement. MIE apps are mouse-first, so opt out of the capability gate via @custom-variant, next to the existing dark variant. Tradeoff is the standard pre-v4 behavior: hover styles can linger after a tap on touch devices.
Deploying ui with
|
| Latest commit: |
a6a0ed5
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://0d14faf2.ui-6d0.pages.dev |
| Branch Preview URL: | https://button-affordance-and-button.ui-6d0.pages.dev |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves interaction affordances in the design system by restoring a pointer cursor and a more perceptible hover state for Button, adding a ButtonLink component for navigation without nested interactive elements, and ensuring hover: styles apply even when Tailwind v4’s hover capability gating would otherwise suppress them.
Changes:
- Adjust
Buttonbase/ghost styling (cursor-pointer, stronger ghost hover/active tokens). - Add
ButtonLink(polymorphicassupport) with Storybook autodocs, unit tests, barrel export, and tsup entry. - Override Tailwind v4 hover gating via a custom
hovervariant in base styles.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tsup.config.ts | Adds tsup entrypoint for components/ButtonLink. |
| src/styles/base.css | Defines @custom-variant hover (&:hover); to apply hover styles unconditionally. |
| src/index.ts | Re-exports ButtonLink from the package root. |
| src/components/ButtonLink/index.ts | Component barrel export for ButtonLink. |
| src/components/ButtonLink/ButtonLink.tsx | Implements ButtonLink using buttonVariants, icons, and as rendering. |
| src/components/ButtonLink/ButtonLink.test.tsx | Adds unit tests for rendering, class application/merging, as, icons, and ref forwarding. |
| src/components/ButtonLink/ButtonLink.stories.tsx | Adds autodocs stories demonstrating variants, icons, and router-link usage. |
| src/components/Button/Button.tsx | Restores pointer cursor and increases ghost hover/active contrast (incl. dark mode). |
Comment on lines
+34
to
+41
| // One token step stronger than before: neutral-100 on a white page | ||
| // is a ~4% delta most displays can't show, so hover read as | ||
| // "nothing happens" and users only saw the active state on click. | ||
| 'hover:bg-neutral-200', | ||
| 'active:bg-neutral-300', | ||
| 'dark:text-neutral-400', | ||
| 'dark:hover:bg-neutral-800', | ||
| 'dark:active:bg-neutral-700', | ||
| 'dark:hover:bg-neutral-700', | ||
| 'dark:active:bg-neutral-600', |
Comment on lines
+124
to
+125
| target="_blank" | ||
| rel="noreferrer" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related fixes for button affordances, motivated by a consumer report in
opensource-server/create-a-containerwhere ghost buttons appeared to have no hover state at all (users only saw feedback when clicking) and navigation "buttons" showed the wrong cursor.1.
fix(Button): pointer cursor + perceptible ghost hovercursor-pointerin the base styles. Tailwind v4's preflight resets buttons tocursor: default, soButtonsilently lost its clickable affordance.QuickActionalready opts back in — this bringsButtonto parity with its own siblings.hover:bg-neutral-100 → 200,active: 200 → 300(dark:hover: 800 → 700,active: 700 → 600). Measured on a white page,neutral-100(#f5f5f5) is a ~4% luminance delta — effectively invisible on most displays, which is why hover read as "nothing happens" and the (ungated, stronger) active state was the only feedback users noticed.2.
feat(ButtonLink): navigation links styled as buttonsConsumers wrap
<Button>in a router<Link>to get button-looking navigation. That nests interactive elements (invalid HTML), creates a double tab stop, and the button's cursor hides the link pointer.ButtonLinkreusesbuttonVariantson a real link element:Real links keep middle-click/Ctrl+click to a new tab, "copy link address", and correct assistive-technology semantics. It's deliberately a separate component rather than
as/asChildonButton: links have no disabled/loading semantics, so each keeps an honest API. Ships with autodocs story, unit tests, barrel export, and a tsup entry.3.
fix(styles): apply hover styles unconditionallyTailwind v4 wraps the built-in
hover:variant in@media (hover: hover). Browsers in VMs/remote desktops and some touchscreen laptops misreport pointer capability, which silently disables every hover style in the compiled CSS even though:hoverstill matches actual mouse movement. Added@custom-variant hover (&:hover);next to the existingdarkcustom variant, per Tailwind's documented escape hatch. Tradeoff: pre-v4 behavior returns on touch devices (hover styles may linger after a tap).Testing
pnpm typecheck && pnpm lint && pnpm format && pnpm test— all green (255 tests, incl. 6 newButtonLinktests)button-primary.png,button-all-variants.png) capture non-hovered states, so the ghost hover change should not disturb them;cursornever appears in screenshots. Happy to runpnpm test:visualand attach diffs if CI disagrees.Notes for reviewers