Skip to content

Component Authoring Guide

Cindy Zhang edited this page Aug 26, 2026 · 8 revisions

Component Authoring Guide

Building a new component end-to-end? See Component Lifecycle for the full lifecycle. Start implementation and architecture review with the Architecture Cheat Sheet, then use this page for the detailed conventions.

The practical reference for building new Astryx components. Covers file structure, StyleX patterns, token usage, and conventions.


File Structure

Every component lives in its own directory under /packages/core/src/:

/packages/core/src/Button/
├── Button.tsx                 # Main component implementation
├── index.ts                   # Exports
├── Button.doc.mjs             # Typed documentation (JSDoc + ComponentDoc type)
└── Button.test.tsx            # Tests

Stories are not colocated. They live in the Storybook app, one file per component:

/apps/storybook/stories/Button.stories.tsx

The directory, the file, and the exported component all share the component's name — no prefix:

Component Directory File Export
Button Button/ Button.tsx Button
Text Input TextInput/ TextInput.tsx TextInput
Stack Stack/ Stack.tsx Stack

Component Documentation ({Name}.doc.mjs)

Every component directory has a {Name}.doc.mjs file that exports typed documentation. This replaces the old README.md approach — docs are now structured data that the CLI imports directly (no markdown parsing).

Structure

/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
export const docs = {
  name: 'Button',
  description: 'Primary interactive element for user actions.',
  
  features: [
    "Variants: 'primary', 'secondary', 'ghost', 'destructive'",
    'Sizes: sm (28px), md (32px), lg (36px)',
    'Loading state: Shows spinner, disables interaction',
  ],
  
  props: [
    {
      name: 'label',
      type: 'string',
      description: 'Accessible label; used as aria-label for icon-only buttons.',
      required: true,
    },
    {
      name: 'variant',
      type: "'primary' | 'secondary' | 'ghost' | 'destructive'",
      description: 'Visual style variant.',
      default: "'secondary'",
    },
    // ... more props
  ],
  
  examples: [
    {
      label: 'Basic',
      code: '<Button variant="primary">Save</Button>',
    },
    {
      label: 'With icon',
      code: '<Button icon={PlusIcon} variant="secondary">Add</Button>',
    },
  ],
  
  theming: {
    targets: [
      {className: 'astryx-button', visualProps: ['variant', 'size']},
    ],
    vars: [
      {name: '--button-radius', description: 'Border radius', default: 'var(--radius-element)'},
    ],
  },
  
  accessibility: [
    'Uses native <button> element for correct ARIA semantics.',
    'Icon-only buttons use label prop as aria-label.',
  ],
  
  keyboard: 'Enter/Space activates the button; Tab/Shift+Tab moves focus',
  
  notes: [
    'Hover states use backgroundImage overlay pattern for consistent layering.',
  ],
};

Type Checking

The ComponentDoc type comes from @astryxdesign/cli/authoring — import it by package name, not by a relative path. Run type checking with:

pnpm --filter @astryxdesign/core typecheck:docs

This uses tsc --checkJs to validate all .doc.mjs files against the ComponentDoc type. CI runs this automatically.

Single vs Multi-Component Directories

  • Single component (Button, Switch, Badge): Use props directly on the doc object
  • Multi-component (Table, Dialog, Layer): Use components array with separate entries for each exported component
// Multi-component example (Table)
export const docs = {
  name: 'Table',
  description: 'Data table with rich cell content via renderCell.',
  examples: [/* top-level composition examples */],
  components: [
    {
      name: 'Table',
      description: 'Main table component with data-driven rows.',
      props: [/* Table props */],
      examples: [/* Table-specific examples */],
    },
    {
      name: 'TableRow',
      description: 'Individual row within BaseTable.',
      props: [/* TableRow props */],
      examples: [/* TableRow-specific examples */],
    },
    // ... more components
  ],
};

File Header Convention

Every component file starts with a JSDoc block describing its role:

/**
 * Button — Primary interactive element for user actions.
 *
 * @input variant, size, disabled, loading, children
 * @output Styled <button> element with theme-aware variants
 * @position Inline within forms, toolbars, cards, dialogs
 *
 * SYNC: When modified, update this header and Button.doc.mjs.
 */
