Skip to main content

x-col

A zero-config flex-column custom element. Children are stacked vertically. Use gap, align, and padding attributes — no CSS required.

Quick start
import { registerLayoutElements } from "mates-ui";
registerLayoutElements(); // registers <x-row>, <x-col>, <x-box>

html`
  <x-col gap="var(--md-space-3)">
    ${Text("Title",    { type: "title-sm" })}
    ${Text("Subtitle", { type: "body-sm", color: "muted" })}
    ${Button("Action", { variant: "filled", size: "sm" })}
  </x-col>
`

Notification settings

import { html, atom, renderApp } from 'mates';
import { Button, Divider, Switch, Text } from 'mates-ui';

const App = () => {
const items = [
{ label: "Email", sub: "Receive email notifications", a: atom(true) },
{ label: "Push", sub: "Browser push notifications", a: atom(true) },
{ label: "SMS", sub: "Text message alerts", a: atom(false) },
{ label: "Desktop", sub: "Desktop notifications", a: atom(false) },
];
return () => html`
<x-col gap="var(--md-space-4)">
<x-col gap="var(--md-space-1)">
${Text("Notifications", { type: "title-sm" })}
${Text("Choose how to be notified.", { type: "body-sm", color: "muted" })}
</x-col>
${Divider()}
<x-col gap="var(--md-space-3)">
${items.map(({ label, sub, a }) => html`
<x-row align="between center">
<x-col gap="2px">
${Text(label, { type: "body-sm" })}
${Text(sub, { type: "label-sm", color: "muted" })}
</x-col>
${Switch({ checked: a(), onChange: (_, v) => a.set(v) })}
</x-row>
`)}
</x-col>
${Divider()}
<x-row align="end" gap="var(--md-space-2)">
${Button("Cancel", { variant: "ghost", size: "sm" })}
${Button("Save preferences", { variant: "filled", size: "sm" })}
</x-row>
</x-col>
`;
};

Compose message

import { html, atom, renderApp } from 'mates';
import { Button, Text, InputField, Textarea } from 'mates-ui';

