# Corn UI > Corn UI is a lightweight, framework-agnostic component library built with vanilla JavaScript and CSS custom properties. It works in any environment — no framework required. React wrapper packages are available by appending `-react` to any package name. - Docs: https://cornui.com - Version: 1.0.0-beta.4 - License: MIT ## CDN Quick Start Load Corn UI without npm or a build tool. All packages expose a global variable on `window`. Prefer the single entry point: `index.min.js` carries every package behind the one `CornUI` global, and `index.min.css` every stylesheet. Reach for the per-package bundles below only when a page needs part of the library. ```html ``` Pin a version in anything you deploy — the `/latest/` path follows the newest release and will change underneath you. Versioned files never change: `https://cornui.com/cdn/v1.0.0-beta.4/index.min.js`. Every bundle has a published sha384 hash, listed at `https://cornui.com/cdn/v1.0.0-beta.4/integrity.json`. An `integrity` attribute needs `crossorigin` beside it or the check fails closed: ```html ``` To vendor the library and build offline, download `https://cornui.com/cdn/v1.0.0-beta.4/corn-ui-v1.0.0-beta.4.tar.gz`, which unpacks to `corn-ui-v1.0.0-beta.4/dist/` holding every bundle plus `integrity.json`. Each file is also addressable by name from the GitLab Package Registry, and every tagged release lists them: https://gitlab.com/corn-dev/corn-ui/-/releases Per-package bundles: ```html ``` `@corndev/ui-icons` has no CDN bundle — install it from npm. CDN globals and usage: ```js // @corndev/ui → window.CornUI const { createButton, createStack } = CornUI; const btn = createButton(document.querySelector('#btn'), { variant: 'primary' }); // @corndev/ui-overlays → window.CornUIOverlays const { createModal, createDrawer, createTooltip } = CornUIOverlays; const modal = createModal({ title: 'Hello', content: 'World', size: 'md' }); modal.open(); // @corndev/ui-notifications → window.CornUINotifications const { createToastManager, createAlert } = CornUINotifications; const toast = createToastManager({ position: 'top-right' }); toast.show({ message: 'Saved!', type: 'success' }); // @corndev/ui-markdown → window.CornUIMarkdown const { createMarkdown } = CornUIMarkdown; createMarkdown(document.querySelector('#content'), { content: '# Hello\n\nWorld' }); ``` ### Latest CDN URLs (unpinned) Always points to the most recently published release. Use pinned versioned URLs in production. ``` https://cornui.com/cdn/latest/theme.min.css https://cornui.com/cdn/latest/corn-ui.min.js https://cornui.com/cdn/latest/corn-ui-overlays.min.css https://cornui.com/cdn/latest/corn-ui-overlays.min.js https://cornui.com/cdn/latest/corn-ui-notifications.min.css https://cornui.com/cdn/latest/corn-ui-notifications.min.js https://cornui.com/cdn/latest/corn-ui-markdown.min.css https://cornui.com/cdn/latest/corn-ui-markdown.min.js ``` ## npm Installation Packages are published to npmjs.org under the `@corndev` scope; no registry configuration is needed. ```bash npm install @corndev/ui npm install @corndev/ui-overlays npm install @corndev/ui-notifications npm install @corndev/ui-markdown npm install @corndev/ui-icons npm install @corndev/ui-hooks npm install @corndev/store npm install @corndev/query ``` Install every `@corndev` package at the same version; each depends on its siblings at exactly its own. **Import each package's stylesheet once.** `@corndev/ui/theme` styles only `@corndev/ui`'s components; every other package ships its own, and without it that package's components render as unstyled markup (a development build warns, naming the missing import): ```js import '@corndev/ui/theme'; import '@corndev/ui-overlays/styles'; // Modal, Drawer, Accordion, Tooltip, Popover, Menu import '@corndev/ui-notifications/styles'; // Alert, Toast import '@corndev/ui-markdown/styles'; // Markdown import '@corndev/ui-icons/styles'; // Icon ``` The `-react` packages ship no CSS; the stylesheet comes from the vanilla package beside each. ## React Packages Every vanilla package has a React mirror. Add `-react` to any package name: - `@corndev/ui-react` — React wrappers for `@corndev/ui`. npm install @corndev/ui @corndev/ui-react - `@corndev/ui-overlays-react` — React wrappers for `@corndev/ui-overlays`. npm install @corndev/ui-overlays @corndev/ui-overlays-react - `@corndev/ui-notifications-react` — React wrappers for `@corndev/ui-notifications`. npm install @corndev/ui-notifications @corndev/ui-notifications-react - `@corndev/ui-markdown-react` — React wrappers for `@corndev/ui-markdown`. npm install @corndev/ui-markdown @corndev/ui-markdown-react - `@corndev/ui-icons-react` — React wrappers for `@corndev/ui-icons`. npm install @corndev/ui-icons-react React usage: ```jsx import '@corndev/ui/theme'; import { Button, Stack, Input } from '@corndev/ui-react'; import { Modal, Drawer, Accordion, Tooltip, Popover, Menu } from '@corndev/ui-overlays-react'; import { Alert, NotificationProvider, useNotifications } from '@corndev/ui-notifications-react'; import { Markdown } from '@corndev/ui-markdown-react'; import { SearchIcon } from '@corndev/ui-icons-react'; ``` Every React component also accepts `className` (merged with the Corn UI classes) and forwards any remaining props to the underlying DOM element. NotificationProvider usage (wrap your app once, then call `useNotifications()` anywhere): ```jsx import { NotificationProvider, useNotifications } from '@corndev/ui-notifications-react'; function App() { return ( ); } function SaveButton() { const { show } = useNotifications(); return ; } ``` ## Design System - **Colors:** Purple primary (`--corn-primary`), Indigo secondary (`--corn-secondary`) - **Spacing:** 4px base — `--corn-spacing-{xs|sm|md|lg|xl}` - **Typography:** 14px, 1.4 line-height, system font stack - **Radius:** `--corn-radius-sm` 4px / `--corn-radius-md` 6px / `--corn-radius-lg` 8px - **Dark mode:** Manual toggle via `data-theme="dark"` on `` - **BEM naming:** All CSS classes use `.corn-*` prefix ## Theming Corn UI is themed entirely through CSS custom properties. There is no theme object, no build step, and no JS API — you redefine tokens in your own CSS and every component follows. Component CSS never hardcodes a colour, so you never need `!important` or selector overrides. Load the theme once, then override the tokens you care about: ```css @import '@corndev/ui/theme'; :root { --corn-primary: #0ea5e9; --corn-primary-hover: #38bdf8; --corn-primary-active: #0284c7; } ``` That is a complete re-theme. Focus rings, overlays, and shadows are **derived** from the brand tokens with `color-mix()`, so they track `--corn-primary` and `--corn-danger` automatically — you do not restate them. Override a derived token directly only if you want it to stop following its source: ```css :root { --corn-focus-ring-width: 2px; /* thinner ring, same hue */ --corn-focus-ring: 0 0 0 2px rgba(0, 0, 0, 0.4); /* opt out of the hue entirely */ } ``` Tokens are ordinary custom properties, so they cascade — scope a theme to part of the page by setting them on any element: ```css .checkout-panel { --corn-primary: #16a34a; } ``` ### Dark mode Set `data-theme="dark"` on ``. The bundled dark theme overrides 40 tokens. Because the derived tokens are composed rather than copied, focus rings re-tint for dark mode with no extra declarations. ```js document.documentElement.setAttribute('data-theme', 'dark'); ``` The dark theme ships on `:root[data-theme='dark']`, which outranks a plain `:root` on purpose: without the leading `:root` the two selectors tie on specificity, and since app CSS loads after the library, a `:root` re-theme would silently win in dark mode too and take dark mode down with it. So a `:root` block sets the **light** theme; to change a dark value, match the same selector: ```css /* light */ :root { --corn-primary: #0ea5e9; } /* dark */ :root[data-theme='dark'] { --corn-primary: #38bdf8; } ``` A bare `[data-theme='dark'] { … }` scores lower and will not apply. Tokens the dark theme does not restate are derived and follow the `:root` override in both. `prefers-color-scheme` is not wired up — the attribute is the only switch. Track the OS setting by flipping it: ```js const mq = matchMedia('(prefers-color-scheme: dark)'); const sync = () => document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light'); sync(); mq.addEventListener('change', sync); ``` ### Browser support Derived tokens use `color-mix()` (Chrome 111, Safari 16.2, Firefox 113 — all 2023). Older browsers fall back to the default purple/red rings, which stay visible but stop tracking your brand colour. ## Design Tokens Every token, read from `src/theme/tokens.css`. All of them are overridable. ### COLORS — Primary & Secondary | Token | Default | |---|---| | `--corn-primary` | `#7c3aed` | | `--corn-primary-hover` | `#6d28d9` | | `--corn-primary-active` | `#5b21b6` | | `--corn-primary-light` | `#ede9fe` | | `--corn-secondary` | `#4f46e5` | | `--corn-secondary-hover` | `#4338ca` | | `--corn-secondary-active` | `#3730a3` | | `--corn-secondary-light` | `#e0e7ff` | ### COLORS — Status Colors | Token | Default | |---|---| | `--corn-success` | `#047857` | | `--corn-success-light` | `#d1fae5` | | `--corn-warning` | `#b45309` | | `--corn-warning-light` | `#fef3c7` | | `--corn-danger` | `#b91c1c` | | `--corn-danger-light` | `#fee2e2` | | `--corn-info` | `#1d4ed8` | | `--corn-info-light` | `#dbeafe` | ### COLORS — Semantic Text Colors | Token | Default | |---|---| | `--corn-text-primary` | `#1a1a1a` | | `--corn-text-secondary` | `#4d4d4d` | | `--corn-text-tertiary` | `#6b6b6b` | | `--corn-text-inverse` | `#ffffff` | ### COLORS — Semantic Background Colors | Token | Default | |---|---| | `--corn-bg-primary` | `#ffffff` | | `--corn-bg-secondary` | `#f9f9f9` | | `--corn-bg-tertiary` | `#f0f0f0` | ### COLORS — Border Colors | Token | Default | |---|---| | `--corn-border-light` | `#e5e5e5` | | `--corn-border-primary` | `#d0d0d0` | | `--corn-border-secondary` | `#a0a0a0` | ### COLORS — Chart / Data Viz | Token | Default | |---|---| | `--corn-chart-1` | `#4a3aa7` | | `--corn-chart-2` | `#eb6834` | | `--corn-chart-3` | `#1baf7a` | | `--corn-chart-4` | `#eda100` | | `--corn-chart-5` | `#e87ba4` | | `--corn-chart-6` | `#008300` | | `--corn-chart-7` | `#2a78d6` | | `--corn-chart-8` | `#e34948` | | `--corn-chart-seq-1` | `#b8a5fc` | | `--corn-chart-seq-2` | `#9575f8` | | `--corn-chart-seq-3` | `#7c3aed` | | `--corn-chart-seq-4` | `#5b21b6` | ### SPACING | Token | Default | |---|---| | `--corn-spacing-xs` | `4px` | | `--corn-spacing-sm` | `8px` | | `--corn-spacing-md` | `16px` | | `--corn-spacing-lg` | `24px` | | `--corn-spacing-xl` | `32px` | ### TYPOGRAPHY | Token | Default | |---|---| | `--corn-font-family` | `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif` | | `--corn-font-family-mono` | `'Monaco', 'Menlo', 'Ubuntu Mono', 'Courier New', monospace` | | `--corn-font-size-xs` | `11px` | | `--corn-font-size-sm` | `12px` | | `--corn-font-size-base` | `14px` | | `--corn-font-size-lg` | `16px` | | `--corn-font-size-xl` | `18px` | | `--corn-font-weight-regular` | `400` | | `--corn-font-weight-medium` | `500` | | `--corn-font-weight-bold` | `700` | | `--corn-line-height` | `1.4` | | `--corn-control-height-sm` | `calc(var(--corn-font-size-sm) * var(--corn-line-height) + 10px)` | | `--corn-control-height-md` | `calc(var(--corn-font-size-base) * var(--corn-line-height) + 14px)` | | `--corn-control-height-lg` | `calc(var(--corn-font-size-lg) * var(--corn-line-height) + 22px)` | ### BORDER RADIUS | Token | Default | |---|---| | `--corn-radius-sm` | `4px` | | `--corn-radius-md` | `6px` | | `--corn-radius-lg` | `8px` | ### SHADOWS | Token | Default | |---|---| | `--corn-shadow-sm` | `0 1px 2px rgba(0, 0, 0, 0.05)` | | `--corn-shadow-md` | `0 4px 6px rgba(0, 0, 0, 0.1)` | | `--corn-shadow-lg` | `0 10px 15px rgba(0, 0, 0, 0.1)` | ### TRANSITIONS | Token | Default | |---|---| | `--corn-transition-fast` | `100ms ease-in-out` | | `--corn-transition-base` | `200ms ease-in-out` | | `--corn-transition-slow` | `300ms ease-in-out` | ### BREAKPOINTS | Token | Default | |---|---| | `--corn-bp-sm` | `640px` | | `--corn-bp-md` | `960px` | | `--corn-bp-lg` | `1280px` | ### SEMANTIC / DERIVED | Token | Default | |---|---| | `--corn-focus-ring-width` | `3px` | | `--corn-focus-ring-color` | `rgba(124, 58, 237, 0.15)` | | `--corn-focus-ring-error-color` | `rgba(185, 28, 28, 0.15)` | | `--corn-focus-ring` | `0 0 0 var(--corn-focus-ring-width) var(--corn-focus-ring-color)` | | `--corn-focus-ring-error` | `0 0 0 var(--corn-focus-ring-width) var(--corn-focus-ring-error-color)` | | `--corn-backdrop` | `rgba(0, 0, 0, 0.5)` | | `--corn-shadow-color` | `rgba(0, 0, 0, 0.15)` | | `--corn-chart-grid` | `var(--corn-border-light)` | | `--corn-chart-axis` | `var(--corn-border-primary)` | | `--corn-layout-divider` | `var(--corn-border-primary)` | | `--corn-chart-surface` | `var(--corn-bg-primary)` | | `--corn-chart-muted` | `var(--corn-text-tertiary)` | ## Component API Reference All vanilla `create*` functions return a controller object. Shared interface: - `update(options)` — apply new options without re-mounting - `destroy()` — remove all Corn UI classes and event listeners Components marked **CSS-only** have no JS controller — apply their classes directly. ## @corndev/ui Core vanilla JS layout and form components with CSS design tokens. **Install:** `npm install @corndev/ui` **React package:** `@corndev/ui-react` **CDN JS:** `https://cornui.com/cdn/v1.0.0-beta.4/corn-ui.min.js` (global: `CornUI`) **CDN CSS:** `https://cornui.com/cdn/v1.0.0-beta.4/theme.min.css` ### Components ### createTreeState A tree's state with no DOM and no framework: which nodes are expanded, which one is selected, and a hook for fetching a branch's children when it opens. The part of a tree every consumer writes identically — the renderer, with its icons and context menus, stays with the consumer. **JavaScript** — this is the API: `createTreeState(options)` → `TreeState`. It renders nothing. **Options:** | Option | Type | Default | Description | |---|---|---|---| | `defaultExpanded` | `Iterable` | `undefined` | Nodes open to begin with (uncontrolled). | | `defaultSelected` | `string\|null` | `undefined` | Node selected to begin with (uncontrolled). | | `onExpand` | `(value: string) => void\|Promise` | `undefined` | Called when a node opens, once per open. It may be async: a hierarchy that is fetched as it is explored — a filesystem, an API, a large table — is the common case, not the exotic one, and walking it up front is what makes a file tree do tens of thousands of round trips it did not need to. The node stays open while the promise is pending, and stays open if it rejects: a branch that collapsed itself under the cursor would lose the user's place, and the caller knows what to show instead. A rejection is passed to `onError` rather than swallowed. Same contract as `createNavigation`'s `onExpand(item)`, so one "get these children" function serves both. | | `onCollapse` | `(value: string) => void` | `undefined` | Called when a node closes. | | `onSelect` | `(value: string\|null) => void` | `undefined` | Called when the selection changes — *changes*, not "when select() is called". Selecting the node that is already selected does nothing. | | `onError` | `(error: unknown, value: string) => void` | `undefined` | Called when `onExpand` fails — whether it threw synchronously or returned a promise that rejected. The two behave the same on purpose: a caller who wrote a non-async `onExpand` has no reason to expect a throw to escape `toggle()` into their event handler. | **Note:** `onExpand` may be async, because a hierarchy fetched as it is explored — a filesystem, an API, a large table — is the common case. The node opens immediately and stays open while the promise is pending, and stays open if it rejects, with the error going to `onError`; a branch that collapsed itself under the cursor would lose the user's place. It is the same callback contract as `createNavigation`'s `onExpand(item)`, so one "get these children" function serves both. Selecting the node that is already selected does nothing and fires nothing, since consumers act on a selection by loading what was selected. `getState()` returns the same object until something changes, which is what React's useSyncExternalStore requires. `useTree` in @corndev/ui-hooks binds it to a component and adds nothing. ### Button Interactive button element. **HTML:** ```html ``` **Classes:** | Class | |---| | `.corn-button` | **Attributes:** | Attribute | Values | |---|---| | `data-size` | `sm\|md\|lg` | | `data-variant` | `icon\|unstyled` | | `data-level` | `primary\|secondary\|success\|danger\|warning\|ghost\|neutral` | | `data-emphasis` | `solid\|soft\|outline\|quiet` | | `data-full-width` | `present or absent` | | `data-loading` | `present or absent` | **JavaScript** — optional: `createButton(el, options)` → `{ update: (opts: ButtonOptions) => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Options** — the React props are the same: | Option | Type | Default | Description | |---|---|---|---| | `variant` | `'button'\|'icon'\|'unstyled'` | `'button'` | What kind of control this is. `icon` makes it square and drops the label padding, for a control whose whole content is an icon. Independent of `level`. `unstyled` has no appearance at all — for making a whole card or row clickable — and ignores `level` and `emphasis`, since it has no fill. | | `level` | `'primary'\|'secondary'\|'success'\|'danger'\|'warning'\|'neutral'\|'ghost'` | `'primary'` | Which colour. `ghost` is deprecated — it is the one value that names a fill rather than a colour, and it means `level='primary'` with `emphasis='outline'`. `neutral` has no hue: the text's own colour, for an action that should not read as a status or the accent — with `emphasis: 'quiet'`, a plain grey hover on text-coloured ink. | | `emphasis` | `'solid'\|'soft'\|'outline'\|'quiet'` | `'solid'` | How much of that colour is filled in, independently of which colour it is. `solid` is a filled block, `soft` a tint with the colour as the text, `outline` a border and no fill, `quiet` neither — for a destructive action in a table row that should not shout. Hover escalates one rung; the hit area is the same at every rung. | | `size` | `'sm'\|'md'\|'lg'` | `'md'` | | | `disabled` | `boolean` | `false` | | | `loading` | `boolean` | `false` | | | `fullWidth` | `boolean` | `false` | Fill the row. An unstyled button becomes a block, for a list entry or a whole card that is one control. | | `type` | `'button'\|'submit'\|'reset'` | `'button'` | | **React:** `Button` — from `@corndev/ui-react` **Note:** `data-variant` and `data-level` are separate dimensions on purpose: the variant says what kind of control this is (`button` or `icon`), and the level carries the colour ladder, so a red delete icon is expressible as `variant=icon level=danger`. An icon button is square by construction and sized by hit area rather than by its glyph — the control height for its size, `--corn-control-height-{sm,md,lg}` (26.8/33.6/44.4px by default), the same as a text button and an input, and all clear of the 24px minimum in WCAG 2.5.8. It needs an `aria-label`: an icon-only button with no accessible name is invisible to a screen reader, and `createButton` warns when one is missing. **`data-level` is the colour and `data-emphasis` is the fill**, and they are independent: `solid` (the default) is a filled block, `soft` a tint with the colour as the text, `outline` a border and no fill, `quiet` neither. That split is what makes a quiet red Delete expressible — before it, `danger` was always a solid block and the only borderless value, `ghost`, was hardcoded to primary. Hover escalates one rung, and the padding is identical at every rung so a quiet button still meets the 24px target. **`level="ghost"` is deprecated**: it is the one value of `level` that names a fill rather than a colour, and it means `level="primary" emphasis="outline"`. It still renders, so existing markup is unaffected. **`variant="unstyled"`** removes the appearance entirely, for making a whole card or row clickable and keyboard-reachable; it has no fill, so `level` and `emphasis` are not emitted, while the focus ring, disabled and loading states and a 24px minimum target remain. ### Stack Flex stack container — vertical or horizontal. **HTML:** ```html
First
Second
``` **Classes:** | Class | |---| | `.corn-stack` | **Attributes:** | Attribute | Values | |---|---| | `data-direction` | `vertical\|horizontal` | | `data-gap` | `xs\|sm\|md\|lg\|xl` | | `data-align` | `start\|center\|end\|stretch` | | `data-justify` | `start\|center\|end\|between\|around` | **JavaScript** — optional: `createStack(el, options)` → `{ update: (opts: StackOptions) => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Options** — the React props are the same: | Option | Type | Default | |---|---|---| | `direction` | `'vertical'\|'horizontal'` | `'vertical'` | | `gap` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'` | `'md'` | | `align` | `'start'\|'center'\|'end'\|'stretch'` | `'stretch'` | | `justify` | `'start'\|'center'\|'end'\|'between'\|'around'` | `'start'` | **React:** `Stack` — from `@corndev/ui-react` ### Group Horizontal flex group for inline elements. **HTML:** ```html
``` **Classes:** | Class | |---| | `.corn-group` | **Attributes:** | Attribute | Values | |---|---| | `data-nowrap` | `present or absent` | | `data-gap` | `xs\|sm\|md\|lg\|xl` | | `data-align` | `start\|center\|end\|baseline` | | `data-justify` | `start\|center\|end\|between\|around` | | `data-grow` | `present or absent` | **JavaScript** — optional: `createGroup(el, options)` → `{ update: (opts: GroupOptions) => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Options** — the React props are the same: | Option | Type | Default | Description | |---|---|---|---| | `gap` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'` | `'sm'` | | | `align` | `'start'\|'center'\|'end'\|'baseline'` | `'center'` | | | `justify` | `'start'\|'center'\|'end'\|'between'\|'around'` | `'start'` | | | `wrap` | `boolean` | `true` | | | `grow` | `boolean` | `false` | Every child takes an equal share of the row, whatever it contains. A child's own `flex` always wins — the rule is 0-0-0, so even a single class like `.no-grow { flex: none }` keeps that child at its natural width. With `wrap`, a child that no longer fits moves to the next line and fills it; with `wrap={false}` the children shrink instead. | **React:** `Group` — from `@corndev/ui-react` ### Spacing Margin and padding utility classes on the spacing tokens. **CSS-only** — no `create*` function and no React wrapper; the classes are the whole API. **HTML:** ```html

Endpoints

Every call this API accepts.

…
``` **Classes:** | Class | Description | |---|---| | `.corn-m-0` | | | `.corn-m-xs` | | | `.corn-m-sm` | | | `.corn-m-md` | | | `.corn-m-lg` | | | `.corn-m-xl` | | | `.corn-mt-0` | | | `.corn-mt-xs` | | | `.corn-mt-sm` | | | `.corn-mt-md` | | | `.corn-mt-lg` | | | `.corn-mt-xl` | | | `.corn-mb-0` | | | `.corn-mb-xs` | | | `.corn-mb-sm` | | | `.corn-mb-md` | One of 60: `corn-{m\|p}{side}-{step}`. Side is none (all four), `t`/`b` (block start/end), `x` (inline both) or `y` (block both); step is `0` or a spacing token, `xs`–`xl`. | | `.corn-mb-lg` | | | `.corn-mb-xl` | | | `.corn-mx-0` | | | `.corn-mx-xs` | | | `.corn-mx-sm` | | | `.corn-mx-md` | | | `.corn-mx-lg` | | | `.corn-mx-xl` | | | `.corn-my-0` | | | `.corn-my-xs` | | | `.corn-my-sm` | | | `.corn-my-md` | | | `.corn-my-lg` | | | `.corn-my-xl` | | | `.corn-p-0` | | | `.corn-p-xs` | | | `.corn-p-sm` | | | `.corn-p-md` | | | `.corn-p-lg` | | | `.corn-p-xl` | | | `.corn-pt-0` | | | `.corn-pt-xs` | | | `.corn-pt-sm` | | | `.corn-pt-md` | | | `.corn-pt-lg` | | | `.corn-pt-xl` | | | `.corn-pb-0` | | | `.corn-pb-xs` | | | `.corn-pb-sm` | | | `.corn-pb-md` | | | `.corn-pb-lg` | | | `.corn-pb-xl` | | | `.corn-px-0` | | | `.corn-px-xs` | | | `.corn-px-sm` | | | `.corn-px-md` | | | `.corn-px-lg` | | | `.corn-px-xl` | | | `.corn-py-0` | | | `.corn-py-xs` | | | `.corn-py-sm` | | | `.corn-py-md` | | | `.corn-py-lg` | | | `.corn-py-xl` | | ### Box Token-based surface / layout primitive with padding, background, and border. **HTML:** ```html
Content
``` **Classes:** | Class | |---| | `.corn-box` | **Attributes:** | Attribute | Values | |---|---| | `data-padding` | `xs\|sm\|md\|lg\|xl` | | `data-padding-x` | `xs\|sm\|md\|lg\|xl` | | `data-padding-y` | `xs\|sm\|md\|lg\|xl` | | `data-background` | `primary\|secondary\|tertiary` | | `data-border` | `present or absent` | | `data-radius` | `sm\|md\|lg` | **JavaScript** — optional: `createBox(el, options)` → `{ update: (opts: BoxOptions) => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Vanilla options** — the React props differ, see below: | Option | Type | Default | Description | |---|---|---|---| | `padding` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'none'` | `'none'` | | | `paddingX` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'none'` | `undefined` | Overrides horizontal padding | | `paddingY` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'none'` | `undefined` | Overrides vertical padding | | `background` | `'primary'\|'secondary'\|'tertiary'\|'none'` | `'none'` | | | `border` | `boolean` | `false` | | | `radius` | `'sm'\|'md'\|'lg'\|'none'` | `'none'` | | **React:** `Box` — from `@corndev/ui-react` **React props:** | Prop | Type | Default | Description | |---|---|---|---| | `padding` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'none'` | `'none'` | | | `paddingX` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'none'` | `undefined` | Overrides horizontal padding | | `paddingY` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|'none'` | `undefined` | Overrides vertical padding | | `background` | `'primary'\|'secondary'\|'tertiary'\|'none'` | `'none'` | | | `border` | `boolean` | `false` | | | `radius` | `'sm'\|'md'\|'lg'\|'none'` | `'none'` | | | `as` | `React.ElementType` | `'div'` | | | `children` | `React.ReactNode` | `undefined` | | | `className` | `string` | `undefined` | | ### Container Centered, max-width content column. sm=640px, md=960px, lg=1280px. **HTML:** ```html
Page content
``` **Classes:** | Class | |---| | `.corn-container` | **Attributes:** | Attribute | Values | |---|---| | `data-size` | `sm\|md\|lg\|fluid` | **JavaScript** — optional: `createContainer(el, options)` → `{ update: (opts: ContainerOptions) => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Vanilla options** — the React props differ, see below: | Option | Type | Default | |---|---|---| | `size` | `'sm'\|'md'\|'lg'\|'fluid'` | `'md'` | **React:** `Container` — from `@corndev/ui-react` **React props:** | Prop | Type | Default | |---|---|---| | `size` | `'sm'\|'md'\|'lg'\|'fluid'` | `'md'` | | `as` | `React.ElementType` | `'div'` | | `children` | `React.ReactNode` | `undefined` | | `className` | `string` | `undefined` | ### Grid CSS grid container with column and gap utilities. **HTML:** ```html
Column 1
Column 2
Column 3
Location
Name
Format
``` **Classes:** | Class | |---| | `.corn-grid` | | `.corn-grid-col` | **Attributes:** | On | Attribute | Values | |---|---|---| | `.corn-grid` | `data-cols` | `present or absent` | | `.corn-grid` | `data-gap` | `xs\|sm\|md\|lg\|xl` | | `.corn-grid` | `data-row-gap` | `xs\|sm\|md\|lg\|xl` | | `.corn-grid` | `data-col-gap` | `xs\|sm\|md\|lg\|xl` | | `.corn-grid-col` | `data-span` | `1\|2\|3\|4\|5\|6\|7\|8\|9\|10\|11\|12\|content\|auto` | **CSS variables:** | Variable | Default | Description | |---|---|---| | `--corn-grid-cols` | `12` | Column count when `data-cols` is set | **JavaScript** — optional: `createGrid(el, options)` → `{ update: (opts: GridOptions) => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Options** — the React props are the same: | Option | Type | Default | Description | |---|---|---|---| | `cols` | `number\|null` | `12` | Equal columns, written to the `--corn-grid-cols` custom property. Pass `null` for no column template, so you can set `grid-template-columns` or `grid-template-areas` yourself. | | `gap` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'\|null` | `'md'` | Uniform gap between cells. | | `rowGap` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'` | `undefined` | Row gap only; overrides the uniform gap. | | `colGap` | `'xs'\|'sm'\|'md'\|'lg'\|'xl'` | `undefined` | Column gap only; overrides the uniform gap. | **React:** `Grid`, `GridCol` — from `@corndev/ui-react` **Note:** **`Grid.Col` says how much of the row a child takes**: `span` of 1–12 (of `cols`), `content` for as wide as what is inside it, or `auto` for whatever the row has left. A grid holding columns lays out as a wrapping flex row, because `content` and `auto` cannot be said with fixed tracks; the spans subtract the gaps they do not straddle, so 6 + 6 and 4 + 8 sum exactly to the row. A child that is not a column still renders, sized to its content — nothing is dropped. Below 640px every column takes the full row. In plain markup it is `.corn-grid-col` with `data-span`. The column count is a custom property, not a class, so the controller and the React `cols` prop own `--corn-grid-cols` and overwrite an inline one. Pass `cols: null` to opt out entirely and set `grid-template-columns` or `grid-template-areas` yourself. An invalid count falls back to the stylesheet default of 12 rather than emitting `repeat(0, …)` and voiding the template. **`` props** — A cell that says how much of the row it takes. | Prop | Type | Default | Description | |---|---|---|---| | `span` | `number\|'content'\|'auto'` | `12` | How much of the row to take. | | `children` | `React.ReactNode` | `undefined` | | | `className` | `string` | `undefined` | | | `style` | `React.CSSProperties` | `undefined` | | ### Collapse Animated show/hide of a region; hidden content leaves the tab order. **HTML:** ```html
…
``` **Classes:** | Class | |---| | `.corn-collapse` | | `.corn-collapse-inner` | **Attributes:** | Attribute | Values | |---|---| | `data-open` | `present or absent` | **JavaScript** — optional: `createCollapse(el, options)` → `{ update: (opts: CollapseOptions) => void, open: () => void, close: () => void, toggle: () => void, destroy: () => void }`. Only adds and removes the classes and attributes above, so the HTML is equivalent. Use it to change options at runtime. **Options** — the React props are the same: | Option | Type | Default | Description | |---|---|---|---| | `open` | `boolean` | `false` | Whether the region is shown. Opening animates it to its natural height; closing animates it shut and then hides it from the tab order and assistive technology. | **React:** `Collapse` — from `@corndev/ui-react` **Note:** **Height animates without measuring**: the region is a one-row grid going from `0fr` to `1fr`, so it reaches the content's natural height and follows content that changes size while open. **Closed is `visibility: hidden`** once the transition ends, so hidden controls leave the tab order and the accessibility tree; reduced motion makes the change instant through the transition tokens. The toggle button, with its `aria-expanded` and `aria-controls`, is yours. In React, pass a function as children to build the region only when it first opens; it stays built so closing can animate. Without an animation, `{open && }` is simpler. ### Breadcrumbs Where the current page sits in a hierarchy, with the middle collapsed when the path is long. **HTML:** ```html ``` **Classes:** | Class | |---| | `.corn-breadcrumbs` | | `.corn-breadcrumbs-item` | | `.corn-breadcrumbs-link` | | `.corn-breadcrumbs-ellipsis` | | `.corn-breadcrumbs-current` | **JavaScript** — required for behaviour: `createBreadcrumbs(el, options)` → `{ update: (opts: Partial) => void, destroy: () => void }`. Adds builds the trail from data and makes the collapsed middle (`maxItems`) expand on click. **Options** — the React props are the same: | Option | Type | Default | Description | |---|---|---|---| | `items` | `BreadcrumbItem[]` | `required` | The path, the current page last. Every crumb but the last is a link when it has an `href`; the last is always the current page, marked `aria-current="page"`. | | `maxItems` | `number` | `undefined` | Show at most this many crumbs. The middle ones collapse behind a "…" button that shows them; the first crumb and the current page always stay. | | `separator` | `string` | `undefined` | Drawn between crumbs, `/` by default. Hidden from assistive technology. | | `label` | `string` | `'Breadcrumb'` | The navigation landmark's name. | **React:** `Breadcrumbs` — from `@corndev/ui-react` **Note:** **An ordered list in a labelled `