Tag Purpose
@input Props the component accepts
@output What the component renders
@position Where the component is typically used in a layout
SYNC Notes about dependencies or coordination with other parts of the system

'use client' Directive

All component files that use React hooks must include 'use client'. See RSC Compatibility for the full decision.

Astryx components consume theme via useContext, making them client components. The directive goes after any file-level JSDoc, before imports:

/**
 * @file MyComponent.tsx
 */

'use client';

import {useContext} from 'react';

Rule of thumb: If your file imports anything from 'react' other than types, it needs 'use client'. A component that only accepts ref as a prop imports nothing at runtime.


Basic Component Template

'use client';

import type {HTMLAttributes, ReactNode, Ref} from 'react';
import * as stylex from '@stylexjs/stylex';
import {colorVars} from '../theme/tokens.stylex';

const styles = stylex.create({
  base: {
    // Base styles using tokens
    fontFamily: 'inherit',
    borderWidth: 0,
    cursor: 'pointer',
  },
});

const variants = stylex.create({
  default: {
    backgroundColor: colorVars['--color-background-surface'],
    color: colorVars['--color-text-primary'],
  },
  primary: {
    backgroundColor: colorVars['--color-accent'],
    color: colorVars['--color-on-accent'],
  },
});

// Derive type from StyleX object
export type MyComponentVariant = keyof typeof variants;

export interface MyComponentProps
  extends Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'className'> {
  /** Ref forwarded to the root element. */
  ref?: Ref<HTMLDivElement>;
  variant?: MyComponentVariant;
  children: ReactNode;
}

export function MyComponent({variant = 'default', ref, children, ...props}: MyComponentProps) {
  return (
    <div ref={ref} {...stylex.props(styles.base, variants[variant])} {...props}>
      {children}
    </div>
  );
}

MyComponent.displayName = 'MyComponent';

Key elements:

  1. ref as a prop — React 19 passes ref like any other prop; declare it on the props interface and hand it to the root element. forwardRef is banned (@eslint-react/no-forward-ref)
  2. stylex.create — Styles are static objects, compiled at build time
  3. Token imports — Always use tokens, never hardcoded values
  4. displayName — Set explicitly for React DevTools

Type Derivation from StyleX Objects

Derive variant types directly from the StyleX object so types stay in sync with styles automatically:

const variants = stylex.create({
  primary: { /* ... */ },
  secondary: { /* ... */ },
  ghost: { /* ... */ },
});

export type ButtonVariant = keyof typeof variants;
// Results in: 'primary' | 'secondary' | 'ghost'

When you add or remove a variant from the StyleX object, the type updates automatically. No manual type maintenance needed.

The same pattern works for sizes or any other variant dimension:

const sizes = stylex.create({
  sm: {height: sizeVars['--size-element-sm']},
  md: {height: sizeVars['--size-element-md']},
  lg: {height: sizeVars['--size-element-lg']},
});

export type ButtonSize = keyof typeof sizes;
// Results in: 'sm' | 'md' | 'lg'

Token Usage

Always use tokens from tokens.stylex instead of hardcoded values:

Token objects are StyleX defineVars maps, so they are indexed by the CSS custom property name — colorVars['--color-accent'], not colorVars['--color-accent']:

import {
  colorVars,
  spacingVars,
  radiusVars,
  durationVars,
  easeVars,
  typographyVars,
  shadowVars,
} from '../theme/tokens.stylex';

const styles = stylex.create({
  base: {
    backgroundColor: colorVars['--color-background-surface'],
    padding: spacingVars['--spacing-3'],
    borderRadius: radiusVars['--radius-element'],
    fontFamily: typographyVars['--font-family-body'],
    transitionDuration: durationVars['--duration-fast'],
    transitionTimingFunction: easeVars['--ease-standard'],
    boxShadow: shadowVars['--shadow-low'],
  },
});

Token Reference

