matlab/matlab-uihtml-design
Generate beautiful, distinctive HTML/CSS/JS control panels for MATLAB uihtml components. 8 built-in styles (Clean, Material, Cosmic Dark, Neumorphic, Dashboard Light, Midnight Gradient, Minimal Mono, Warm Dark) plus custom aesthetics. Produces production-grade UI with sliders, buttons, toggles, and panels. Use when building visually polished MATLAB app UIs with uihtml.
npx skills add https://github.com/matlab/agent-skills-playground --skill matlab-uihtml-design
Generate production-grade HTML/CSS/JS control panels for MATLAB uihtml components with distinctive, configurable visual styles.
uihtmluihtml controlsBuilt-in styles are documented in references/design-styles.md. To apply a style:
references/styles/<name>.md start "" "<skill-directory>/assets/style-gallery.html" # Windows
open "<skill-directory>/assets/style-gallery.html" # macOS
Then ask: "I've opened the style gallery in your browser. Which style would you like? You can also describe a custom aesthetic."
The gallery shows interactive previews of all 8 built-in styles with a Dark/Light toggle. The available styles are:
| Style | Vibe |
|-------|------|
| Clean | Frosted glass, depth layers, spring animations |
| Material | Tonal surfaces, elevation, rounded shapes |
| Cosmic Dark | Deep space, neon glow, glassmorphism |
| Neumorphic Dark | Embossed/debossed, soft shadow pairs |
| Dashboard Light | White cards, indigo accent, data-dense |
| Midnight Gradient | Blue-to-purple gradients, luxury glow |
| Minimal Mono | Ultra-flat, pill buttons, single accent |
| Warm Dark | Amber/yellow accent, friendly, smart home |
If the user says "just pick one" or wants to move fast, default to Clean.
Each style reference follows the 9-section DESIGN.md format and provides complete specifications for colors, typography, components, motion, and guardrails.
Before generating code, commit to a clear aesthetic direction:
Execute the chosen direction with precision. Bold maximalism and refined minimalism both work, as long as the choice is intentional.
setup(htmlComponent) boilerplate; defer full MATLAB-side wiring to the matlab-uihtml-app-builder skillAll styles use CSS custom properties for theming. The base architecture:
:root {
/* Semantic colors, filled by chosen style */
--color-bg-primary: ...;
--color-bg-secondary: ...;
--color-bg-surface: ...;
--color-accent: ...;
--color-accent-hover: ...;
--color-text-primary: ...;
--color-text-secondary: ...;
--color-border: ...;
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
/* Typography */
--font-family: ...;
--font-size-sm: ...;
--font-size-md: ...;
--font-size-lg: ...;
/* Radii & Shadows */
--radius-sm: ...;
--radius-md: ...;
--shadow-sm: ...;
--shadow-md: ...;
/* Motion */
--transition-fast: ...;
--transition-med: ...;
}
Support both light and dark modes via prefers-color-scheme media query or a data-theme attribute on <html>:
@media (prefers-color-scheme: dark) { ... }
/* or */
[data-theme="dark"] { ... }
uihtml containers in MATLAB apps are often height-constrained (e.g., a narrow side panel). When applying any style:
overflow: hidden is set on the body, clipped controls are invisible and unusableWhen the control set genuinely can't be compacted further (4+ panels, mixed sliders + toggles + buttons), make the panel container scroll instead of clipping. Keep body non-scrolling so the background gradient stays anchored, and let the inner .app (or whatever you named the flex column) scroll:
html, body {
overflow: hidden; /* body never scrolls; background gradient stays put */
}
.app {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(255, 255, 255, 0.12) transparent; /* Firefox */
}
/* Chromium (uihtml uses CEF/Chromium) */
.app::-webkit-scrollbar { width: 6px; }
.app::-webkit-scrollbar-track { background: transparent; }
.app::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.10);
border-radius: 3px;
transition: background var(--t-fast);
}
.app::-webkit-scrollbar-thumb:hover { background: var(--accent-glow); }
Adapt the colors to the chosen palette:
| Style | Thumb default | Thumb hover |
|---|---|---|
| Dark styles (Cosmic Dark, Midnight, Neumorphic, Warm Dark, Minimal Mono) | rgba(255,255,255,0.10) | var(--accent-glow) |
| Light styles (Clean, Dashboard Light, Material light mode) | rgba(0,0,0,0.12) | rgba(0,0,0,0.22) or var(--accent) |
Keep the scrollbar 6px wide and the thumb under 20% alpha; anything heavier breaks the style. Don't show track borders or arrows.
These are cross-cutting rules that apply on top of whatever style is chosen. They exist to keep generated control panels from reading as "AI made that." A named built-in style that deliberately uses one of these moves as its identity (Cosmic Dark's neon glow, Midnight Gradient's blue-to-purple, Clean's glass) is intentional *voice* and is fine. The guardrails target the unconscious reflex of reaching for these on every panel.
font-variant-numeric: tabular-nums so the width doesn't jitter as digits change while dragging.@media (prefers-reduced-motion: reduce) fallback, typically a crossfade or an instant state change. Include this in the template's <style> block: @media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
uihtml (a headless Chromium/CEF render) a transition that never fires ships the panel blank. --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); /* smooth */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* snappier */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); /* decisive */
A subtle spring/compression on a control *press* is fine; a bouncy elastic entrance applied uniformly to every panel is a tell.
transform / opacity / filter, not layout properties (width, height, top, margin).uihtml panels frequently set overflow: hidden or overflow: auto (see Scrollable Panels above), a position: absolute menu inside them is cut off. The reliable fix across uihtml's embedded Chromium is position: fixed with coordinates computed from the trigger's getBoundingClientRect() (flip above / left when near a viewport edge). Native <dialog> + showModal() (top layer) also escapes clipping. The Popover API and CSS anchor positioning are cleaner but need a recent Chromium. Only rely on them if you've confirmed the installed MATLAB's uihtml renderer supports them; otherwise fall back to position: fixed.:focus-visible state for keyboard users: ≥3:1 against adjacent colors, 2–3px, offset *outside* the element. A box-shadow ring or outline + outline-offset both work; never ship outline: none with no replacement.999 / 9999.Every control needs its states designed, not just the default. Keyboard users never see :hover, so focus is separate from hover and is not optional.
| State | Applies to | Treatment |
|---|---|---|
| Default / hover / active | all controls | base; subtle lift or color shift on hover; press feedback (e.g. transform: scale(0.97)) on active |
| Focus | all controls | visible :focus-visible ring per the rule above |
| Disabled | buttons, inputs | reduced opacity, cursor: not-allowed, no hover response |
| Loading / busy | actions that call MATLAB | spinner, pulse, or a busy-styled button; a long MATLAB op (>~1s) should show progress and stay cancellable |
| Error / success | status feedback | color plus an icon or text message, never color alone |
Loading / error / success are usually driven from the MATLAB backend; see matlab-uihtml-app-builder for the event wiring (e.g. SimComplete / SimError, busy-button state machine).
If you're about to write any of these, restructure instead:
/* ❌ all of these render the same orange side-stripe */
.panel { border-left: 4px solid var(--accent); } /* the obvious one */
.panel { border-inline-start: 3px solid var(--accent); } /* logical-property alias */
.panel { box-shadow: inset 4px 0 0 var(--accent); } /* inset shadow, no `border-left` in sight */
.panel { border-width: 1px 1px 1px 4px; border-color: … var(--accent); } /* asymmetric border widths */
.panel::before { content: ''; position: absolute; left: 0; width: 4px; height: 100%; background: var(--accent); }
/* ✅ mark state another way */
.panel[data-state="active"] { border: 1px solid var(--accent); } /* full hairline border */
.panel[data-state="active"] { background: var(--accent-soft); } /* an ~8% accent wash, defined as a token */
.panel[data-state="active"] .title::before { content: '● '; color: var(--accent); } /* leading glyph */
Rule of thumb: any left/right border wider than 1px, or any accent-colored fill narrower than ~8px running the full height of one edge, is a side-stripe. Rewrite it.
background-clip: text over a gradient. Emphasize with weight, size, or a single solid color.box-shadow (blur ≥16px) on the same element. Pick one: a defined border, or a shadow at ≤8px blur.border-radius ≥32px on panels or inputs. Panels top out around 12–16px; full-pill radius is only for tags and buttons.Panel text is part of the design. Slop in a label or a status line reads as "AI made that" as loudly as a side-stripe does. These are engineer-facing MATLAB tools, so write like the product does: declarative, plain, and trusting the reader's competence.
MATLAB-driven status text (wired in matlab-uihtml-app-builder) follows the same rules: the string that MATLAB sends to a status line is microcopy too.
uihtml container sizes; use flexbox/grid with relative unitssetup(htmlComponent) function pattern:function setup(htmlComponent) {
window.htmlComponent = htmlComponent;
// Listen for events from MATLAB
htmlComponent.addEventListener("EventName", function(event) {
// Handle event.Data
});
// Send events to MATLAB
htmlComponent.sendEventToMATLAB("EventName", { key: value });
}
SetTheme event. The MATLAB side can detect the desktop theme and push it to the HTML component. The settings path is R2025a+, so wrap it in try/catch with a sensible default so the app still works on older releases:% Detect MATLAB desktop theme (R2025a+); fall back to a default on older releases
themeStr = 'dark'; % or 'light', whichever the style is designed for
try
s = settings;
themeStr = lower(char(s.matlab.appearance.MATLABTheme.ActiveValue));
catch
% settings.matlab.appearance not available on this release; keep the default
end
sendEventToHTMLSource(htmlComp, "SetTheme", struct("theme", themeStr));
The HTML setup() function already includes a SetTheme listener that sets data-theme on the root element, triggering the CSS variable swap.
-apple-system, 'Segoe UI', system-ui, sans-serif) is a legitimate, fast, offline-safe default. The built-in templates use one, and it's the right call for control panels. What reads as "AI made that" is reaching for a *distinctive-but-overused* display face for personality: Inter, Roboto, Geist, Space Grotesk, Plus Jakarta Sans, Fraunces (and plain Arial). Avoid those. If you pair fonts, pair on a real contrast axis (serif + sans); don't pair two similar sans-serifs. Also avoid purple-on-white cliches and cookie-cutter layouts; see the refuse-and-rewrite list in Design Guardrails above<a href="…" target="_blank" rel="noopener"> opens in the system default browser from uihtml, not inside the MATLAB figure. Useful for "view docs / view source" links in the app header. Style the link to fit the palette (accent color on hover, no underline by default): .external-link {
color: var(--text-secondary);
text-decoration: none;
transition: color var(--t-fast), text-shadow var(--t-fast);
}
.external-link:hover {
color: var(--accent-secondary);
text-shadow: 0 0 8px var(--accent-glow); /* dark styles */
}
Take matlab/matlab-uihtml-design from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.