Skip to content

feat: add BareButton primitive, migrate role=button sites#7733

Open
SahilJat wants to merge 1 commit into
Flagsmith:mainfrom
SahilJat:feat/bare-button-primitive
Open

feat: add BareButton primitive, migrate role=button sites#7733
SahilJat wants to merge 1 commit into
Flagsmith:mainfrom
SahilJat:feat/bare-button-primitive

Conversation

@SahilJat

@SahilJat SahilJat commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for submitting a PR! Please check the boxes below:

[X] I have read the Contributing Guide /Flagsmith/flagsmith/blob/main/CONTRIBUTING.md.
[ ] I have added information to docs/ if required so people know about the feature.
[X] I have filled in the "Changes" section below.
[X] I have filled in the "How did you test this code" section below.

Changes

Closes #7626

New BareButton primitive

When we need a clickable surface that doesn't look like a button (stepper steps, selectable cards, list rows, chip-delete icons), the previous options were both problematic:

— manual keyboard handling, easy to get wrong, no disabled semantics
• + per-component all: unset — repeats the reset everywhere, easy to drift

BareButton renders a native

with all browser defaults reset via a single .bare-btn CSS class. It gives focus management, disabled semantics, keyboard handling and screen-
reader
role for free.

New files:

• web/components/base/forms/BareButton.tsx — component with forwardRef , accepts all

props
• web/styles/components/_bare-button.scss — all: unset + minimal layout primitives + focus-visible outline + disabled cursor
• documentation/components/BareButton.stories.tsx — Storybook stories (Default, Disabled, AsSelectableCard, AsChipDelete)
• web/components/base/forms/tests/BareButton.test.ts — Jest unit test (exports, displayName, forwardRef, prop types)

Migrated all role='button' sites in web/ :

• SelectableCard.tsx —

→ , removed manual onKeyDown handler
• ChipInput.tsx — → , added aria-label for accessibility
• Paging.js — 3×
→ , these previously had no keyboard handling at all

How did you test this code?

  1. Storybook — npm run storybook → navigate to Components → Forms → BareButton:
    • Verified the button renders with no browser default styling (no border, no background)
    • Verified Tab focuses the button and shows a focus-visible outline
    • Verified Enter and Space activate the click handler
    • Verified the Disabled story is non-interactive (no cursor, cannot click or focus)
    • Verified the AsSelectableCard and AsChipDelete stories demonstrate correct migration patterns
  2. Grep verification — confirmed zero remaining role='button' instances in web/ :
    grep -rn "role='button'" frontend/web/ # no results
    grep -rn 'role="button"' frontend/web/ # only the JSDoc comment in BareButton.tsx

@SahilJat SahilJat requested a review from a team as a code owner June 9, 2026 04:44
@SahilJat SahilJat requested review from talissoncosta and removed request for a team June 9, 2026 04:44
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

@SahilJat is attempting to deploy a commit to the Flagsmith Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the front-end Issue related to the React Front End Dashboard label Jun 9, 2026

@talissoncosta talissoncosta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job @SahilJat! Two things worth tidying before this lands:

1. Co-locate the styles with the component.

Our newer components keep their SCSS next to them (e.g. SelectableCard.scss, ContentCard.scss, CenteredModal.scss) rather than as a global partial. Could you move this to web/components/base/forms/BareButton.scss, import it from the component (import './BareButton.scss' in BareButton.tsx), and drop the @import 'bare-button' line from web/styles/components/_index.scss?

2. Make the reset zero-specificity.

.bare-btn is (0,1,0) — the same as a single consumer class like .selectable-card. When composed, all: unset ties with the consumer's styles and the winner falls to stylesheet load order (which matters even more once this loads via component import). .page/.chip-icon only survive because they're nested (0,2,0); .selectable-card is a flat class and could get flattened. Wrapping the reset in :where() drops it to (0,0,0) so any consumer class deterministically wins. Keep the state rules at normal specificity so the focus ring still applies:

// Reset at zero specificity so consumer classes always win, regardless of load order.
:where(.bare-btn) {
  all: unset;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  box-sizing: border-box;
}

.bare-btn {
  &:disabled,
  &[aria-disabled='true'] { cursor: default; pointer-events: none; }
  &:focus-visible {
    outline: 2px solid var(--color-border-action);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }
}

Also, can you please rebase your PR and fix the conflicts ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

front-end Issue related to the React Front End Dashboard

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add BareButton design-system primitive

2 participants