Public
Likeval-tool-playground-skill
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v22View latest version
Reference for building tool val UIs with Pico CSS v2. Read this when implementing the UI layer.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="color-scheme" content="light dark"> <title>Tool Name</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> <style> :root { --pico-font-size: 16px; } .container { max-width: 900px; } </style> </head> <body> <main class="container">...</main> </body> </html>
Key rules:
- System theme only — never force light or dark. Include
<meta name="color-scheme" content="light dark">. - No external images or favicons — use emoji or inline SVG
- No frameworks (React, etc.) for simple tools — vanilla HTML/CSS/JS
Use <hgroup> — Pico automatically mutes the subtitle:
<hgroup> <h1>📄 Tool Name</h1> <p>What it does. Link to <a href="#">underlying library</a> if wrapping one.</p> </hgroup>
This is the one pattern we always write custom CSS for. Use ARIA roles:
<div role="tablist"> <button role="tab" aria-selected="true" aria-controls="tab-a">Tab A</button> <button role="tab" aria-selected="false" aria-controls="tab-b">Tab B</button> </div> <div role="tabpanel" id="tab-a" class="active">...</div> <div role="tabpanel" id="tab-b">...</div>
[role="tablist"] { border-bottom: 1px solid var(--pico-muted-border-color); margin-bottom: 1.5rem; }
[role="tab"] {
background: none; border: none;
border-bottom: 3px solid transparent; border-radius: 0;
padding: 0.75rem 1.5rem; margin: 0;
color: var(--pico-muted-color); font-weight: 500; cursor: pointer;
}
[role="tab"][aria-selected="true"] { color: var(--pico-primary); border-bottom-color: var(--pico-primary); }
[role="tab"]:hover { color: var(--pico-primary); }
[role="tabpanel"] { display: none; }
[role="tabpanel"].active { display: block; }
document.querySelectorAll('[role="tab"]').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('[role="tab"]').forEach(t => t.setAttribute('aria-selected', 'false'));
document.querySelectorAll('[role="tabpanel"]').forEach(p => p.classList.remove('active'));
tab.setAttribute('aria-selected', 'true');
document.getElementById(tab.getAttribute('aria-controls')).classList.add('active');
});
});
URL input with action button — use Pico's fieldset group:
<fieldset role="group"> <input type="url" id="urlInput" placeholder="https://example.com" autofocus> <button id="fetchBtn" aria-busy="false">Extract</button> </fieldset>
Loading state — Pico's aria-busy adds a spinner to buttons:
function setLoading(btn, loading) {
btn.setAttribute('aria-busy', loading);
btn.disabled = loading;
}
Enter key to submit:
urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') fetchBtn.click(); });
Code/markdown output with copy button (custom — no Pico equivalent):
<div class="output-wrap"> <button class="copy-btn" data-tooltip="Copy to clipboard">⎘ Copy</button> <pre class="result"><code id="output"></code></pre> </div>
.output-wrap { position: relative; }
.copy-btn {
position: absolute; top: 0.5rem; right: 0.5rem;
background: var(--pico-secondary-background); border: 1px solid var(--pico-muted-border-color);
border-radius: 4px; padding: 0.3rem 0.6rem; font-size: 0.8rem; z-index: 1; cursor: pointer;
}
pre.result {
max-height: 70vh; overflow: auto; white-space: pre-wrap; word-break: break-word;
padding: 1rem; padding-top: 2.5rem; margin: 0; font-size: 0.85rem;
}
Metadata bar (custom — no Pico equivalent):
.meta { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; font-size: 0.8rem; color: var(--pico-muted-color); }
Before writing custom CSS, check if Pico already handles it:
- Cards —
<article>with<header>and<footer> - Accordion —
<details>+<summary>(Pico styles it natively) - Loading —
aria-busy="true"on any element - Buttons —
.secondary,.contrast,.outlinevariants - Button groups —
role="group"wrapper,aria-current="true"for active - Tooltips —
data-tooltip="text"on any element, pure CSS - Validation —
aria-invalid="true|false"on inputs,<small>helper text inherits color - Switch toggle —
<input type="checkbox" role="switch"> - Dropdown —
<details class="dropdown">+<summary>+<ul> - Nav —
<nav>with<ul>auto-distributes horizontally - Modal —
<dialog>+<article>,.modal-is-openon<html> - Progress —
<progress>for determinate,<progress />for spinner - Grid —
.gridfor auto-layout equal columns - Tables — wrap in
<div class="overflow-auto">for mobile - Typography —
<small>,<mark>,<kbd>,<del>, blockquotes all styled
<footer> <a href="https://www.val.town/u/kamalnrf">kamalnrf</a> · <a href="https://github.com/...">source</a> </footer>
- Writing custom
<details>CSS — Pico already styles it as an accordion - Not using
<fieldset role="group">— more semantic than<div role="group">for form inputs - Missing
<meta name="color-scheme">— needed for scrollbars and native controls to respect dark mode - Custom disabled button styles — Pico handles
disablednatively - Not using
data-tooltip— free styled tooltips, better thantitleattributes - Custom error colors — use
var(--pico-del-color)for error text - Custom footer borders — only add border-top inside
<main>, not<article> - Missing
.overflow-auto— tables without this wrapper break on mobile - Custom toggle switches — use
<input type="checkbox" role="switch"> - Custom dropdown menus — use
<details class="dropdown">instead - Not using
<progress>—<progress />gives an indeterminate spinner for free - Custom nav layout —
<nav>with<ul>auto-distributes horizontally