Skip to main content

API Patterns

Vortex components share the same prop patterns. Learn them once and they apply everywhere.

Class names and attributes

Most components accept two props for their root element:

  • className takes a string or an array. Falsy entries are skipped.
  • attributes takes any HTML attribute, including data-*, aria-*, and ref. Keeping them separate prevents clashes with component props.
<Button
  className={["checkout", isPending && "checkout--pending"]}
  attributes={{ "data-testid": "checkout", "aria-describedby": "order-total" }}
>
  Place order
</Button>

Checkbox, Radio, Switch, Slider, and FileUpload also accept inputAttributes, applied to the native <input>:

<Checkbox name="terms" inputAttributes={{ required: true }}>I accept the terms</Checkbox>

Compose class names

Native elements need a string. classNames drops falsy values and joins the rest:

import { classNames } from "@vortexlabs/vortex";

<button className={classNames("checkout", isPending && "checkout--pending")}>Place order</button>
// isPending true:  "checkout checkout--pending"
// isPending false: "checkout"

Use it in your own components to merge their classes with a passed-in className:

import { classNames, type VortexGlobalProps } from "@vortexlabs/vortex";

type Props = {
  isPending: boolean;
  className?: VortexGlobalProps.ClassName;
};

const CheckoutButton = (props: Props) => {
  const { isPending, className } = props;
  const rootClassNames = classNames("checkout", isPending && "checkout--pending", className);

  return <button className={rootClassNames}>Place order</button>;
};

Responsive props

Props typed as Responsive<T> accept a breakpoint object. Values are mobile-first: each breakpoint applies upward until a larger one overrides it.

<Stack direction={{ xs: "column", md: "row" }} gap={4} />
BreakpointApplies from
xs0px
sm640px
md768px
lg1024px
xl1440px

Use the same breakpoints in your own CSS with @custom-media queries:

@media (--vortex-breakpoints-lg) {
  .sidebar {
    display: block;
  }
}

To read the active breakpoint in JavaScript, use useBreakpoints. To show or hide content per breakpoint, use Visibility. To change the values, see Custom breakpoints.

Compound components

Multi-part components expose their parts as properties, such as Avatar.Group and Avatar.Item. Each part accepts its own className and attributes.

<Avatar.Group ariaLabel="Reviewers">
  <Avatar.Item initials="AL" />
  <Avatar.Item initials="MK" />
</Avatar.Group>

State and change handlers

Form inputs such as TextField, Checkbox, and Switch call onChange with one object instead of a native event: name, the new value or checked, and the original event when there is one.

<TextField name="email" onChange={({ name, value }) => setField(name, value)} />

Stateful components work either way. Pass a default prop, such as defaultChecked, to let the component manage its state, or the matching prop, such as checked, to control it.

// Uncontrolled
<Switch name="notifications" defaultChecked>Notifications</Switch>

// Controlled
<Switch name="notifications" checked={enabled} onChange={({ checked }) => setEnabled(checked)}>
  Notifications
</Switch>

Root element

Card, Container, Grid, Stack, Text, Avatar, and Visibility accept as to change the rendered element:

<Card as="section">…</Card>

Button, Link, and Interactive choose their element from their props: href renders an <a>, onClick renders a <button>.

Right-to-left

Set dir="rtl" on <html> or any wrapper. Components inside mirror their layout, animations, popover positions, and arrow-key navigation.

<html dir="rtl">

// Or only part of the page
<section dir="rtl">…</section>

Icons you pass in don't flip. Swap directional icons with useRTL:

const [rtl] = useRTL();

<Button endIcon={rtl ? ArrowLeft : ArrowRight}>Next</Button>

In your own CSS, use logical properties such as margin-inline-start and inset-inline-end so it mirrors too.