CtrlK
BlogDocsLog inGet started
Tessl Logo

bestax-custom-component

Build a custom React component in the bestax/Bulma style. In an app using @allxsmith/bestax-bulma — compose existing components, helper props, public hooks (useBulmaClasses, usePrefixedClassNames), and --bulma-* CSS variables. In the bestax monorepo — the full component pipeline (SCSS partial, stories, tests, docs, wiring). Use when creating a component beyond stock Bulma or extending one.

69

Quality

87%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Building a custom component the bestax way

This skill teaches how to build a component that isn't in the library — composed from bestax pieces in an app, or as a full library "extra" inside the bestax monorepo.

Which context are you in?

  • The bestax monorepo (the repo contains bulma-ui/src/) → follow references/library-contributor.md instead of this file: five-file layout, SCSS partial, stories, jest tests, docs page, wiring.
  • An app depending on @allxsmith/bestax-bulma (e.g. scaffolded by npm create bestax) → continue here. Everything below assumes public package imports and a plain Vite app.

For form components (Field/Control/Input/etc.) use the bestax-form skill instead.

Check for an existing component first

Before building anything, search the library for a component that already does this — or a close synonym — and tell the user what you found. Many requests are covered by an existing element, or are best built by composing existing ones. Building a near-duplicate (a "label" when Tag exists, a "banner" when Notification exists) fragments the API and is usually the wrong call.

Where to look:

  • references/component-catalog.md — start here. Every documented component with a one-line purpose, grouped by category. Scan it for the name and its synonyms before anything else.
  • https://bestax.io/docs/api — one doc page per shipped component (full props).