const App = () => {
const to = atom("");
const body = atom("");
const sent = atom(false);
return () => sent() ? html`
<x-col gap="var(--md-space-3)" align="center">
${Text("Message sent!", { type: "title-sm" })}
${Button("Send another", { variant: "ghost", size: "sm",
onClick: () => { to.set(""); body.set(""); sent.set(false); } })}
</x-col>
` : html`
<x-col gap="var(--md-space-3)">
${InputField({ label: "To", value: to(), onInput: (_, v) => to.set(v) })}
${Textarea({ label: "Message", rows: 4, value: body(), onInput: (_, v) => body.set(v) })}
<x-row align="end" gap="var(--md-space-2)">
${Button("Cancel", { variant: "ghost", size: "sm" })}
${Button("Send", { variant: "filled", size: "sm",
disabled: !to().trim() || !body().trim(),
onClick: () => sent.set(true) })}
</x-row>
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Sidebar navigation

import { html, atom, renderApp } from 'mates';
import { Button, Card, Text } from 'mates-ui';

const ITEMS = ["Dashboard", "Projects", "Team", "Analytics", "Settings", "Help"];

const App = () => {
const active = atom("Dashboard");
return () => html`
<x-row gap="var(--md-space-4)">
<x-col p="var(--md-space-2)" gap="var(--md-space-0-5)" style="min-width:160px;">
${ITEMS.map(item => Button(item, {
variant: active() === item ? "filled" : "ghost",
size: "sm",
style: { width: "100%", justifyContent: "flex-start" },
onClick: () => active.set(item),
}))}
</x-col>
${Card({
variant: "outlined",
style: { flex: "1" },
body: html`
<x-col gap="var(--md-space-2)">
${Text(active(), { type: "title-sm" })}
${Text("Content for " + active(), { type: "body-sm", color: "muted" })}
</x-col>
`,
})}
</x-row>
`;
};

renderApp(App, document.getElementById('app'));

Task checklist

import { html, atom, renderApp } from 'mates';
import { Badge, Button, Card, Checkbox, LinearProgress, Text } from 'mates-ui';
import { AddIcon } from 'mates-icons';

const App = () => {
const tasks = atom([
{ id: 1, text: "Set up project structure", done: true },
{ id: 2, text: "Design component library", done: true },
{ id: 3, text: "Implement routing", done: false },
{ id: 4, text: "Write unit tests", done: false },
{ id: 5, text: "Deploy to production", done: false },
]);
const toggle = id => tasks.set(tasks().map(t => t.id === id ? { ...t, done: !t.done } : t));
return () => {
const all = tasks(), done = all.filter(t => t.done).length;
return html`
${Card({
variant: "outlined",
body: html`
<x-col gap="var(--md-space-4)">
<x-row align="between center">
${Text("Sprint tasks", { type: "title-sm" })}
${Badge(`${done}/${all.length} done`, { variant: done === all.length ? "success" : "primary" })}
</x-row>
${LinearProgress({ value: Math.round(done / all.length * 100), label: "Progress" })}
<x-col gap="var(--md-space-1)">
${all.map(task => html`
<x-row align="start center" gap="var(--md-space-3)" py="var(--md-space-1)">
${Checkbox({ checked: task.done, label: "", onChange: () => toggle(task.id) })}
${Text(task.text, { type: "body-sm", style: { flex: "1",
textDecoration: task.done ? "line-through" : "none",
opacity: task.done ? "0.5" : "1" } })}
</x-row>
`)}
</x-col>
<x-row align="end">

Profile card

import { html, renderApp } from 'mates';
import { Avatar, Button, Card, Chip, Divider, Text } from 'mates-ui';
import { ArrowForwardIcon, StarIcon } from 'mates-icons';

const PEOPLE = [
{ name: "Alice Johnson", role: "Engineering Lead", status: "online", rating: "4.9", tags: ["React", "TypeScript", "Node"] },
{ name: "Ben Okafor", role: "Product Designer", status: "away", rating: "4.7", tags: ["Figma", "UX", "Design"] },
];

const App = () => () => html`
<x-row gap="var(--md-space-4)" wrap>
${PEOPLE.map(p => Card({
variant: "outlined",
style: { flex: "1", minWidth: "14rem" },
body: html`
<x-col gap="var(--md-space-3)">
<x-col gap="var(--md-space-2)" align="center">
${Avatar({ name: p.name, size: "lg", status: p.status })}
<x-col gap="2px" align="center">
${Text(p.name, { type: "body-sm", style: { fontWeight: "600" } })}
${Text(p.role, { type: "label-sm", color: "muted" })}
</x-col>
<x-row gap="var(--md-space-1)" align="center">
${StarIcon({ size: 14 })}
${Text(p.rating, { type: "label-sm" })}
</x-row>
</x-col>
${Divider()}
<x-row gap="var(--md-space-1)" wrap>
${p.tags.map(tag => Chip(tag, { variant: "tag" }))}
</x-row>
${Button("View profile", {
variant: "outlined", size: "sm",
trailingIcon: ArrowForwardIcon({ size: 14 }),
style: { width: "100%" },
})}

Detail panel

import { html, renderApp } from 'mates';
import { Card, Text } from 'mates-ui';

const FIELDS = [
{ label: "Project", value: "Mates Design System" },
{ label: "Status", value: "In Progress" },
{ label: "Owner", value: "Alice Johnson" },
{ label: "Created", value: "Jan 12, 2025" },
{ label: "Due date", value: "Mar 31, 2025" },
{ label: "Priority", value: "High" },
];

const App = () => () => html`
${Card({
variant: "outlined",
body: html`
<x-col gap="0">
${FIELDS.map(({ label, value }) => html`
<x-row align="between center" py="var(--md-space-2)"
style="border-bottom:1px solid var(--md-color-border);">
${Text(label, { type: "label-sm", color: "muted",
style: { minWidth: "9rem", flexShrink: "0" } })}
${Text(value, { type: "body-sm" })}
</x-row>
`)}
</x-col>
`,
})}
`;

renderApp(App, document.getElementById('app'));

Progress stepper

import { html, atom, renderApp } from 'mates';
import { Button, Card, Text } from 'mates-ui';
import { CheckCircleIcon } from 'mates-icons';

const STEPS = [
{ step: 1, label: "Create account", sub: "Basic info & email", done: true },
{ step: 2, label: "Verify email", sub: "Check your inbox", done: true },
{ step: 3, label: "Setup workspace", sub: "Name & team size", done: false },
{ step: 4, label: "Invite teammates", sub: "Optional — skip for now", done: false },
];

const App = () => {
const current = atom(3);
return () => html`
${Card({
variant: "outlined",
style: { maxWidth: "24rem" },
body: html`
<x-col gap="var(--md-space-0-5)">
${STEPS.map((s, i, arr) => html`
<x-row gap="var(--md-space-3)" align="start start">
<x-col align="center">
<div style="width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:600;flex-shrink:0;background:${s.done?'var(--md-sys-color-primary)':s.step===current()?'var(--md-sys-color-primary-container)':'var(--md-surface-container)'};color:${s.done?'var(--md-sys-color-on-primary)':s.step===current()?'var(--md-sys-color-on-primary-container)':'var(--md-color-on-surface-variant)'};border:${!s.done&&s.step!==current()?'2px solid var(--md-color-border)':'none'};">
${s.done ? CheckCircleIcon({ size: 14 }) : Text(String(s.step), { type: "label-sm", style: { color: "inherit" } })}
</div>
${i < arr.length - 1 ? html`<div style="width:2px;height:24px;background:var(--md-color-border);margin:2px 0;"></div>` : html``}
</x-col>
<x-col gap="2px" py="var(--md-space-0-5)">
${Text(s.label, { type: "body-sm", style: { fontWeight: s.step === current() ? "600" : "400" } })}
${Text(s.sub, { type: "label-sm", color: "muted" })}
</x-col>
</x-row>
`)}
</x-col>
<x-row gap="var(--md-space-2)" style="margin-top:var(--md-space-4);">
${Button("Back", { variant: "ghost", size: "sm", disabled: current() <= 1, onClick: () => current.set(c => Math.max(1, c-1)) })}

x-col

import { registerLayoutElements } from "mates-ui"; registerLayoutElements();
Prop Type Default Description
align "center" | "[jc]" | "[jc] [ai]" | "[jc] [ai] [as]" start stretch Space-separated shorthand. 1 value = jc (or "center" = both). 2 values = jc + ai. 3 values = jc + ai + align-self.
attr AttrMap HTML attributes on the root element.
classes string | string[] Extra root classes — joined and appended after built-in classes.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
fill boolean attr false Sets width:100% and height:100% on the element.
gap any CSS length Raw CSS gap between children, e.g. `"var(--md-space-3)"` or `"1rem"`.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
inline boolean attr false Uses inline-flex instead of flex.
on OnEventMap DOM event handlers on the root element.
p any CSS length Padding on all sides.
px any CSS length Padding inline (left + right).
py any CSS length Padding block (top + bottom).
spacing number | CSS length MUI-style spacing scale — number × 8px, e.g. `"2"` → `"16px"`. Raw CSS lengths pass through unchanged.
style StyleMap Inline CSS on the root element.
wrap boolean attr false Enables flex-wrap.