Category Tokens Examples
colorVars Semantic colors, text, icons, status, overlays, borders --color-accent, --color-background-surface, --color-text-primary, --color-text-secondary, --color-neutral, --color-overlay-hover, --color-overlay-pressed, --color-border, --color-error, --color-success
spacingVars Consistent spacing scale --spacing-0 (0px), --spacing-0-5 (2px), --spacing-1 (4px), --spacing-2 (8px), --spacing-3 (12px) … --spacing-12 (48px)
sizeVars Fixed control heights --size-element-sm (28px), --size-element-md (32px), --size-element-lg (36px)
radiusVars Border radius by role --radius-none, --radius-inner (4px), --radius-element (8px), --radius-container (12px), --radius-page (28px), --radius-full
shadowVars Box shadows --shadow-low, --shadow-med, --shadow-high, --shadow-inset-hover, --shadow-inset-selected
durationVars Motion durations --duration-fast (175ms), --duration-medium (410ms), --duration-slow (975ms) — each with a -min and -max variant
easeVars Easing curves --ease-standard
typographyVars Font families --font-family-body, --font-family-code, --font-family-heading
typeScaleVars Semantic type roles — prefer these over raw sizes --text-body-size, --text-label-size, --text-supporting-size, --text-large-size (each with -weight and -leading)
textSizeVars Raw geometric size scale --font-size-2xs--font-size-base (14px) … --font-size-5xl

The xstyle Prop Pattern

For components that need consumer-provided styles, use the xstyle prop. This ensures consumers use StyleX (maintaining compile-time optimization) instead of inline styles.

Implementation

import type {StyleXStyles} from '@stylexjs/stylex';

export interface MyComponentProps
  extends Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'className'> {
  /** StyleX styles to apply to the component. */
  xstyle?: StyleXStyles;
  children?: ReactNode;
}

export function MyComponent({xstyle, ref, children, ...props}: MyComponentProps) {
  return (
    <div ref={ref} {...stylex.props(styles.base, xstyle)} {...props}>
      {children}
    </div>
  );
}

Key Points

  1. Omit style and className — Use Omit<HTMLAttributes<HTMLElement>, 'style' | 'className'> to prevent inline styles and ensure StyleX usage.

  2. Merge order matters — Pass xstyle as the last argument to stylex.props() so consumer styles override component defaults.

  3. Constrained vs. freeform:

    • Higher-level components (Button, Card): Prefer constrained APIs with specific props (variant, size) over open xstyle. Enforces design consistency.
    • Primitive/layout components (Stack, Box): Allow freeform xstyle since these are building blocks that need flexibility.

Consumer Usage

import * as stylex from '@stylexjs/stylex';
import {colorVars, radiusVars} from '@astryxdesign/core';

const customStyles = stylex.create({
  highlight: {
    backgroundColor: colorVars['--color-background-muted'],
    borderRadius: radiusVars['--radius-element'],
  },
});

// Single style
<HStack gap="space2" xstyle={customStyles.highlight}>
  <Item />
</HStack>

// Multiple styles via array
<VStack xstyle={[styles.container, styles.padded]}>
  <Content />
</VStack>

Conditional Styles

Apply styles conditionally using stylex.props:

{...stylex.props(
  styles.base,
  variants[variant],
  sizes[size],
  isDisabled && styles.disabled,
  isLoading && styles.loading,
  xstyle,  // Consumer override last
)}

StyleX merges these left-to-right. Later styles override earlier ones for the same property. false/undefined values are safely ignored.


Pseudo-Selectors

StyleX supports pseudo-selectors via nested objects with default for the base state:

const styles = stylex.create({
  interactive: {
    backgroundColor: {
      default: colorVars['--color-background-surface'],
      ':hover': colorVars['--color-overlay-hover'],
      ':active': colorVars['--color-overlay-pressed'],
    },
    outline: {
      default: 'none',
      ':focus-visible': `2px solid ${colorVars['--color-accent']}`,
    },
    outlineOffset: {
      default: null,
      ':focus-visible': '3px',
    },
  },
});

Hover Guards for Touch Devices

All :hover styles MUST use @media (hover: hover) guards to prevent "sticky hover" on mobile/touch devices:

const styles = stylex.create({
  interactive: {
    backgroundColor: {
      default: null,
      ':hover': {
        '@media (hover: hover)': colorVars['--color-overlay-hover'],
      },
      ':active': colorVars['--color-overlay-pressed'],  // No guard needed for :active
    },
  },
});
  • :hover — Always wrap in @media (hover: hover)
  • :active — No guard needed (press feedback is good on touch)
  • :focus-visible — No guard needed (keyboard focus must always work)

Overlay Hover/Active Pattern

For interactive elements where hover/active colors should layer on top of the base background (not replace it), use backgroundImage:

const variants = stylex.create({
  primary: {
    backgroundColor: colorVars['--color-accent'],
    backgroundImage: {
      default: null,
      ':hover': {
        '@media (hover: hover)': `linear-gradient(${colorVars['--color-overlay-hover']}, ${colorVars['--color-overlay-hover']})`,
      },
      ':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
    },
  },
});

Why this works: CSS background-image renders on top of background-color. By using a solid-color linear-gradient as the overlay, you get a semi-transparent tint over whatever the base color is — without needing a pseudo-element.

Why not ::after? StyleX doesn't support combined pseudo-selectors like :hover::after, so this backgroundImage trick is the standard Astryx pattern.

Variant-Specific Focus Colors

Some components need different focus outline colors per variant:

const variants = stylex.create({
  primary: {
    outline: {
      default: null,
      ':focus-visible': `2px solid ${colorVars['--color-accent']}`,
    },
    outlineOffset: {
      default: null,
      ':focus-visible': '3px',
    },
  },
  destructive: {
    outline: {
      default: null,
      ':focus-visible': `2px solid ${colorVars['--color-error']}`,
    },
    outlineOffset: {
      default: null,
      ':focus-visible': '3px',
    },
  },
});

Loading State Pattern

Pattern for components with loading indicators:

const loadingStyles = stylex.create({
  loading: {
    position: 'relative',
    color: 'transparent', // Hide text while loading
  },
  spinnerContainer: {
    position: 'absolute',
    inset: 0,
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  spinner: {
    animationName: stylex.keyframes({
      to: {transform: 'rotate(360deg)'},
    }),
    animationDuration: durationVars['--duration-slow-min'],
    animationTimingFunction: 'linear',
    animationIterationCount: 'infinite',
  },
});

// In component JSX:
<button
  ref={ref}
  {...stylex.props(
    styles.base,
    variants[variant],
    loading && loadingStyles.loading,
  )}
  disabled={disabled || loading}
  {...props}>
  {children}
  {loading && (
    <span {...stylex.props(loadingStyles.spinnerContainer)}>
      <Spinner {...stylex.props(loadingStyles.spinner)} />
    </span>
  )}
</button>

The key trick: set color: 'transparent' on the loading state to hide the text content while keeping the button's dimensions stable, then absolutely position the spinner on top.


Animations

Use stylex.keyframes for animations. Define them inline within stylex.create:

const styles = stylex.create({
  spinner: {
    animationName: stylex.keyframes({
      to: {transform: 'rotate(360deg)'},
    }),
    animationDuration: durationVars['--duration-slow-min'],
    animationTimingFunction: 'linear',
    animationIterationCount: 'infinite',
  },
  fadeIn: {
    animationName: stylex.keyframes({
      from: {opacity: 0},
      to: {opacity: 1},
    }),
    animationDuration: durationVars['--duration-medium'],
    animationTimingFunction: easeVars['--ease-standard'],
  },
  slideDown: {
    animationName: stylex.keyframes({
      from: {
        opacity: 0,
        transform: 'translateY(-8px)',
      },
      to: {
        opacity: 1,
        transform: 'translateY(0)',
      },
    }),
    animationDuration: durationVars['--duration-medium'],
    animationTimingFunction: easeVars['--ease-standard'],
  },
});

Use transition tokens (durationVars['--duration-fast'], durationVars['--duration-medium']) for durations to stay consistent with the theme.


Theme Integration

Container Padding (Bleed Components)

If your component needs to escape container padding (like Table or Divider), read the matching edge from --container-padding-inline-start, --container-padding-inline-end, --container-padding-block-start, and --container-padding-block-end, then apply the corresponding negative margin. There is no isotropic --container-padding, --container-padding-inline, or --container-padding-block. If your component creates a new container context, reset all four for descendants. See Container Padding System for the full reference and patterns.

themeProps — Theme Targeting

Every component must apply themeProps so theme authors can target it via defineTheme component overrides. This is the primary mechanism for per-component theming.

import {themeProps, mergeProps} from '../utils';

// On the element with visual styles (background, shadow, radius)
<div
  {...mergeProps(
    themeProps('card'),
    stylex.props(styles.cardOuter),
  )}>

Key rules:

  • themeProps goes on the element with the visual styles — not necessarily the root element. For layer-based components (Tooltip, HoverCard, Popover), it goes on the visual container, not the positioning wrapper.
  • Pass variant props for variant-specific targeting: themeProps('button', {variant, size})
  • Use mergeProps to combine themeProps with stylex.props
  • Do NOT add themeProps to composition wrappers that just wrap other themed Astryx components — it creates specificity conflicts

Sub-Element Targeting

Visually distinct sub-elements within a component need their own themeProps so theme authors can target them independently.

Add a target when the sub-element:

  • Has its own color, background, or border distinct from the parent
  • Cannot be styled via a CSS descendant selector from the root

Do NOT add a target when the sub-element:

  • Is structural only (wrapper divs for layout)
  • Is text content that inherits from the parent
  • Has appearance fully controlled by global tokens
// Switch: track gets root themeProps, thumb gets sub-element target
<div {...mergeProps(themeProps('switch'), stylex.props(styles.track))}>
  <div {...mergeProps(themeProps('switch-thumb'), stylex.props(styles.thumb))} />
</div>

Scale guideline: Most components need 0-1 sub-element targets. Compound components (Layout, Table) may need 3-5. If a component needs more than 5, consider decomposing it.

Component CSS Vars (When Needed)

Prefer themeProps targeting for most theming. Only expose a CSS custom property when:

  • The value participates in a calc() expression (e.g. concentric radius)
  • Multiple sibling elements reference the same value
// Component var — only because items derive radius via calc()
const styles = stylex.create({
  menu: {
    '--dropdown-radius': radiusVars['--radius-2'],
    '--dropdown-padding': spacingVars['--spacing-1'],
    borderRadius: 'var(--dropdown-radius)',
  },
  item: {
    // Concentric radius — derived from container vars
    borderRadius: 'max(0px, calc(var(--dropdown-radius) - var(--dropdown-padding)))',
  },
});

Document component vars in the .doc.mjs file's theming.vars field:

theming: {
  targets: [{className: 'astryx-dropdown-menu'}],
  vars: [
    {name: '--dropdown-radius', description: 'Menu popup radius', default: 'var(--radius-2)'},
    {name: '--dropdown-padding', description: 'Menu popup padding', default: 'var(--spacing-1)'},
  ],
},

Extensible Variant Types

Components with variant props use an interface registry pattern so theme packages can add custom variants via module augmentation:

// Define variants as an interface (not a union type)
export interface ButtonVariantMap {
  primary: true;
  secondary: true;
  ghost: true;
  destructive: true;
}
export type ButtonVariant = keyof ButtonVariantMap;

Theme packages can then extend the map:

// In @astryxdesign/theme-meta/types.ts
declare module '@astryxdesign/core/Button' {
  interface ButtonVariantMap {
    'primary-muted': true;
    'primary-outline': true;
  }
}

Runtime behavior: Unknown variants gracefully receive base styles only. StyleX's styles.variants[variant] returns undefined for unrecognized keys, which StyleX ignores. The theme provides the visual definition through component overrides:

defineTheme({
  components: {
    button: {
      'variant:primary-muted': { backgroundColor: '...' },
    },
  },
});

Render data-variant={variant} on the element with themeProps so theme CSS can target custom variants.

Consuming Theme Overrides

Components do not read overrides in JavaScript. Theming happens in CSS: themeProps() puts a stable class and a data-* reflection on the styled element, and the theme's components entry targets that class. Merge it ahead of stylex.props() with mergeProps so both class names survive:

import {themeProps, mergeProps} from '../utils';

export function Button({variant = 'primary', size = 'md', ref, children, ...props}: ButtonProps) {
  return (
    <button
      ref={ref}
      {...mergeProps(
        themeProps('button', {variant, size}),
        stylex.props(styles.base, variants[variant], sizes[size]),
      )}
      {...props}>
      {children}
    </button>
  );
}

themeProps('button', {variant: 'primary', size: 'sm'}) emits {className: 'astryx-button primary sm', 'data-variant': 'primary', 'data-size': 'sm'}. Every prop that selects a style object must be passed to themeProps — otherwise a theme cannot target that variation. See Theming Infrastructure.


Known StyleX Limitations

  1. No runtime stylex.create() — All styles must be compiled at build time via the Babel plugin. You cannot dynamically create styles at runtime (e.g., in Storybook's preview.tsx or based on runtime values).

  2. Combined pseudo-selectors don't work:hover::after is not supported. Use the backgroundImage overlay pattern instead of ::after pseudo-elements for hover/active effects.

  3. No stylex.create in non-compiled files — The consuming app must handle the build for proper style deduping, merging, and bundling. Library code must be compiled by the consumer's build pipeline.

  4. Shorthand property limitations — Some CSS shorthands behave differently. Prefer longhand properties (e.g., paddingTop, paddingRight instead of padding) when you need per-side control.


Complete Example: Button

Putting it all together — here's how the patterns combine in a real component:

/**
 * Button — Primary interactive element for user actions.
 *
 * @input variant, size, disabled, loading, children
 * @output Styled <button> element with theme-aware variants
 * @position Inline within forms, toolbars, cards, dialogs
 *
 * SYNC: When modified, update this header and Button.doc.mjs.
 */

import type {ButtonHTMLAttributes, ReactNode, Ref} from 'react';
import * as stylex from '@stylexjs/stylex';
import {colorVars, spacingVars, radiusVars, durationVars, easeVars, sizeVars} from '../theme/tokens.stylex';
import {themeProps, mergeProps} from '../utils';
import type {ButtonVariantMap} from './index';

const styles = stylex.create({
  base: {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: spacingVars['--spacing-2'],
    borderWidth: 0,
    borderRadius: radiusVars['--radius-element'],
    cursor: 'pointer',
    fontFamily: 'inherit',
    transitionDuration: durationVars['--duration-fast'],
    transitionTimingFunction: easeVars['--ease-standard'],
    transitionProperty: 'background-color, transform, outline',
    transform: {
      default: null,
      ':active': 'scale(0.98)',
    },
  },
  disabled: {
    cursor: 'not-allowed',
    opacity: 0.5,
  },
});

const variants = stylex.create({
  primary: {
    backgroundColor: colorVars['--color-accent'],
    color: 'white',
    backgroundImage: {
      default: null,
      ':hover': {
        '@media (hover: hover)': `linear-gradient(${colorVars['--color-overlay-hover']}, ${colorVars['--color-overlay-hover']})`,
      },
      ':active': `linear-gradient(${colorVars['--color-overlay-pressed']}, ${colorVars['--color-overlay-pressed']})`,
    },
    outline: {
      default: null,
      ':focus-visible': `2px solid ${colorVars['--color-accent']}`,
    },
    outlineOffset: {
      default: null,
      ':focus-visible': '3px',
    },
  },
  secondary: { /* ... */ },
  ghost: { /* ... */ },
  destructive: { /* ... */ },
});

const sizes = stylex.create({
  sm: {height: sizeVars['--size-element-sm']},
  md: {height: sizeVars['--size-element-md']},
  lg: {height: sizeVars['--size-element-lg']},
});

export type ButtonVariant = keyof ButtonVariantMap;
export type ButtonSize = keyof typeof sizes;

export interface ButtonProps
  extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style' | 'className'> {
  /** Ref forwarded to the root element. */
  ref?: Ref<HTMLButtonElement>;
  variant?: ButtonVariant;
  size?: ButtonSize;
  loading?: boolean;
  children: ReactNode;
}

export function Button({variant = 'primary', size = 'md', loading, disabled, ref, children, ...props}: ButtonProps) {
  return (
    <button
      ref={ref}
      disabled={disabled || loading}
      aria-busy={loading || undefined}
      {...mergeProps(
        themeProps('button', {variant, size}),
        stylex.props(
          styles.base,
          variants[variant],
          sizes[size],
          (disabled || loading) && styles.disabled,
        ),
      )}
      {...props}>
      {children}
    </button>
  );
}

Button.displayName = 'Button';

See /packages/core/src/Button/Button.tsx for the full production implementation.

Clone this wiki locally