Button UI & Design System Guide
Comprehensive architectural reference, design system manual, and practical usage guide for all button UI in Waldur HomePort. This guide covers BaseButton, buttonVariants(), direct tooltip integration, design tokens, links styled as buttons, dropdown toggles, segmented controls, accessibility models, and linting guardrails.
Table of Contents
- Architecture & Design Philosophy
- Unified Button Engine
- Integer Height System
- Inset Box-Shadow Border Mechanics
- Transitions & Easing
- Focus Ring & Forced-Colors Accessibility
- Active State Press Dynamics
- The 12 Button Variants & Design Tokens
- Solid & Bordered Variants
- Text Variants
- Design Token Architecture & Dark Theme Inversion
- Complete Token Mapping Reference
- Tooltips, Disabled States & Direct DOM Integration
- Why
data-disabledand Direct Tooltips Replace Wrapper Spans - The
alwaysMountLifecycle Guarantee - Explaining Unavailable Actions (
disabledReasonvstooltip) - Lint Enforcement:
enforce-disabled-button-tooltip - Icons, Typography & Loading States
- Phosphor Icon Integration
- Exact SVG Sizing via Standard Tailwind Classes
- Icon-Only Square Buttons
- Loading Spinner Mechanics (
pending) - Prohibition of
.svg-iconWrappers - Links Rendered as Buttons
- Supported Link Props
- Text-Anchor Suppression
- Specialized Entity Link Wrappers
- Prohibited Link Button Patterns
- Dropdown & Menu Toggles
- Why Action Toggles Use
buttonVariants()Instead ofBaseButton - Caret Rotation Animation (
ButtonCaret) - Required Marker Classes (
dropdown-toggle,no-arrow,btn-icon) - Centralized Icon Sizing in Toggles
- Segmented Controls & View Switchers
- SegmentedControl vs. BaseButton
- Keyboard & Focus Model (Radix RadioGroup)
- Variants:
neutralvs.brand - Flexbox Alignment Safeguard (
self-center) - Shared Styling with Real Tab Panels (
SigninForm) - Convenience & Specialized Wrappers
SubmitButtonCloseDialogButtonCompactEditButtonSaveButton- Component Decision Matrix
- Linting Rules & Prohibited Anti-Patterns
1. Architecture & Design Philosophy
Unified Button Engine
Waldur HomePort's button architecture is built on top of Tailwind CSS v4, Radix UI primitives, and the design tokens engine. All legacy Bootstrap and Metronic .btn CSS rules have been removed from the application bundle.
Every button in the application is rendered either by:
BaseButton(import { BaseButton } from 'waldur-ui'), orbuttonVariants()applied to elements that cannot be a<button>(e.g. router<Link>, custom Radix triggers).
Both share the exact same class-variance-authority (cva) definition, ensuring identical typography, heights, paddings, state tokens, focus rings, and transitions across the entire codebase.
Integer Height System
Buttons are implemented with exact, predictable pixel heights across three standardized tiers:
| Size | Height | Padding (px / py) |
Typography & Line Height | Corner Radius | Icon Size | Default Context |
|---|---|---|---|---|---|---|
sm |
28px | px-[8px] py-[4px] |
text-sm leading-5 (14px/20px) |
rounded-md (6px) |
16px (size-4 wrapper) |
Table rows, filter bars, inline badges, popovers |
md |
36px | px-[12px] py-[8px] |
text-sm leading-5 (14px/20px) |
rounded-lg (8px) |
20px (size-5 wrapper) |
Default size. Page actions, card headers, toolbars |
lg |
44px | px-[16px] py-[10px] |
text-base leading-6 (16px/24px) |
rounded-lg (8px) |
20px (size-5 wrapper) |
Form submissions, dialog footers, primary hero CTAs |
[!NOTE] The default size is
md(36px).BaseButtonandbuttonVariants()default tosize="md"when omitted.
Inset Box-Shadow Border Mechanics
A standard CSS border: 1px solid ... participates in border-box layout calculation. Under a 13px root font size and fractional rem cascades, physical borders produced fractional heights (e.g. 43.97px instead of 44px), causing subpixel rasterization errors where 1px borders rendered blurry, uneven, or visually thicker depending on scroll position.
To ensure pixel-perfect geometric heights across all operating systems and display densities, BaseButton uses an inset box-shadow border:
1 2 | |
- Inset shadows do not expand outer box dimensions.
- Dimensions remain clean integers (
28px,36px,44px). - Variants without visible borders declare
shadow-[inset_0_0_0_1px_transparent]to maintain identical layout geometry without layout shifting.
Transitions & Easing
Tailwind's standard transition-colors utility animates color, background-color, and border-color, but does not animate box-shadow. Because the border is drawn via an inset box-shadow, standard transitions caused the border color to snap instantly while the background eased.
BaseButton explicitly uses:
1 | |
This guarantees that background colors and border outlines ease synchronously across :hover, :focus-visible, and :active states.
Focus Ring & Forced-Colors Accessibility
Focus rings are rendered using native CSS outlines rather than outer box-shadows:
1 | |
Why native outline?
- Container resets: Global reset rules or parent card containers applying
box-shadow: none !importantcannot suppress native outlines. - Forced-Colors / High-Contrast mode: Windows High Contrast and assistive technologies honor native
outlineproperties while often strippingbox-shadow. - Clean mouse interaction:
:focus-visibleensures keyboard navigation displays an authoritative 2px focus indicator, while mouse clicks do not leave persistent rings. - Contrast compliance: Focus rings for all variants (including
tertiary) use brand or high-contrast error tokens to achieve a minimum 3:1 contrast ratio against light and dark backgrounds (WCAG 2.1 AA / 1.4.11 Non-text Contrast).
Active State Press Dynamics
When a user triggers a button via the keyboard (Enter or Space), the button matches :active and :focus-visible simultaneously.
active:shadow-none: Clears the inset border shadow during press so it does not bleed through the active background fill.- High-contrast text flip on vivid fills: For
danger,warning, andsuccess, the active press state switches from a subtle tint to a vivid, saturated fill (--btn-*-bg-pressed). To preserve readability and contrast, the text color flips to crisp white:
1 | |
2. The 12 Button Variants & Design Tokens
Waldur HomePort defines 12 standard variants across two families:
1 2 3 4 | |
Solid & Bordered Variants
| Variant | Purpose & Hierarchy | Light Mode Appearance | Active Press Appearance |
|---|---|---|---|
primary |
Main call-to-action on a page or dialog | Solid brand fill (brand-600), white text |
Darker brand fill (brand-800), shadow cleared |
secondary |
Supporting action complementary to primary | Two-tone: brand tint (brand-50), brand border (brand-300), plum text (brand-900), and vibrant magenta icon (brand-500) |
Darker brand tint (brand-300), brand text |
tertiary |
Default neutral button for tables, headers, toolbars | Solid white, subtle gray border (gray-300), dark gray text |
Light gray fill (gray-100), shadow cleared |
tertiary-ghost |
Low-emphasis action with no idle border | Transparent background, tertiary text | Light gray fill (gray-100), shadow cleared |
danger |
Destructive actions (Delete, Terminate, Revoke) | Error tint (error-50), red border & text |
Vivid red fill (error-500), white text |
warning |
Cautionary actions requiring warning | Warning tint (warning-50), amber border & text |
Vivid amber fill (warning-600), white text |
success |
Confirmation / affirmative actions (Approve, Accept) | Success tint (success-50), green border & text |
Vivid green fill (success-500), white text |
Text Variants
Text variants render with a transparent background and no border in their idle state. They are ideal for inline table actions, breadcrumb links, card headers, and compact secondary actions:
| Variant | Idle Styling | Hover State | Active Press State |
|---|---|---|---|
text-primary |
Brand text (brand-700), transparent bg |
Brand-tint hover (brand-50) |
In light mode: stays brand-700In dark mode: brightens to gray-50 |
text-secondary |
Gray text (gray-700), transparent bg |
Subtle gray hover (gray-100) |
Transparent bg, gray text |
text-danger |
Red text (error-700), transparent bg |
Error-tint hover (error-50), red text |
Transparent bg, error text |
text-warning |
Amber text (warning-600), transparent bg |
Warning-tint hover (warning-50), amber text |
Transparent bg, warning text |
text-success |
Green text (success-700), transparent bg |
Success-tint hover (success-50), green text |
Transparent bg, success text |
Design Token Architecture & Dark Theme Inversion
All button colors are defined in packages/design-tokens/src/buttonColors.css. Zero button styling relies on Metronic SCSS or runtime Bootstrap mixins.
- Brand-Reactive vs. Fixed Ramps:
primary,secondary, andtext-primaryderive directly from the runtime--waldur-brand-*palette (customizable per tenant/deployment).danger,warning, andsuccessderive from static error/warning/success ramps.tertiarycombines a neutral gray surface with a--waldur-brand-600focus ring to ensure WCAG 2.1 compliance on white surfaces.- Dark Mode Mechanics (
:root[data-theme='dark']): - In dark mode, buttons automatically map to inverted dark surfaces (
--color-gray-dark-*and dark brand ramps). - Solid buttons (
tertiary,secondary) adopt deep dark surface colors (gray-dark-950,brand-900) with high-contrast text (gray-dark-300,gray-dark-50). - Disabled states shift to
var(--color-gray-dark-800)background withvar(--color-gray-dark-500)text.
Complete Token Mapping Reference
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
3. Tooltips, Disabled States & Direct DOM Integration
Why data-disabled and Direct Tooltips Replace Wrapper Spans
In legacy implementations, Tailwind/shadcn applied disabled:pointer-events-none to disabled buttons. However, pointer-events: none prevents the browser from firing mouseenter and pointerover events on <button disabled>. To make tooltips work on disabled buttons, libraries commonly introduced a workaround: wrapping the button in an extra <span className="inline-block">.
The wrapper <span> created severe architectural problems:
- Broken Flexbox Layouts: Placing an intermediate
<span>around the<button>intercepts flex parent rules. Classes likealign-self-center,self-end,w-100, orflex-1applied to the button failed to affect the wrapper, requiring fragile class mirroring hacks. - DOM Identity & Mount Churn: When a button toggled between enabled (no tooltip) and disabled (with tooltip), React saw different JSX trees (
<button>vs<Tooltip><span><button></span></Tooltip>). React destroyed the old DOM node and mounted a brand new button, losing keyboard focus and breaking test handles.
The Modern Waldur Solution:
- Removed
disabled:pointer-events-nonefrombuttonVariants(). - Added
disabled:cursor-not-allowed data-disabled:cursor-not-allowedto the base variants. - Added matching
data-disabled:*styles across all 12 variants. - Direct
<Tooltip>wrapping:<BaseButton>renders<button>directly inside<Tooltip>with zero wrapper spans:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Because pointer-events remain enabled on <button disabled>, the native <button> receives hover events directly. Radix Tooltip fires effortlessly while the cursor correctly shows not-allowed.
The alwaysMount Lifecycle Guarantee
Tooltip provides an alwaysMount prop (packages/ui/src/Tooltip.tsx). When a button specifies tooltip or disabledReason, BaseButton sets alwaysMount={true} on the Tooltip.
- Even when the button is currently enabled and
effectiveTooltipisundefined,alwaysMountensures the RadixTooltip.RootandTooltip.Triggerremain mounted in the DOM tree. - When the button transitions to disabled (triggering
disabledReason), the JSX tree structure remains 100% identical. - The button is never unmounted or remounted. Keyboard focus and DOM refs remain rock-solid across state transitions.
Explaining Unavailable Actions (disabledReason vs tooltip)
| Prop | Visibility | Typical Use Case |
|---|---|---|
disabledReason |
Only visible when button is disabled | Explaining why an action is blocked (e.g., missing permissions, quota exceeded, invalid state) |
tooltip |
Always visible (both enabled and disabled) | Describing what the button does (standard for icon-only buttons) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Lint Enforcement: enforce-disabled-button-tooltip
To guarantee outstanding UX, the custom ESLint rule waldur-custom/enforce-disabled-button-tooltip runs as an error across the repository.
Any <BaseButton disabled={...}> without a corresponding disabledReason or tooltip prop will fail CI linting:
1 2 3 4 5 6 7 8 9 | |
4. Icons, Typography & Loading States
Phosphor Icon Integration
Waldur HomePort uses @phosphor-icons/react with weight="bold" for button icons:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Exact SVG Sizing via Standard Tailwind Classes
Phosphor icons render with width="1em" height="1em" by default. In a standard <button>, 1em resolves against the button's font-size (14px on sm/md, 16px on lg), causing icons to look smaller than intended.
BaseButton wraps iconNode in a dedicated container styled with standard Tailwind utility classes:
1 2 3 4 5 6 7 8 9 | |
Why this architecture?
- Standard Tailwind classes:
size-4 [&>svg]:size-4(16px) onsm, andsize-5 [&>svg]:size-5(20px) onmd/lg. - The child
[&>svg]:size-*selector forces Phosphor icons past their intrinsic1emfont-size sizing directly via static Tailwind utilities. - Clean DOM without inline
--icon-sizestyles or custom CSS variable overhead.
Icon-Only Square Buttons
When label is omitted and iconNode is supplied, BaseButton automatically activates iconOnly: true:
- Padding is reset to
p-0. - Dimensions are pinned to exact squares:
28px × 28px(sm),36px × 36px(md), and44px × 44px(lg). aria-labelis automatically populated fromtooltipordisabledReasonif available.
1 2 3 4 5 6 7 8 | |
Loading Spinner Mechanics (pending)
When pending={true}:
- The button is automatically disabled (
isDisabled = disabled || pending). - The regular icon is swapped for LoadingSpinner.
- The spinner applies
-me-[3.25px]when a label is present to counteract thegap-2flex spacing, keeping the spinner positioned at the exact visual offset expected alongside the text. - Button width remains visually balanced without layout jumps.
1 2 3 4 5 6 | |
Prohibition of .svg-icon Wrappers
[!CAUTION] Never wrap button icons in
.svg-icon. Legacy Metronic stylesheets define.svg-icon { fill: #A1A5B7 !important; }. This rule beatscurrentColorand turns crisp white icons on primary buttons into a muddy gray. Always pass bare Phosphor icons directly toiconNode.
5. Links Rendered as Buttons
When a user action triggers router navigation (changing the URL or transitioning states) rather than invoking a mutation or callback, it must be rendered as an <a> element for accessibility and standard browser behaviors (middle click, open in new tab).
The application provides src/core/Link.tsx, which accepts button styling props and applies buttonVariants() to the underlying anchor:
1 2 3 4 5 6 7 8 9 10 | |
Supported Link Props
buttonVariant: AnyButtonVariant('primary','secondary','tertiary', etc.).buttonSize: AnyButtonSize('sm','md','lg'). Defaults to'md'.buttonIconOnly: Boolean for square icon-only anchors.
Text-Anchor Suppression
When buttonVariant is omitted, Link applies the default .text-anchor class (brand-colored underline text link). When buttonVariant is present, Link automatically suppresses .text-anchor, allowing buttonVariants() to control all colors.
Specialized Entity Link Wrappers
All domain link wrappers forward buttonVariant, buttonSize, and buttonIconOnly:
1 2 3 4 5 6 7 8 | |
Prohibited Link Button Patterns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
6. Dropdown & Menu Toggles
Action dropdown toggles—such as TableDropdownToggle, AddDropdownToggle, and ActionDropdownButton.Toggle—render dropdown triggers inside tables, card headers, and toolbars.
Why Action Toggles Use buttonVariants() Instead of BaseButton
Action toggles render a direct <button> element styled with buttonVariants({ variant, size }) rather than wrapping in BaseButton:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Rationale:
- Direct Child Selector for Caret Animation: The rotating caret animation is governed by:
1 2 3 | |
BaseButton wraps its iconNode inside an intermediate <span> wrapper. That extra wrapper prevents the > .rotate-toggle-180 child combinator from matching. Rendering a raw <button> with buttonVariants({ variant, size }) and <ButtonCaret size={size} /> ensures the caret remains a direct child.
- Clean Radix Trigger Composition:
RadixDropdownMenu.Trigger asChildclones its immediate child, attaching ref and handlers. Rendering<button>directly ensures zero intermediate DOM layers.
Caret Rotation Animation (ButtonCaret)
When a Radix dropdown opens, it injects data-state="open" onto the trigger button. <ButtonCaret size={size} /> includes the .rotate-toggle-180 class, which smoothly rotates the icon 180 degrees using CSS transitions.
Required Marker Classes (dropdown-toggle, no-arrow, btn-icon)
Toggles retain three specific utility classes:
dropdown-toggle: Required for CSS caret rotation and for read-only view hiding rules (table .dropdown-toggle { display: none !important }).no-arrow: Suppresses Bootstrap's legacy CSS::aftercaret pseudo-element, since toggles render an explicit PhosphorCaretDownIcon.btn-icon: An inert marker used by read-only view stylesheets to hide icon-only toggles in panels and cards (.dropdown-toggle.btn-icon { display: none }).
Centralized Icon Sizing in Toggles
Toggle buttons and standalone icons use the centralized sizing helper getButtonIconSize(size) and the <ButtonCaret size={size} /> component from waldur-ui:
16px(BUTTON_ICON_SIZES.sm) forsize="sm"20px(BUTTON_ICON_SIZES.lg) forsize="md"/size="lg"This preserves exact alignment with standardBaseButtonicons while inheriting the button'scurrentColor.
7. Segmented Controls & View Switchers
When an interface presents mutually exclusive options that change what a view shows (filtering a table, toggling between card/list mode, switching chart date ranges) rather than navigating to a different page, use SegmentedControl (packages/ui/src/SegmentedControl.tsx).
SegmentedControl vs. BaseButton
1 2 3 4 5 6 7 8 9 10 11 12 | |
- Do NOT use a row of separate
BaseButtoncomponents for view switching. - Do NOT use legacy
react-bootstrapToggleButtonGroupor.btn-groupclasses.
Keyboard & Focus Model (Radix RadioGroup)
SegmentedControl is built on @radix-ui/react-radio-group:
- Items have
role="radio", and the container hasrole="radiogroup". - Single Tab Stop: Tab enters the group directly at the currently selected option and leaves in one step.
- Arrow Navigation: Arrow keys (← / →) move focus and automatically select the target option.
- Numeric options round-trip cleanly through Radix strings back to their typed numbers.
Variants: neutral vs. brand
| Variant | Idle Segments | Selected Segment | Typical Context |
|---|---|---|---|
neutral (default) |
Solid white (--btn-tertiary-bg) |
Tertiary pressed gray (--btn-tertiary-bg-pressed) |
Secondary view switchers in toolbars and table headers |
brand |
Light brand tint | Primary brand fill (--btn-primary-bg) with white text |
Primary page-level switchers (e.g. reporting period, chart mode) |
Flexbox Alignment Safeguard (self-center)
In segmentedStyles.ts, the control list declares:
1 | |
Under flexbox's default align-items: stretch, a control placed in a flex row alongside taller siblings (e.g. an input or search field) would stretch vertically, corrupting its intended button height. self-center guarantees the control strictly retains its 28px/36px/44px height regardless of surrounding elements.
Shared Styling with Real Tab Panels (SigninForm)
When options own full tab panels, use Radix Tabs rather than SegmentedControl. However, import segmentedListClassName and segmentedItemClassName from waldur-ui to style the Tabs.List and Tabs.Trigger elements so they visually match SegmentedControl identically.
8. Convenience & Specialized Wrappers
Waldur provides specialized button wrappers for standard application patterns. All of them internally render BaseButton:
SubmitButton
Location: src/form/SubmitButton.tsx
Used for submitting forms.
- Defaults to large size (
size="lg", 44px). Passsize="sm"for compact popovers or inline forms. - Pending state (
submitting): Shows<LoadingSpinner />and disables the button. - Form invalidation (
invalid): Disables the button when form validation fails. iconNode&iconOnLeft: Defaults to trailing icon; passiconOnLeft={true}for leading icons.- Associating with external forms: Supports
form="form-id"for submission buttons rendered in dialog footers outside the<form>DOM tree.
1 2 3 4 5 6 7 | |
CloseDialogButton
Location: src/modal/CloseDialogButton.tsx
Used as the standard dismiss/cancel button in modal dialog footers.
- Defaults to
variant="tertiary"andsize="lg"(44px). - Automatically connects to
useModal().closeDialog(). - Default label is
translate('Cancel').
1 2 3 4 5 6 7 | |
CompactEditButton
Location: src/form/CompactEditButton.tsx
Used for inline editing in key-value tables and settings rows.
- Fixed
size="sm"(28px). - Renders
PencilSimpleIconwithvariant="tertiary". - Prevents layout bloat in tight table cells.
1 2 3 | |
SaveButton
Location: src/core/SaveButton.tsx
Used in forms that track dirty/unsaved state.
- Automatically switches to
variant="warning"and shows an unsaved notification badge whendirty={true}. - Wraps in a tooltip explaining unsaved changes.
9. Component Decision Matrix
| Scenario / Use Case | Component | Variant | Size | Notes |
|---|---|---|---|---|
| Primary Page Action | BaseButton |
primary |
md (36px) or lg (44px) |
Top-right page header actions |
| Modal Submission | SubmitButton |
primary |
lg (44px) |
Rightmost button in modal footer |
| Modal Cancel / Dismiss | CloseDialogButton |
tertiary |
lg (44px) |
Leftmost button in modal footer |
| Destructive Action | BaseButton |
danger |
Contextual | Use disabledReason if deletion blocked |
| Table Row Action (Text) | BaseButton |
tertiary or text-secondary |
sm (28px) |
Fits within 36px table row height |
| Table Row Action (Icon) | BaseButton |
tertiary or text-secondary |
sm (28px) |
Provide tooltip |
| Table Actions Dropdown | ActionsDropdown |
tertiary |
lg (44px) |
Uses TableDropdownToggle trigger |
| Table Toolbar Filter/Refresh | BaseButton |
tertiary |
lg (44px) |
Standard table control height |
| Inline Form / Popover Submit | SubmitButton |
primary |
sm (28px) |
Compact form contexts |
| Key-Value Row Edit | CompactEditButton |
tertiary |
sm (28px) |
Inline field editing |
| Page / State Navigation | Link |
buttonVariant="primary" |
md (36px) |
Keeps routing anchor semantics |
| View / Mode Switcher | SegmentedControl |
neutral or brand |
sm (28px) or md (36px) |
Mutually exclusive view options |
10. Linting Rules & Prohibited Anti-Patterns
ESLint Rules Matrix
| ESLint Rule | Severity | What It Enforces |
|---|---|---|
no-restricted-imports |
error |
Blocks importing Button or DropdownButton from react-bootstrap. Directs developers to BaseButton, SubmitButton, CloseDialogButton, or ActionDropdownButton. |
waldur-custom/no-bootstrap-button-markup |
error |
Blocks <button className="btn ...">, <a className="btn ...">, and any hand-rolled Bootstrap button markup. |
waldur-custom/enforce-disabled-button-tooltip |
error |
Enforces that every disabled <BaseButton> has a tooltip or disabledReason explaining why the action is unavailable. |
waldur-custom/enforce-dialog-button-order |
error |
Enforces standard dialog button order: dismissive buttons (CloseDialogButton) on the left, affirmative/submission buttons (SubmitButton) on the right. |
waldur-custom/no-edit-button-size-override |
error |
Blocks overriding size="sm" on EditButton; requires using CompactEditButton instead. |
Converting Legacy Call Sites
| From | To |
|---|---|
react-bootstrap Button, <button className="btn btn-x btn-sm"> |
<BaseButton variant="…" size="sm" label={…} /> |
<a className="btn …"> / Link with btn classes |
<Link buttonVariant="…" buttonSize="…"> |
ToggleButtonGroup, btn-group + btn-check |
SegmentedControl |
ButtonGroup of unrelated actions |
<div className="d-flex gap-2"> of BaseButtons |
Radix Trigger asChild around a button |
BaseButton directly (it is forwardRef); use a raw element + buttonVariants() only when the trigger's child must have a specific DOM shape (see ActionsDropdown) |
| HTML built as a string (chart tooltips) | class="${buttonVariants({ variant, size })}" |
disabled with no explanation |
Add disabledReason |
.svg-icon / font icon inside the button |
iconNode={<PhosphorIcon weight="bold" />} |
Rules of thumb when converting:
- Do not carry
btn-*classes across: Legacy Bootstrap/Metronic CSS rules were compound with.btn(.btn.btn-sm,.btn.btn-icon), so carryingbtn-sm,btn-icon,btn-active-icon-dangeror similar on an element without the literal.btnclass never matched anything. Pick first-classvariantandsizeprops instead. - Verify replacement in place: Old rules often had container-level minimum heights or widths that stopped applying when converted to
BaseButton. If a converted button looks smaller than before, setsizeexplicitly (sm,md,lg).
Good vs. Bad Code Comparison
1. Button Imports & Wrappers
1 2 3 4 5 6 7 8 9 10 | |
2. Disabled State Explanations
1 2 3 4 5 6 7 8 9 | |
3. Icons in Buttons
1 2 3 4 5 6 7 8 9 10 11 | |
4. Links as Buttons
1 2 3 4 5 6 7 8 9 | |
5. Mutually Exclusive Switchers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |