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:
classNametakes a string or an array. Falsy entries are skipped.attributestakes any HTML attribute, includingdata-*,aria-*, andref. 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} />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.