Skill: Front-End Designer (Vanilla CSS + Tailwind + Vanilla JS)
This skill focuses on building clean, maintainable, accessible UI using:
- •Tailwind CSS for fast, consistent styling (spacing, typography, colors, states)
- •Vanilla CSS for higher-level layout and patterns (Flex/Grid), custom components, and edge cases
- •Vanilla JS for UI behavior (event delegation, state-driven rendering, progressive enhancement)
The goal is to keep UI code clearly structured, well commented, and segmented by responsibility.
What “Good” Looks Like
Outcomes
- •The UI remains readable in light/dark mode.
- •Layouts are responsive and don’t rely on “magic numbers.”
- •Interactive elements work with keyboard + screen readers.
- •Styling is consistent: no ad-hoc one-off class soup or scattered CSS overrides.
- •JavaScript is predictable: clear state, clear render/update boundaries.
Non-goals
- •Adding new frameworks (React/Vue/etc.).
- •Introducing complex build tooling beyond the existing Tailwind pipeline.
Conventions (DailiesApp)
Use Tailwind for
- •Spacing, typography, colors, shadows, borders, hover/focus states
- •Responsive variants (e.g.,
sm:,md:) - •Dark mode variants (e.g.,
dark:)
Use vanilla CSS for
- •Layout systems (Flex/Grid) when structure matters and classes would become noisy
- •Reusable “component shells” (e.g., task row, modal, toast) where semantic naming helps
- •Complex states/animations or cross-cutting rules
Use vanilla JS for
- •Event handling, toggles, modals, timers, and DOM updates
- •Event delegation for lists (tasks, backups, history)
- •Minimal, scoped DOM queries and predictable update functions
File Touchpoints
- •
index.html: semantic structure, ARIA, data-attributes/hooks - •
app.js: UI logic, event listeners, rendering, IndexedDB interactions - •
styles.css/custom.css: custom CSS for layout/components not covered by Tailwind - •
tailwind.config.js: tokens, dark mode strategy, theme extensions
Structure Your UI Code (Segmentation Rules)
HTML
Prefer semantic containers:
- •
<main>for primary content - •
<section>for dashboard areas - •
<aside>for secondary panels - •
<button>for actions (not clickable<div>)
Add stable JS hooks using data-* attributes instead of brittle selectors:
html
<!-- Hooks are stable; classes can change freely --> <button type="button" data-action="toggle-theme" class="inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium bg-zinc-900 text-white hover:bg-zinc-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 dark:bg-zinc-100 dark:text-zinc-900"> Toggle theme </button>
CSS
Segment by responsibility (recommended ordering inside styles.css / custom.css):
- •Tokens (CSS variables)
- •Layout primitives (Flex/Grid helpers)
- •Components (task row, modal, toast)
- •Utilities (rare, app-specific)
- •Overrides (last resort; keep tiny)
JavaScript
Segment by responsibility:
- •State (minimal global state)
- •DOM hooks (one place for queries)
- •Render/update functions (idempotent when possible)
- •Event handlers (thin; call into render/update)
- •Integration (IndexedDB, charts, cloud sync)
Layout Skill: Flex + Grid (Vanilla CSS)
Flex: rows, alignment, “media objects”
Use Flex when you’re aligning items on one axis (toolbars, list rows, header actions).
css
/* ========== Layout Primitives: Flex ========== */
.l-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.l-row--between {
justify-content: space-between;
}
.l-row__grow {
flex: 1 1 auto;
min-width: 0; /* prevents overflow in flex rows */
}
Grid: dashboards, card layouts, consistent spacing
Use Grid when you want two-dimensional layout with predictable tracks.
css
/* ========== Layout Primitives: Grid ========== */
.l-grid {
display: grid;
gap: 1rem;
}
/* Responsive grid without Tailwind class noise */
.l-grid--cards {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
align-items: start;
}
Prefer CSS variables for theme-friendly values
css
/* ========== Tokens ========== */
:root {
--surface: #ffffff;
--surface-muted: #f4f4f5;
--text: #18181b;
--border: #e4e4e7;
--focus: #3b82f6;
}
.dark {
--surface: #09090b;
--surface-muted: #18181b;
--text: #fafafa;
--border: #27272a;
--focus: #60a5fa;
}
Styling Skill: Tailwind + Vanilla CSS (Hybrid Pattern)
Rule of thumb
- •If it’s local and simple, use Tailwind utilities in the markup.
- •If it’s structural/reused, create a named class in CSS and keep Tailwind for tokens/states.
Example: Task row shell in vanilla CSS + Tailwind tokens
html
<li class="taskRow" data-task-id="123"> <div class="taskRow__main"> <label class="taskRow__title"> <input type="checkbox" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-900" data-action="toggle-task" /> <span class="taskRow__titleText">Morning workout</span> </label> </div> <div class="taskRow__actions"> <button type="button" class="rounded-md px-2 py-1 text-xs font-medium bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" data-action="start-timer"> Start </button> <button type="button" class="rounded-md px-2 py-1 text-xs font-medium text-red-700 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-950/40" data-action="delete-task"> Delete </button> </div> </li>
css
/* ========== Components: Task Row ========== */
.taskRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 0.75rem;
background: var(--surface);
}
.taskRow__main {
min-width: 0;
}
.taskRow__title {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
}
.taskRow__titleText {
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.taskRow__actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 0 0 auto;
}
/* Explicit focus style for non-Tailwind elements */
.taskRow :focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 35%, transparent);
}
Behavior Skill: Vanilla JS (Clear + Maintainable)
Principles
- •Event delegation for lists (one listener on the container).
- •Data attributes for actions:
data-action="...". - •One-way UI updates: state changes then render.
- •Prefer small, named functions over inline anonymous handlers.
Example: Delegated click handling for task list
js
// ========== DOM Hooks ==========
const dom = {
taskList: document.querySelector('[data-role="task-list"]'),
};
// ========== Event Wiring ==========
const setupTaskListEvents = () => {
if (!dom.taskList) return;
dom.taskList.addEventListener('click', (event) => {
const actionEl = event.target.closest('[data-action]');
if (!actionEl) return;
const action = actionEl.dataset.action;
const row = actionEl.closest('[data-task-id]');
const taskId = row ? Number(row.dataset.taskId) : null;
switch (action) {
case 'start-timer':
if (taskId == null) return;
startTimerForTask(taskId);
break;
case 'delete-task':
if (taskId == null) return;
confirmDeleteTask(taskId);
break;
default:
// Keep a default branch so unknown actions fail safely.
console.warn('Unknown action:', action);
}
});
};
Example: Render function boundaries
js
// Rendering should be idempotent: given the same state, produce the same UI.
const renderTaskRow = (task) => {
return `
<li class="taskRow" data-task-id="${task.id}">
<div class="taskRow__main">
<label class="taskRow__title">
<input type="checkbox" data-action="toggle-task" ${task.status ? 'checked' : ''} />
<span class="taskRow__titleText">${escapeHtml(task.title)}</span>
</label>
</div>
<div class="taskRow__actions">
<button type="button" data-action="start-timer">Start</button>
<button type="button" data-action="delete-task">Delete</button>
</div>
</li>
`;
};
// Simple XSS-safe escape for user text.
const escapeHtml = (value) => {
const div = document.createElement('div');
div.textContent = String(value);
return div.innerHTML;
};
Accessibility Baseline (Required)
- •All interactive elements are reachable by keyboard.
- •Visible focus (
:focus-visible) is never removed without replacement. - •Buttons have clear names (text or
aria-label). - •Modals:
- •Use
role="dialog"andaria-modal="true" - •Focus moves into modal on open; returns to trigger on close
- •Escape closes the modal
- •Use
Modal skeleton
html
<div class="fixed inset-0 hidden" data-role="modal" aria-hidden="true"> <!-- Backdrop --> <div class="absolute inset-0 bg-black/50" data-action="close-modal"></div> <!-- Dialog --> <div class="absolute left-1/2 top-1/2 w-[min(92vw,520px)] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-white p-4 shadow-xl dark:bg-zinc-900" role="dialog" aria-modal="true" aria-labelledby="modal-title"> <div class="l-row l-row--between"> <h2 id="modal-title" class="text-base font-semibold">Modal title</h2> <button type="button" data-action="close-modal" aria-label="Close"> ✕ </button> </div> <div class="mt-3"> <!-- content --> </div> </div> </div>
Responsive + Dark Mode Rules
- •Design mobile-first: base styles for small screens, then add
sm:/md:enhancements. - •Don’t hardcode colors in CSS unless they are derived from variables.
- •Dark mode must remain readable (no low-contrast text, no invisible borders).
Performance & Maintainability
- •Avoid repeated DOM querying inside loops; cache selectors.
- •Prefer
DocumentFragmentorinnerHTMLonce per render, not per item. - •Destroy and recreate Chart.js instances when needed (avoid leaks).
- •Keep CSS selectors shallow; avoid overly specific chains.
Review Checklist (Use in PRs)
Layout & styling
- •Uses Flex/Grid intentionally (not mixed randomly).
- •Tailwind utilities are readable and consistent (no duplicated/conflicting classes).
- •Custom CSS is segmented: tokens → layout → components → utilities → overrides.
JS behavior
- •Uses
data-*hooks and event delegation for dynamic lists. - •Clear separation between state update and rendering.
- •User-provided text is escaped or inserted via
textContent.
A11y
- •Keyboard navigation works.
- •Visible focus exists on all interactive elements.
- •Modal interactions follow baseline rules.
QA (manual)
- •Test in light + dark.
- •Test at narrow width (mobile) and desktop.
- •Verify empty states (no tasks, no time entries, no backups).
Starter Template (Copy/Paste)
Use this section when creating a new UI piece.
1) Markup (semantic + hooks)
html
<section class="l-grid l-grid--cards" aria-label="[Feature name]"> <div class="rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900"> <header class="l-row l-row--between"> <h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">[Title]</h3> <button type="button" data-action="[action]" class="text-sm underline">[Action]</button> </header> <div class="mt-3" data-role="[feature-body]"></div> </div> </section>
2) CSS (component shell)
css
/* ========== Components: [Feature] ========== */
.feature {
/* Keep this as the structural shell; prefer Tailwind for colors/spacing if local */
display: grid;
gap: 0.75rem;
}
3) JS (events + render)
js
// ========== [Feature] ==========
const setupFeature = () => {
const root = document.querySelector('[data-role="[feature-body]"]');
if (!root) return;
// Render once on init
renderFeature(root);
// Optional events
root.addEventListener('click', (event) => {
const actionEl = event.target.closest('[data-action]');
if (!actionEl) return;
// Handle actions
});
};
const renderFeature = (root) => {
// Keep it deterministic: compute UI from state, then update DOM.
root.innerHTML = '<p class="text-sm text-zinc-600 dark:text-zinc-300">[Empty state]</p>';
};