Then decide, and surface the decision to the user:

  • Exact / synonym match exists → recommend using it. Don't build a duplicate. (E.g. a small colored label/badge/chip → Tag / Tags already exist.)
  • Partial overlap → prefer composing the existing pieces inside your new component rather than re-implementing them. (E.g. a "profile card" → there's no ProfileCard, but Card, Image, Title, SubTitle, and Content exist; build ProfileCard to compose them.)
  • Genuine gap → build the new component using the pattern below.

State plainly which case applies before writing code, e.g. "Tag already covers a colored label — use that instead" or "No ProfileCard exists; I'll build one composing the existing Card/Image/Title elements."

Composition first

Build from existing components before writing any CSS: Box, Card, Title, SubTitle, Icon, Block, Content, Tag, plus the shared Bulma helper props (spacing, color, typography, flexbox). Some compound sub-parts are the exception — Modal.Card, Tabs.Tab, Message.Body take no Bulma helper props, just className + HTML attributes plus their own few (Tabs.Tab requires index={i} and has built-in disabled and icon/iconLibrary/iconVariant/iconSize/iconFeatures — don't nest an <Icon> there) — so put helper props on the parent or on an element inside them, never invent them there. (Card.* sub-parts do take helper props, like Table.*/Menu.*/Hero.*.) Most "custom components" are a composition function — zero new styles. See examples/stat-card.tsx for a complete worked example.

Rendering something only the browser knows (the reader's time zone, a localStorage value)? Wrap it in the library's ClientOnly rather than checking typeof window: it renders a fallback until the page has hydrated, so the server and client markup match. references/api.md has the details.

A panel opened from a button (a filter form, share options, an inline edit) is Popover: use it rather than building one. It anchors the panel, moves focus in and back, and closes on Escape and outside presses.

Building something else that floats (a command palette)? Render it through the library's Portal rather than createPortal: it renders nothing on the server and during hydration, so the server and client markup match. references/api.md has the details.

Building some other overlay that holds focus until it is dismissed (a command palette)? Use the library's useFocusTrap rather than a hand-rolled Tab handler: it finds the tab stops the browser visits and hands focus back on close. references/api.md has the signature and a worked panel.

The component spine

Same shape the library itself uses, with all imports from the package. Every reusable component gets it — including pure compositions with zero CSS (a heading block, a labeled wrapper): extend BulmaClassesProps, run your props through useBulmaClasses, merge its bulmaHelperClasses into className, and spread the rest it returns — spreading the raw props instead leaks helper props onto the DOM and emits none of their classes. The usePrefixedClassNames root class is needed only when component-scoped CSS (or a variant class) targets it — a zero-CSS composition may omit that call. File at src/components/MyComponent.tsx:

import type React from 'react';
import {
  classNames,
  usePrefixedClassNames,
  useBulmaClasses,
  type BulmaClassesProps,
} from '@allxsmith/bestax-bulma';

export interface MyComponentProps
  extends
    Omit<React.HTMLAttributes<HTMLDivElement>, 'color'>,
    Omit<BulmaClassesProps, 'color'> {
  color?: 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';
}

export function MyComponent({
  color,
  className,
  children,
  ...props
}: MyComponentProps) {
  const { bulmaHelperClasses, rest } = useBulmaClasses(props);
  const mainClasses = usePrefixedClassNames('mycomponent', {
    [`is-${color}`]: !!color,
  });
  return (
    <div
      className={classNames(mainClasses, bulmaHelperClasses, className)}
      {...rest}
    >
      {children}
    </div>
  );
}

This gives your component the full Bulma helper-prop surface (m, p, textAlign, …) for free. references/api.md documents the helpers.

Styling ladder — use the lowest rung that works

Rung 1 — helper props only (default). House rules: never style={{}}. Layout with Block/Box and display="flex", flexDirection, alignItems, justifyContent, and space the children with gap. Before writing style={{ … }} anywhere, translate each declaration:

Inline style you're about to writeHelper props instead
marginTop: '1rem' (any margin/padding)mt="4" — m/mt/mx/p/py/… scale: 1=0.25rem, 2=0.5rem, 3=0.75rem, 4=1rem, 5=1.5rem, 6=3rem (nearest step)
textAlign: 'center'textAlign="centered" (also left, right, justified)
color: '#…'textColor with the nearest Bulma color: primary, link, info, success, warning, danger, white, black, grey (+ grey-light, grey-dark, …)
backgroundColor: '#…'bgColor (same palette)
fontSize: …textSize="1"…"7" (1 largest) — for headings use Title/SubTitle size
fontWeight: …textWeight: light, normal, medium, semibold, bold
textTransform, italicstextTransform: uppercase, lowercase, capitalized, italic
display: 'flex' + flex propertiessame-named props: display="flex", flexDirection, justifyContent, alignItems, flexWrap
height: '100%' on a flex childflexGrow="1"
display: 'none'visibility="hidden", or responsive display* props (displayMobile, displayTablet, …)
gap: '1rem' (flex or grid)gap="2", or one axis: columnGap, rowGap; gap scale: 1=0.5rem, 2=1rem, 4=2rem, half steps ("1.5")

Spacing, typography, and flex helpers are on every component; textColor/bgColor are on the content components you'll compose with (Box, Block, Title, Content, Card, …) — the ones with a semantic color variant (Tag, Tabs, Panel) take color instead. Notification is the mixed case: it takes textColor, but its background comes from the semantic color prop, not bgColor.

A value with no helper equivalent (maxWidth: 720, a one-off gradient) moves you to rung 2 — a named class in a stylesheet — never to inline style.

Rung 2 — a plain CSS file, scoped under the component's class, consuming --bulma-* variables — never literal colors, so Theme and dark mode keep working:

/* src/components/MyComponent.css — import from the .tsx file */
.mycomponent {
  /* component-scoped custom props, initialized from Bulma tokens:
     any ancestor (or Theme) can re-theme by overriding them */
  --mycomponent-radius: var(--bulma-radius);
  --mycomponent-accent: var(--bulma-primary);
  border-radius: var(--mycomponent-radius);
  border: 1px solid var(--bulma-border);
  background: var(--bulma-scheme-main);
  color: var(--bulma-text);
}
.mycomponent .mycomponent-value {
  color: var(--mycomponent-accent);
}

Caveat: with the prefixed CSS flavor / ConfigProvider classPrefix, usePrefixedClassNames prefixes your classes too — your CSS selectors must match (or build them with plain classNames instead).

Rung 3 — real Sass (optional). npm i -D sass — nothing else; Vite compiles imported .scss zero-config, and bulma is resolvable because it's a runtime dependency of bestax-bulma. Then the full register-vars/getVar pattern from references/library-contributor.md works in-app. Prefixed flavor: @use 'bulma/sass/utilities/initial-variables' with ($class-prefix: 'bestax-').

Verify in the browser

Types don't see layout. Run npm run dev, render the component, and actually look at it: vertical centering of inline text (use display="flex" alignItems="center", not line-height hacks), balanced padding, nothing clipping, every color/size variant, and dark mode legibility. Fix what you see, then re-check. No browser available (headless)? Fall back to npm run build plus a Node renderToString smoke render, grep the emitted HTML for the expected classes, and flag the visual pass as not done.

Tests and stories in an app

The scaffolded app has no test runner and no Storybook — do not install or scaffold them unasked. If the app already has vitest/jest + Testing Library, write the four test shapes: render, prop→class mapping, helper-prop passthrough (m="3" → m-3), and the ConfigProvider classPrefix case if the app uses a prefix.

Checklist

  • Inventory checked (catalog + bestax.io/docs/api) and the decision surfaced to the user.
  • All imports from @allxsmith/bestax-bulma (no deep/internal paths).
  • Composition first — existing components + helper props before any CSS; every reusable component gets the spine.
  • No inline style={{}} anywhere — translate via the rung-1 mapping table; values with no helper equivalent get a named class (rung 2).
  • Lowest sufficient ladder rung (helper props → scoped CSS vars → Sass).
  • All colors/radii derived from --bulma-* variables — no literals.
  • Renders correctly via npm run dev, including dark mode.
Repository
allxsmith/bestax
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.