
Security News
Deno Goes the Way of the Dinosaur
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.
@codellyson/justui
Advanced tools
Design system for the just* app family — theme tokens, twelve themes, a no-flash boot script, ~50 React components on Radix, Astro adapters, and a Tailwind v4 design config.
Shared design system for the just* app family — theme tokens, twelve themes, a no-flash boot script, around fifty React components on Radix, Astro adapters, and a Tailwind v4 design config.
See STYLE_GUIDE.md for the full design spec.
pnpm playground
Every component documented in MDX with live, editable previews, a switcher across all twelve themes, and a control that changes the host's root font size. See playground/README.md to add a page.
pnpm add @codellyson/justui
Peers your app needs separately:
pnpm add @fontsource-variable/geist @fontsource-variable/geist-mono tailwindcss
pnpm add @heroicons/react
@heroicons/react is a required peer — the React components import from it, and
the leftIcon / icon props expect icon components rather than characters.
One import chain gives you the tokens, the design config and Tailwind itself:
/* src/styles/global.css */
@import "@codellyson/justui/tailwind.css";
That one line pulls in the token fallback, Tailwind itself, and the design config —
the tokens as @theme colours, the px type scale, the radii, the keyframes and the
shell-* container variants. It also declares its own @source, so the package's
classes are scanned without you writing a glob for node_modules. That glob is
worth not writing by hand anyway: under pnpm the package is a symlink into the
store, and a hand-written glob silently matches nothing, leaving an unstyled UI with
no error.
Import tailwindcss yourself first if you need to configure it — say
@import "tailwindcss" source(none). Doing both is harmless; the duplicate costs
about 240 bytes.
Tailwind v4 is required. There is no JS preset — see CHANGELOG.md if you are coming from 0.4.0.
The boot script below sets a dark class; Starlight and next-themes report the mode
as data-theme. The dark: variant matches either, so nothing extra is needed.
---
// src/layouts/Layout.astro
import '@fontsource-variable/geist';
import '@fontsource-variable/geist-mono';
import '../styles/global.css';
---
<!doctype html>
<html lang="en">
<head>
<!-- Runs before <body> renders so the right palette paints first. -->
<script>
import { bootTheme } from '@codellyson/justui/boot';
bootTheme({ keyPrefix: 'myapp' });
</script>
</head>
<body><slot /></body>
</html>
---
import ThemeToggle from '@codellyson/justui/astro/ThemeToggle.astro';
---
<ThemeToggle />
The floating button at bottom-right opens a panel with a mode toggle (light/dark) and every theme. State persists to myapp.theme.id / myapp.theme.mode.
| Export | Purpose |
|---|---|
@codellyson/justui | BUILT_IN_THEMES, DEFAULT_THEME_ID, VAR_MAP, bootTheme |
@codellyson/justui/boot | Just bootTheme — small import if you only want this |
@codellyson/justui/themes | Just the theme catalog |
@codellyson/justui/tokens.css | :root fallback (Espresso dark) |
@codellyson/justui/tailwind.css | Design config for Tailwind v4 (@theme, variants, keyframes) |
@codellyson/justui/astro/Button.astro | Primary / secondary button |
@codellyson/justui/astro/ThemeToggle.astro | Floating theme picker |
@codellyson/justui/react | React component kit (Radix under the hood), useTheme, cn |
import { Button, Input, Menu, Modal } from '@codellyson/justui/react';
Every component ships with a "use client" directive, so importing them from a
Next.js App Router server component works — no wrapper file needed. cn is
the exception and stays server-safe.
Subpath types work on any moduleResolution. node and node10 ignore the
exports field, so a typesVersions fallback maps /react, /boot and /themes
for them; node16, nodenext and bundler read exports directly.
The API follows the house style of @khaime-dev/ui, so names and code transfer
between the two.
variant and color are separate props. variant picks the shape, color
picks the palette — there is no destructive variant:
<Button variant="outline" color="red">Delete</Button>
<Badge variant="solid" color="green">Live</Badge>
Focus has one treatment per kind of focusable, exported from the package so a
new component doesn't invent its own: focusRing for fields and standalone
buttons, focusRingInset for rows in a list or sidebar, focusLink for inline
text. The row variant is inset deliberately — an outer ring boxes the row, and it
has to stay visible on a row that already carries a selected background.
Sizes are xs sm md lg. Where a component needs a real dimension
instead of a scale step, it takes number | string — Modal's width,
AssetUploader's tileWidth / tileHeight / tileRatio. Class-name props are
escape hatches for looks, never the way to set a size.
Form controls own their label. label, description and error are props on
Input, Textarea, Select, Checkbox and Switch — there is no separate
wrapper component. The id, htmlFor, aria-describedby and aria-invalid are
wired for you during render, so the SSR markup carries them too:
<Input label="Slug" description="Lowercase only." error={errors.slug} />
An error supersedes a description, and sets aria-invalid plus the danger
border.
onChange gives you the value, not the event. onChangeEvent is there when
you need the event itself:
<Input value={slug} onChange={setSlug} />
<Select value={region} onChange={setRegion} options={regions} />
<Switch checked={on} onChange={setOn} />
Overlays use opened + onClose, with withCloseButton,
closeOnClickOutside and closeOnEscape:
<Modal opened={opened} onClose={close} title="Rename" withCloseButton>…</Modal>
Modal is adaptive: a centred dialog on wide screens, a bottom sheet below
mobileBreakpoint (768px), and the sheet can be dragged down to dismiss. Pass
centered to keep it centred at every width.
AppShell frames the page. header={{ height }} and navbar={{ width, breakpoint, opened }}
on the root reach AppShell.Header / .Navbar / .Main through CSS variables:
<AppShell header={{ height: 56 }} navbar={{ width: 240, opened, onClose: close }}>
<AppShell.Header>…</AppShell.Header>
<AppShell.Navbar>…</AppShell.Navbar>
<AppShell.Main>…</AppShell.Main>
</AppShell>
The navbar hides below breakpoint (default md) and opened brings it in over
the content with a dismissable overlay.
Compound only where children repeat. Menu, Tabs, Dialog, Table,
AssetUploader, Breadcrumb and AppShell expose parts; everything else is prop-driven. Parts are also exported flat (MenuItem,
TabsList, …) if you need per-part tree-shaking.
<Menu>
<Menu.Target asChild><Button>Actions</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Label>Project</Menu.Label>
<Menu.Item rightSection="⌘R">Rename</Menu.Item>
<Menu.Divider />
<Menu.Item color="red">Delete</Menu.Item>
</Menu.Dropdown>
</Menu>
<Tabs defaultValue="logs" onChange={setTab}>
<Tabs.List>
<Tabs.Tab value="logs" rightSection={<Badge>3</Badge>}>Logs</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="logs">…</Tabs.Panel>
</Tabs>
AssetUploader is compound where tiles are. Files arrive by click, drop or
paste via react-dropzone; reading and uploading
them stays yours. Pass items for the plain case, or tiles as children when each
needs its own overlay — progress, a retry, a cover badge:
<AssetUploader onAddFiles={upload} onReject={warn} maxItems={5} count={assets.length}>
{assets.map((a) => (
<AssetUploader.Item key={a.id} url={a.url} onRemove={() => remove(a.id)}>
{a.progress != null && <UploadRing value={a.progress} />}
</AssetUploader.Item>
))}
<AssetUploader.Add withRemaining />
</AssetUploader>
Table renders rows, nothing else. Columns are
TanStack Table v8 ColumnDefs — pinned to v8
because that's what @khaime-dev/ui uses, and v9 renamed the row-model API, so
column definitions would stop transferring. ColumnDef is re-exported for the
common case.
Paging isn't a table concern: the page is a slice of your own data, and
Pagination is a separate component that works for any paged list.
const rows = all.slice((page - 1) * perPage, page * perPage);
<Table columns={columns} data={rows} />
<Pagination page={page} total={Math.ceil(all.length / perPage)} onChange={setPage} />
Server-side paging is the same code with a request in place of slice.
bordered frames the table — bare for the frame alone, bordered={{ column: true }}
to add vertical rules.
sortable makes eligible headers clickable and wires aria-sort; a column opts
out with enableSorting: false. Own the state with sorting / onSortingChange
(plus manualSorting) when the server does the ordering.
For a hand-written table — a totals row, a colSpan group heading, an expandable
detail row — Table.Root / .Header / .Body / .Footer / .Row / .Head /
.Cell / .Caption are the same styled elements Table is built from, so both
forms render identically. bordered sits on Root and reaches rows via context.
Dialog is the one exception that keeps Radix's open / onOpenChange — it is
the Radix layer. Modal is where the opened / onClose house style lives, as a
preset over it.
<Dialog>
<Dialog.Target asChild><Button>Invite</Button></Dialog.Target>
<Dialog.Content withCloseButton={false}>
<Dialog.Header>
<Dialog.Title>Invite a teammate</Dialog.Title>
</Dialog.Header>
<Dialog.Footer>
<Dialog.Close asChild><Button>Done</Button></Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
Each just* app should call bootTheme({ keyPrefix: '<appname>' }) so theme state doesn't collide on shared origins. The ThemeToggle reads the prefix from the global window.__JUSTUI__ that bootTheme() stamps, so the same component works in every app without per-app config.
If you want themes to be shared across apps on the same origin (so picking Gruvbox in one carries to the other), use the default keyPrefix: 'justui' everywhere.
MIT
FAQs
Design system for the just* app family — theme tokens, twelve themes, a no-flash boot script, ~50 React components on Radix, Astro adapters, and a Tailwind v4 design config.
We found that @codellyson/justui demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.

Security News
Anthropic’s free OSS Scanner uses Mythos and other Claude models to find open source vulnerabilities and send maintainers reports with proposed fixes.

Security News
On Front Lines’ Early Adopters: CISO podcast, Andrew Becherer shares lessons from Datadog, weighs the long-term costs of AI-built tools, and explains how founders can actually gain a CISO’s trust.