@carbonbits/sixr
v1.4.0
Published
e-commerce geared component library
Readme
Sixr
E-commerce geared React component library for carbonbits.
Ships ESM + CJS builds with type declarations, and a prebuilt stylesheet. Static
components are React Server Component safe and need no "use client" boundary in the
Next.js App Router; interactive ones carry their own directive, so either kind imports
cleanly into a server component.
Install
pnpm add @carbonbits/sixrreact and react-dom are peer dependencies (^19.0.0). zod (^4) is an optional
peer dependency, needed only for @carbonbits/sixr/validation.
Usage
Load the stylesheet once, at the root of your app:
// app/layout.tsx
import "@carbonbits/sixr/styles.css";Each component has its own subpath. Import from there:
// app/cart/page.tsx
import { Cart, type CartProduct } from "@carbonbits/sixr/cart";
const products: CartProduct[] = [
{ name: "Stovetop kettle", price: 1500 },
{ name: "Enamel mug", price: 500 },
];
export default function Page() {
return <Cart products={products} currency="KES" locale="en-KE" />;
}The root barrel (@carbonbits/sixr) re-exports everything and works too, but the
subpath is the one to reach for.
Components
Cart
import { Cart } from "@carbonbits/sixr/cart";| Prop | Type | Default | Description |
| ---------- | --------------- | --------- | --------------------------------------- |
| products | CartProduct[] | [] | Lines to render. Empty shows a placeholder. |
| currency | string | "KES" | ISO 4217 code used to format prices. |
| locale | string | "en-KE" | BCP 47 locale used to format prices. |
type CartProduct = {
/** Stable identity for the line, used as the React key. Falls back to `name`. */
id?: string;
name: string;
price: number;
};Pass an id whenever a cart can hold two lines with the same name. Prices are
formatted with Intl.NumberFormat, and the total is summed from price.
Typography
import { Typography } from "@carbonbits/sixr/typography";The type scale. Every piece of text the library renders goes through it, and it is the component to reach for when yours does too. Server-safe.
| Prop | Type | Default | Description |
| -------------- | ------------------------------------------------------------------------- | ------------------ | -------------------------------------------------------- |
| variant | "display" \| "heading" \| "subheading" \| "body" \| "label" \| "hint" \| "code" | "body" | Size, default element and default weight. |
| tone | "default" \| "muted" \| "subtle" \| "accent" \| "on-accent" \| "success" \| "warning" \| "info" \| "danger" | "default" | Colour role from the design tokens. |
| weight | "regular" \| "medium" \| "semibold" \| "bold" | per variant | Overrides the variant's weight. |
| align | "start" \| "center" \| "end" | — | Text alignment. |
| truncate | boolean | false | One line, clipped with an ellipsis. |
| icon | ReactNode | — | An icon beside the text, sized to it, aria-hidden. |
| iconPosition | "leading" \| "trailing" | "leading" | Which side the icon sits on. |
| as | "p" \| "span" \| "div" \| "h1"…"h6" \| "label" \| "code" \| … | per variant | The element to render. |
| asChild | boolean | false | Merge the styles onto the child element instead. |
Default elements: display → h1, heading → h2, subheading → h3, body and
hint → p, label → span, code → code. Any other HTML attribute passes through.
<Typography variant="heading">Welcome back</Typography>
<Typography tone="muted">Sign in to your Carbonbits workspace.</Typography>
<Typography variant="hint" tone="success" icon={<CheckIcon />}>
Verification code accepted
</Typography>
// Style your own element — a Next.js Link, a Radix Label — with `asChild`.
<Typography variant="label" tone="accent" asChild>
<Link href="/reset">Forgot your password?</Link>
</Typography>Inputs: TextInput, EmailInput, PasswordInput, PhoneInput
import { TextInput } from "@carbonbits/sixr/text-input";
import { EmailInput } from "@carbonbits/sixr/email-input";
import { PasswordInput } from "@carbonbits/sixr/password-input";
import { PhoneInput } from "@carbonbits/sixr/phone-input";Labelled single-line fields with optional schema validation. Each one is for a single kind of value and fixes the input type, keyboard and autofill hints that value needs:
| Component | Value | Fixes |
| --------------- | ---------------- | ---------------------------------------------------------------------- |
| TextInput | Free text | type="text". Adds mono for the monospace face (verification codes). |
| EmailInput | An email address | type="email", inputMode="email", autoComplete="email", no autocapitalise or spellcheck. |
| PasswordInput | A secret | type="password", autoComplete="current-password", and a built-in Show/Hide toggle. |
| PhoneInput | A phone number | type="tel", inputMode="tel", autoComplete="tel". |
Interactive, so each ships its own "use client" directive and is reachable only at
its subpath — none is on the root barrel, which stays server-safe.
They share these props:
| Prop | Type | Default | Description |
| -------------------- | ------------------------------------ | -------- | ----------------------------------------------------- |
| label | string | — | Field label, associated with the input. |
| hint | ReactNode | — | Guidance below the field. Replaced by an error. |
| error | string | — | Caller-supplied error. Outranks the schema. |
| schema | FieldSchema | — | Any zod schema. Only safeParse is called. |
| validateOn | "blur" \| "change" \| "never" | "blur" | When the schema runs. |
| onValidationChange | (state: FieldValidation) => void | — | Fires on each validation pass. |
| onChange | (value: string) => void | — | Receives the value, not the event. |
| value | string | — | Makes the field controlled. |
| defaultValue | string | "" | Initial value when uncontrolled. |
| optional | boolean | false | Shows an "Optional" marker on the label. |
| action | ReactNode | — | Trailing label slot, e.g. a "Forgot?" link. |
| trailing | ReactNode | — | Trailing in-field slot. Not on PasswordInput, which owns it. |
And their own:
| Component | Prop | Type | Default | Description |
| --------------- | -------------- | -------------------------------- | -------------------------------- | ------------------------------------ |
| TextInput | mono | boolean | false | Renders the value in the monospace face. |
| PasswordInput | revealable | boolean | true | Shows the Show/Hide toggle. |
| PasswordInput | revealLabels | { show: string; hide: string } | { show: "Show", hide: "Hide" } | Toggle text, for localisation. |
Any other <input> attribute — placeholder, maxLength, disabled, autoComplete —
is passed straight through, and overrides the component's default where there is one.
import { EmailInput } from "@carbonbits/sixr/email-input";
import { emailSchema } from "@carbonbits/sixr/validation";
<EmailInput
label="Work email"
placeholder="[email protected]"
schema={emailSchema}
onChange={setEmail}
/>;Validation runs on blur by default, then on every keystroke once the field has gone
invalid, so a corrected value clears the message immediately. The first schema issue
is shown, the input gets aria-invalid, and the message is announced via role="alert"
and wired up with aria-describedby.
schema is structurally typed, so any zod schema satisfies it without zod appearing
in this package's types — you only need zod installed if you use it. The inputs never
default to a schema for the same reason; pair them with the ready-made ones under
"Validation schemas".
Form / FormError / SubmitButton
import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";The shared skeleton behind the login/signup screens: a <form> that disables its
fields for the duration of onSubmit, plus a submit button that swaps to a spinner
for the same duration. Interactive, so it ships its own "use client" directive and
is reachable only at this subpath.
| Component | Prop | Type | Description |
| ------------- | -------------- | -------------------------- | --------------------------------------------------------- |
| Form | onSubmit | () => void \| Promise<void> | Called after preventDefault. A returned promise drives the disabled/spinner state until it settles. |
| Form | children | ReactNode | Fields, FormError, SubmitButton — whatever the design calls for, in order. |
| FormError | children | ReactNode | Form-level error (a rejected credential, a failed request). Renders nothing when empty. |
| SubmitButton| loadingLabel | ReactNode | Shown next to the spinner in place of children while submitting. |
import { EmailInput } from "@carbonbits/sixr/email-input";
import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";
import { PasswordInput } from "@carbonbits/sixr/password-input";
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string>();
const handleSubmit = async () => {
setError(undefined);
const ok = await signIn(email, password);
if (!ok) setError("That email and password don't match.");
};
return (
<Form onSubmit={handleSubmit}>
<EmailInput label="Work email" onChange={setEmail} />
<PasswordInput label="Password" onChange={setPassword} />
<FormError>{error}</FormError>
<SubmitButton loadingLabel="Signing in…">Sign in</SubmitButton>
</Form>
);
}A field never needs its own disabled={isLoading} — Form groups its children in a
fieldset that disables for the duration of onSubmit, so SubmitButton (and any other
control inside Form) picks that up for free. Fields, OAuth rows, dividers — anything
the design puts between the top of the form and the submit button — are just children;
Form doesn't assume a shape beyond that.
Validation schemas
import { emailSchema, passwordSchema } from "@carbonbits/sixr/validation";Ready-made zod schemas matching the Carbonbits auth rules: emailSchema,
passwordSchema, verificationCodeSchema, backupCodeSchema, fullNameSchema,
jobTitleSchema and phoneSchema. They are ordinary zod schemas — extend them with
.refine, or replace the message with .min(8, "…").
This subpath needs zod (^4), which is an optional peer dependency. Nothing else in
the package requires it.
Styling
dist/styles.css is compiled by Tailwind 4 and contains only the utilities the
library itself uses, so it is safe to load alongside a consumer's own Tailwind build.
Colours and control metrics come from CSS custom properties, all namespaced --sixr-*,
so nothing here can clobber your own Tailwind theme. Override any of them to retheme
the library without rebuilding it:
:root {
--sixr-accent: #0f766e;
--sixr-line: #d4d4d8;
}Dark values follow prefers-color-scheme by default. To drive the theme yourself, set
data-theme="dark" or data-theme="light" on the root element.
Versioning
Released under semantic versioning: breaking changes only land
in major versions, so ^ ranges are safe to pin against.
License
MIT
