• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
kamalnrf

kamalnrf

val-tool-playground-skill

Skill for building consistent tool vals with Pico CSS
Public
Like
val-tool-playground-skill
Home
Code
5
.well-known
1
references
1
README.md
SKILL.md
H
main.ts
Connections
Environment variables
Branches
1
Pull requests
Remixes
History
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.
Sign up now
Code
/
references
/
pico-ui-patterns.md
Code
/
references
/
pico-ui-patterns.md
Search
…
Viewing readonly version of main branch: v16
View latest version
pico-ui-patterns.md

Pico CSS UI Patterns Reference

Reference for building tool val UIs with Pico CSS v2. Read this when implementing the UI layer.

Base Template

<!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

Header

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>

Tabs (custom — Pico has no tab component)

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'); }); });

Input Patterns

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(); });

Output Patterns

Code/markdown output with copy button (custom — no Pico equivalent):

<div class="output-wrap"> <button class="copy-btn" data-tooltip="Copy to clipboard">&#x2398; 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); }

Pico Native Components — Don't Reinvent

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, .outline variants
  • 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-open on <html>
  • Progress — <progress> for determinate, <progress /> for spinner
  • Grid — .grid for auto-layout equal columns
  • Tables — wrap in <div class="overflow-auto"> for mobile
  • Typography — <small>, <mark>, <kbd>, <del>, blockquotes all styled

Footer

<footer> <a href="https://www.val.town/u/kamalnrf">kamalnrf</a> · <a href="https://github.com/...">source</a> </footer>

Common Mistakes

  1. Writing custom <details> CSS — Pico already styles it as an accordion
  2. Not using <fieldset role="group"> — more semantic than <div role="group"> for form inputs
  3. Missing <meta name="color-scheme"> — needed for scrollbars and native controls to respect dark mode
  4. Custom disabled button styles — Pico handles disabled natively
  5. Not using data-tooltip — free styled tooltips, better than title attributes
  6. Custom error colors — use var(--pico-del-color) for error text
  7. Custom footer borders — only add border-top inside <main>, not <article>
  8. Missing .overflow-auto — tables without this wrapper break on mobile
  9. Custom toggle switches — use <input type="checkbox" role="switch">
  10. Custom dropdown menus — use <details class="dropdown"> instead
  11. Not using <progress> — <progress /> gives an indeterminate spinner for free
  12. Custom nav layout — <nav> with <ul> auto-distributes horizontally
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
AboutAlternativesPricingBlogNewsletterCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.