> Text
Themed terminal-style text with color, font, and CRT/LED display variants. Renders as any of several semantic tags via the tag prop.
PREVIEW
// primary color
// dim color
// danger color
// warn color
// success color
mono font
vt font
heading
<script>
import { Text } from 'mukade-ui';
</script>
<Text tag="h1" color="primary" glow>SYSTEM ONLINE</Text>
<Text color="dim" size="0.8rem">last sync: 04:12</Text>
<Text variant="crt">scanline text</Text> - --mukade-text-accent inherits. Setting it on an ancestor recolors every descendant <Text>.
- Because crt and glow derive from currentColor, the accent flows into the scanline and shadow automatically.
> Avatar
Displays a user's profile image with an optional online-status dot and name/subtitle block. Falls back to the first letter of name when no image is set.
PREVIEW
<script>
import { Avatar } from 'mukade-ui';
</script>
<Avatar profile="/me.png" name="KAEDE" sub="operator" online />
<!-- Custom status indicator via children -->
<Avatar name="K">
<span>●</span>
</Avatar> - The profile area keeps a 1 / 1 aspect ratio at any size.
- --mukade-avatar-accent inherits; scope it to a single avatar via style if needed.
> Badge
A small status indicator. Renders as a standalone tag, or as an overlay anchored to a corner of wrapped content.
PREVIEW
<script>
import { Badge } from 'mukade-ui';
</script>
<!-- Tag form -->
<Badge variant="success" label="ONLINE" />
<!-- Overlay form -->
<Badge count={12} max={9} position="top-right">
<Avatar name="K" />
</Badge> - --mukade-badge-accent inherits — setting it on an ancestor recolors all descendant badges.
- The overlay item has pointer-events: none and never intercepts clicks on wrapped content.
> Divider
A horizontal or vertical separator line, optionally with a centered label.
PREVIEW
<script>
import { Divider } from 'mukade-ui';
</script>
<Divider label="SECTION" />
<Divider orientation="vertical" weight="2px" /> - The vertical form uses writing-mode: vertical-lr for the label; it needs a parent with a defined height to stretch.
> Container
A minimal block wrapper that spans the full width of its parent and centers itself horizontally. Adds no padding or visual styling.
PREVIEW
// Container centers content (margin: 0 auto) and spans full width
<script>
import { Container } from 'mukade-ui';
</script>
<Container style="max-width: 60rem;">
<!-- page content -->
</Container> - Defaults to width: 100% with margin: 0 auto. Constrain it with max-width via style/class.
- Adds no padding or visual styling — it is purely structural.
> Stack
A flexbox layout primitive for arranging children in a row or column with gap, alignment, and wrapping control.
PREVIEW
// direction="row" (default)
// direction="column"
// justify="between"
<script>
import { Stack } from 'mukade-ui';
</script>
<Stack direction="column" gap="0.5rem" align="stretch">
<Button>[A]</Button>
<Button>[B]</Button>
</Stack> - align and justify use friendly keywords that map to the corresponding flexbox values.
> ScrollArea
A vertical overflow container with a custom terminal-style scrollbar and an optional sticky header that can react to scroll direction.
PREVIEW
// LOG
// line 1
// line 2
// line 3
// line 4
// line 5
// line 6
// line 7
// line 8
// line 9
// line 10
// line 11
// line 12
// line 13
// line 14
// line 15
// line 16
// line 17
// line 18
// line 19
// line 20
<script>
import { ScrollArea } from 'mukade-ui';
</script>
<ScrollArea maxHeight="20rem" variant="sticky">
{#snippet header()}
<Text>LOG</Text>
{/snippet}
<!-- long content -->
</ScrollArea> - The native scrollbar is hidden; a synthetic thumb tracks the viewport and updates via a ResizeObserver.
- hide retracts the header as you scroll down and restores it as you scroll up; natural lets it scroll away with the content.
- This very sidebar and the docs pane you're reading are each an independent ScrollArea — scroll one without moving the other.
> Drawer
An edge panel that anchors to any of the four sides. permanent sits in the layout as a flex sibling and reserves space; temporary opens as a modal overlay on top of the page.
PREVIEW
// variant="permanent"
content area
// variant="temporary"
<script>
import { Drawer, Button } from 'mukade-ui';
let open = $state(false);
</script>
<Stack direction="row">
<Drawer variant="permanent">
{#snippet children()}
<nav>…</nav>
{/snippet}
</Drawer>
<ScrollArea>…</ScrollArea>
</Stack>
<Button onclick={() => (open = true)}>[MENU]</Button>
<Drawer variant="temporary" direction="right" bind:open aria-label="Navigation">
{#snippet children()}
<nav>…</nav>
{/snippet}
</Drawer> - permanent renders an <aside>, which carries a complementary landmark so assistive technology can jump to it as a region. It is flex-shrink: 0, so it keeps its thickness when sibling content grows.
- temporary renders a <dialog> opened with showModal(). That gives focus trapping, Escape to close, restoration of focus on close, and top-layer stacking without any of it being hand-written.
- Give temporary an aria-label. A dialog without an accessible name is announced only as "dialog", with no indication of what it contains.
- Clicking the backdrop closes the panel. The check works because a click on the backdrop resolves to the <dialog> element itself, while a click on the content resolves to a descendant.
- open stays in sync when the user closes with Escape, because the dialog's close event writes back to it.
- top and bottom panels apply env(safe-area-inset-*) padding, so they clear the notch and home indicator on iOS.
- temporary sets overscroll-behavior: contain, so scrolling to the end of the panel does not start scrolling the page behind it.
- Sliding is skipped under prefers-reduced-motion; the panel simply appears.
- Switching between the two variants for narrow screens is left to the consumer — pass variant accordingly. The widget does not read the viewport itself.
> Panel
A framed card container with optional header/footer regions and a row of status dots. The staple building block for terminal-style windows.
PREVIEW
SYSTEM
// header + footer snippet
PROGRESS
// header with dots indicator
// soft-line variant
<script>
import { Panel } from 'mukade-ui';
</script>
<Panel width="20rem" dots={{ index: 2, max: 3 }}>
{#snippet header()}
<span>SESSION</span>
{/snippet}
<!-- body -->
{#snippet footer()}
<Button>[CONFIRM]</Button>
{/snippet}
</Panel> - dots only render when a header snippet is present.
- Header/footer tint blends --mukade-panel-accent into --mukade-panel-bg, so both hooks compose cleanly.
> Section
A semantic content block with an optional terminal-style title row, prefixed with a > marker.
PREVIEW
TITLE
// Section renders a semantic block with an optional title
<script>
import { Section } from 'mukade-ui';
</script>
<Section>
{#snippet title()}
<Text tag="h2">DIAGNOSTICS</Text>
{/snippet}
<!-- section body -->
</Section> - The > prefix bar uses --mukade-primary for its accent border and text.
> Button
A clickable button with five color variants and independent size/width control. Extends all native button element attributes.
PREVIEW
<script>
import { Button } from 'mukade-ui';
</script>
<Button variant="primary" onclick={submit}>[CONFIRM]</Button>
<Button variant="ghost" disabled>[CANCEL]</Button>
<Button width="100%" size="1.5rem">[BIG]</Button> - size controls font size (element scale); width controls horizontal length. They are separate axes.
- disabled dims the button and blocks the active-press transform.
- Every variant presses with a subtle scale(0.94) on :active.
> Checkbox
A boolean checkbox with an indeterminate state and optional label. Two-way bindable via checked.
PREVIEW
<script>
import { Checkbox } from 'mukade-ui';
let agreed = $state(false);
</script>
<Checkbox label="ACCEPT" bind:checked={agreed} />
<Checkbox label="PARTIAL" indeterminate /> - Keyboard focus is shown via :focus-visible on the hidden input, drawn as an outline on the styled box.
- size is remapped from the native size attribute (meaningless for checkboxes) to a CSS dimension.
> Input
A single-line text input with outlined, filled, or borderless variants. Two-way bindable via value.
PREVIEW
<script>
import { Input } from 'mukade-ui';
let name = $state('');
</script>
<Input placeholder="username" bind:value={name} />
<Input variant="filled" type="password" placeholder="password" /> - :focus brightens the border via --mukade-bright; :disabled mutes background and border.
> Radio
A single-choice input drawn in the terminal convention as ( ) and (X), so it stays distinguishable at a glance from Checkbox's square. Options form a group by sharing one name and one bound value.
PREVIEW
<script>
import { Radio } from 'mukade-ui';
let picked = $state('apple');
</script>
<Radio bind:group={picked} name="fruit" value="apple" label="APPLE" />
<Radio bind:group={picked} name="fruit" value="banana" label="BANANA" />
<Radio bind:group={picked} name="fruit" value="cherry" label="CHERRY" size="1.4rem" disabled /> - name is what makes the options a real group. Browsers use it to provide arrow-key movement between options and to submit the value inside a <form>. Give every option in a group the same name, and use a different one for each separate group on the page.
- name is required by the type, so TypeScript flags a missing one. Plain JavaScript callers get no such check, and an option without a name silently becomes a one-item group of its own.
- The (X) mark follows group: an option shows as selected when group equals its value. Choosing an option writes its value back through the binding.
- The native input's checked state is only set by user interaction, not by group. Clicking an option keeps the two in step. But the initial group value, and any later change made to it from code, move the (X) mark without checking the native input. Until the user clicks, that selection is not submitted with a form, assistive technology announces the option as not checked, and Tab enters the group at the first option rather than the selected one.
- An onchange handler you pass still runs. It fires after the selection has been recorded, so reading group inside it gives the new value.
- The whole row is a <label>, so the label text is part of the clickable area, not just the control.
- The native <input> is hidden rather than removed, which preserves keyboard focus and form semantics. The focus ring is drawn on the styled control, since the element that actually receives focus is invisible.
- On coarse pointers the row grows to a 44px minimum touch target. The control's visual size is unchanged.
- Like a native radio, an already-selected option cannot be cleared by clicking it again. Provide an explicit "none" option if clearing is needed.
- Wrap a group in a <fieldset> with a <legend> so assistive technology can announce what the choice is about.
> Textarea
A multi-line text input with configurable size, an optional resize handle, and a custom scrollbar.
PREVIEW
<script>
import { Textarea } from 'mukade-ui';
let note = $state('');
</script>
<Textarea placeholder="log entry..." bind:value={note} width="24rem" height="8rem" resizing /> - resizing defaults to false (fixed size); set it to allow manual resizing.
> Toggle
A switch-style boolean input with an optional label. Two-way bindable via checked.
PREVIEW
<script>
import { Toggle } from 'mukade-ui';
let power = $state(true);
</script>
<Toggle label="POWER" bind:checked={power} />
<Toggle label="SCALED" size="1.5rem" /> - Keyboard focus is shown via :focus-visible on the hidden input, drawn as an outline on the track.
> TextField
A labeled text input with a floating, terminal-style animated label. Comes in outlined and filled variants.
PREVIEW
<script>
import { TextField } from 'mukade-ui';
let email = $state('');
</script>
<TextField label="EMAIL" type="email" bind:value={email} name="email" required />
<TextField variant="filled" label="PASSWORD" type="password" width="20rem" /> - Supplying id is honored and kept in sync with the label's for; otherwise a stable SSR-safe id is generated.
- User onfocus/onblur handlers are preserved — internal focus tracking composes with, not replaces, yours.
- The label "types" one character at a time when it floats; unlabeled fields show the placeholder immediately.
> Select
A dropdown selector. Compose it with SelectOption children. Closes on outside click or the Escape key.
PREVIEW
<script>
import { Select, SelectOption } from 'mukade-ui';
let region = $state('');
</script>
<Select bind:selected={region} placeholder="region">
<SelectOption key="kr" label="KOREA" />
<SelectOption key="jp" label="JAPAN" />
</Select> - Provides context to child SelectOptions; use them rather than raw options.
- Outside-click and Escape listeners are attached only while open, and removed on close.
- Raise --mukade-select-z-index when the dropdown must sit above other stacked layers (e.g. inside a modal).
- SelectOption is required inside a Select — it registers its key/label on mount and follows the parent's accent/bg colors.
- SelectOption's label is only registered with the parent when both key and label are present; without a label, the trigger displays the raw key. Long labels are clipped with an ellipsis so the dropdown keeps one row per option.
- Each SelectOption renders a real <button> reachable by Tab and activated with Enter or Space, but arrow-key movement between options is not implemented. Selecting one always closes the dropdown — there is no multi-select mode.
> Alert
A status message box with an icon, title, and optional body. Four status variants convey severity.
PREVIEW
<script>
import { Alert } from 'mukade-ui';
</script>
<Alert variant="success" title="SYNC COMPLETE" />
<Alert variant="danger" title="CONNECTION LOST">
retrying in 5s...
</Alert> - Each variant maps to a semantic status color; info/success render with role="status", warn/danger with role="alert".
- danger uses a filled scanline background and bold text so a lone danger alert reads as severe.
- An auto-selected icon (i, ✓, ⚠, !) precedes the title per variant.
> Progress
A determinate progress indicator with two visual forms: a continuous fill bar with a glowing leading edge, or a row of discrete packet blocks that light up as the transfer advances.
PREVIEW
<script>
import { Progress } from 'mukade-ui';
</script>
<Progress value={42} aria-label="Uploading" />
<Progress
variant="packets"
value={70}
count={12}
aria-label="Transmitting"
aria-valuetext="7 of 12 packets"
/> - role="progressbar" sits on the track rather than the outer wrapper, together with aria-valuemin, aria-valuemax, and aria-valuenow.
- aria-valuenow reports the clamped value, so it always stays within the declared range even when value is out of bounds.
- value and count are coerced before use. Non-numeric input, NaN, and null all resolve to 0; Infinity clamps to the maximum.
- count is truncated to an integer and capped at 100, which keeps a mistaken large number from rendering an unbounded number of blocks.
- In the packets variant each block is one packet. Delivered packets stay lit, and the packet currently in flight pulses between the idle and delivered colors.
- The fill width and packet colors transition over 0.5s; the in-flight pulse repeats on a 1s cycle.
> Skeleton
A placeholder for content that has not arrived yet, drawn as a CRT scanline texture. Three shapes (box, circle, line) cover the usual cases, and an optional effect animates the texture.
PREVIEW
// variant="line"
// variant="circle"
// variant="box"
// effect
none
crt
rain
wave
<script>
import { Skeleton } from 'mukade-ui';
</script>
<!-- a block of loading text -->
<Skeleton variant="line" width="100%" />
<Skeleton variant="line" width="80%" />
<Skeleton variant="line" width="45%" />
<!-- an avatar and a card, with motion -->
<Skeleton variant="circle" width="3rem" effect="crt" />
<Skeleton variant="box" width="100%" height="8rem" radius="0.25rem" effect="wave" /> - Use box for cards and images, circle for avatars, and line for text. circle derives its height from width through aspect-ratio: 1 / 1, so height and radius do nothing there; its corners are always fully rounded.
- width is required on every variant. height applies to box and line only, and leaving radius unset gives square corners.
- effect selects the resting texture as well as the motion, not the motion alone. none and crt carry horizontal scanlines, rain a 45° diagonal, and wave vertical bars. Switching effect therefore changes how the placeholder looks while still, not only while animating.
- crt steps a scanline down the shape, rain runs the same idea along the diagonal, and wave sweeps a brighter band across it from left to right.
- The element is aria-hidden. A placeholder shape carries nothing a screen reader can act on, so announce the loading state on the surrounding region instead — with Spinner's role="status", or aria-busy on the container that is being filled.
- Every effect respects prefers-reduced-motion and stops animating. crt and rain settle into a static texture; wave leaves its band resting against the left edge.
- The root clips its overflow, so the moving layers stay inside the shape, including inside the circle.
- Skeleton renders an empty element and takes no content. An optional children is inherited from HTMLAttributes, but nothing is rendered from it.
- Sizing the placeholder close to the real content is what makes it useful — a skeleton of the wrong size moves the layout when the content replaces it.
> Spinner
An indeterminate loading indicator that cycles through -, \, |, and /. It covers the case Progress cannot: work whose completion ratio is unknown.
PREVIEW
// awaiting response...
<script>
import { Spinner } from 'mukade-ui';
</script>
<Spinner />
<Spinner size="2rem" duration={120} aria-label="Uploading" /> - The root is a status live region, so assistive technology announces the spinner once when it appears rather than on every frame.
- The cycling glyph itself is aria-hidden. Without this, a screen reader would read out -, \, |, / several times a second — noise rather than information.
- Under prefers-reduced-motion the cycle never starts and a single glyph is shown. Because the animation is driven by a timer rather than CSS, this is checked in JavaScript at mount.
- The timer is cleared when the component is destroyed.
- Server rendering emits the first glyph as static text; cycling begins after hydration.
- Prefer one spinner over many. A separate spinner per row of a long list creates one timer per row, and a single indicator usually communicates the same thing more clearly.
> Table
A data table. Provide column headers via columns, and rows by composing TableRow / TableCell children.
PREVIEW
| id | name | status |
|---|---|---|
| 01 | mukade-core | online |
| 02 | mukade-ui | online |
| 03 | mukade-net | idle |
<script>
import { Table, TableRow, TableCell } from 'mukade-ui';
</script>
<Table columns={['ID', 'NAME', 'STATUS']} width="30rem">
<TableRow>
<TableCell>01</TableCell>
<TableCell>KAEDE</TableCell>
<TableCell>ONLINE</TableCell>
</TableRow>
</Table> - When width is set, the table clips overflow and uses a fixed layout; otherwise it scrolls horizontally up to --mukade-table-max-size.
- The native scrollbar is hidden for a cleaner terminal look.
- TableRow renders a bottom border between rows; the last row's border is removed automatically. There's no built-in hover, selected, or striped state — add those through class or style.
- TableCell overflow is clipped with an ellipsis and white-space: nowrap. Its own width prop controls where truncation happens, but is only fully honored when the parent Table also has a width — otherwise the table uses an auto layout and treats cell widths as suggestions.
- TableCell and TableRow forward HTMLAttributes<HTMLDivElement> rather than cell/row-specific types, so TypeScript rejects colspan/rowspan/headers on TableCell, and an onclick handler on TableRow sees its currentTarget typed as a div even though both render real <td>/<tr> elements.
- TableCell and TableRow expose no CSS custom properties of their own — text reads --mukade-text directly and the row divider reads --mukade-border-soft directly, both inherited from the enclosing Table.