Skip to content
Back to skills

Ui

ASecurity

you style the app or build its screens — Tailwind CSS from esm.sh without a build step, responsive layout, the accessibility minimum, forms and loading, empty and error states

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 29, 2026
ai-agentstypescriptgoreactapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add freema/drobek --skill ui --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ui?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ui
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/freema-drobek-drobek/badge)](https://www.skillsdirectory.com/skills/freema-drobek-drobek)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ui
description: you style the app or build its screens — Tailwind CSS from esm.sh without a build step, responsive layout, the accessibility minimum, forms and loading, empty and error states
---

# ui — styling and screens

## 1. When to use

Any visible UI. Two styling options, both without a build step: plain CSS
imported from `src/main.tsx` (bundled into `/main.css`), or Tailwind CSS v4's
browser build loaded from esm.sh in `index.html` (it scans the page's
classes at run time). Do not install `tailwindcss`/PostCSS or use
`cdn.tailwindcss.com` (the apps CSP blocks it).

## 2. Minimal working code

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Orders</title>
    <script type="module" src="https://esm.sh/@tailwindcss/browser@4.1.11"></script>
    <style type="text/tailwindcss">
      @theme { --color-brand: #0f766e; }
    </style>
    <link rel="stylesheet" href="/main.css" />
  </head>
  <body class="bg-slate-50 text-slate-900">
    <div id="root"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>
```

```tsx
// src/main.tsx
import { useEffect, useState, type FormEvent } from 'react';
import { createRoot } from 'react-dom/client';

type Load<T> = { state: 'loading' } | { state: 'error'; message: string } | { state: 'ready'; data: T };
type Order = { id: string; title: string };

function Orders() {
  const [orders, setOrders] = useState<Load<Order[]>>({ state: 'loading' });
  const [title, setTitle] = useState('');
  const [saving, setSaving] = useState(false);
  useEffect(() => {
    setTimeout(() => setOrders({ state: 'ready', data: [] }), 300); // replace with a drobek.data list()
  }, []);

  function add(e: FormEvent) {
    e.preventDefault();
    setSaving(true);
    setOrders((o) => (o.state === 'ready' ? { ...o, data: [...o.data, { id: crypto.randomUUID(), title }] } : o));
    setTitle('');
    setSaving(false);
  }

  return (
    <main className="mx-auto max-w-3xl p-4 sm:p-8">
      <h1 className="mb-6 text-2xl font-semibold">Orders</h1>
      <form onSubmit={add} className="mb-6 flex flex-col gap-2 sm:flex-row">
        <label htmlFor="title" className="sr-only">Order title</label>
        <input id="title" required value={title} onChange={(e) => setTitle(e.target.value)} placeholder="New order"
          className="flex-1 rounded-md border border-slate-300 px-3 py-2 focus:outline-2 focus:outline-brand" />
        <button disabled={saving} className="rounded-md bg-brand px-4 py-2 text-white disabled:opacity-50">
          {saving ? 'Saving…' : 'Add'}
        </button>
      </form>
      {orders.state === 'loading' && <p aria-busy="true" className="text-slate-500">Loading…</p>}
      {orders.state === 'error' && <p role="alert" className="text-red-700">{orders.message}</p>}
      {orders.state === 'ready' && orders.data.length === 0 && <p className="text-slate-500">No orders yet.</p>}
      {orders.state === 'ready' && (
        <ul className="grid gap-3 sm:grid-cols-2">
          {orders.data.map((o) => (
            <li key={o.id} className="rounded-lg bg-white p-4 shadow-sm">{o.title}</li>
          ))}
        </ul>
      )}
    </main>
  );
}

createRoot(document.getElementById('root')!).render(<Orders />);
```

## 3. API and types

- Tailwind v4 utilities as usual (`sm:` / `md:` / `lg:` breakpoints,
  `dark:`, `hover:`, `focus:`, `disabled:`); theme tokens in
  `<style type="text/tailwindcss">@theme { … }</style>` (`--color-brand` →
  `bg-brand`, `text-brand`, `outline-brand`); `@apply` works only there,
  NOT in `.css` files (esbuild does not run Tailwind).
- Keep critical layout in plain CSS too if a flash of unstyled content
  matters (Tailwind applies once its script ran).
- Async screens: model `loading | error | ready` (the `Load<T>` above); show
  an empty state; disable the submit button while saving.
- Platform UI: `<LoginGate>` (`skill_info('auth')`) renders a styled card
  in `div.drobek-login` (inline styles; wrap it to position it); `<Form>`
  (`skill_info('forms')`) takes `className` and your own styled inputs.
- Fonts: `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap">` is allowed.

## 4. Rules and limits

- CSP: `script-src` = the app + https://esm.sh (as `type="module"`),
  `style-src` = the app + inline + any https, `img-src` / `font-src` also
  `data:` and https, `connect-src` = the app + esm.sh. No `<iframe>` of the
  app in other sites.
- Responsive: the viewport `<meta>` in `index.html`; mobile first (base
  classes = phone, `sm:`+ for wider); no fixed widths wider than 100vw;
  tap targets ≥ 44 px.
- Accessibility minimum: `<html lang>`; one `<h1>`; every input has a
  `<label>` (or `aria-label`); buttons are `<button>`, links `<a href>`;
  images have `alt`; focus stays visible; status text in `role="status"`,
  errors in `role="alert"`, pending regions `aria-busy="true"`; contrast ≥
  4.5:1 (e.g. `text-slate-500` on white is the lightest body text).
- Tab and shared link: `index.html`'s `<head>` carries a `<meta name="description">` and an icon (`favicon.svg` +
  `<link rel="icon" href="/favicon.svg" type="image/svg+xml">`); the link-preview `og:*` tags: `skill_info('start')`.
- Forms: native validation (`required`, `type="email"`, `min`/`max`)
  first; show the server's message (`DrobekError.message`) next to the
  form; never clear the user's input on an error.

## 5. Errors → fix

| error | cause | fix |
|---|---|---|
| page unstyled | Tailwind script not `type="module"`, or not from esm.sh | use the `<script type="module" src="https://esm.sh/@tailwindcss/browser@4.1.11">` line |
| `unresolved_import` | `import 'tailwindcss'` in TypeScript or `@import "tailwindcss"` in a `.css` file | load the browser build in `index.html` instead |
| `@apply` styles missing | `@apply` / `@tailwind` in a `.css` file pass through esbuild untouched | move them into `<style type="text/tailwindcss">` |
| `build_error` | invalid CSS (`file:line`) | fix the rule |
| layout breaks on phones | missing viewport meta or fixed widths | add the meta; use `max-w-*` + `w-full` |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.