agentsclimarketplace

React styling

Skill jaballer/react-claude-skills/react-styling

21 composable Claude Code skills for senior React + UX engineering — component design, a11y, performance, forms, routing, marketing & docs patterns, testing, and interview craft.

Install
npx -y skills add jaballer/react-claude-skills --skill react-styling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

React styling, design tokens, responsive layout, and motion — Tailwind, CSS Modules, CSS-in-JS, design tokens (CSS custom properties), breakpoints, container queries, CSS animations, Framer Motion, dark mode. Use when styling React components, implementing responsive design, building from Figma, adding animations, or setting up a design token system. Triggers on "Tailwind", "CSS", "styling", "responsive", "animation", "motion", "design tokens", "dark mode".

SKILL.md

16.6 KB, ~4.7k tokens by cl100k_base, as published. Nobody here has run it

react-styling

How a Senior UX Engineer styles React: design-token-driven, responsive by default, motion that respects users, and zero magic numbers.

The "no magic numbers" rule

The fastest senior-vs-mid signal in any review:

// Mid
<div className="mt-[17px] text-[#3a3a3a]">…</div>

// Senior
<div className="mt-4 text-neutral-800">…</div>

Every spacing, color, type-size, border-radius, and shadow value should be a token. If you find yourself typing 17px or #3a3a3a, stop. Either there's an existing token close enough, or there's a missing token you should add.

Design tokens via CSS custom properties

This works in any stack (Next, Vite, Astro). Define once, use everywhere:

/* tokens.css */
:root {
  /* Color — semantic, not literal */
  --color-bg: hsl(0 0% 100%);
  --color-fg: hsl(0 0% 10%);
  --color-fg-muted: hsl(0 0% 40%);
  --color-brand: hsl(173 80% 40%);
  --color-brand-hover: hsl(173 80% 35%);
  --color-border: hsl(0 0% 90%);
  --color-danger: hsl(0 70% 50%);

  /* Space scale (8px-based) */
  --space-1: 0.25rem;  /* 4 */
  --space-2: 0.5rem;   /* 8 */
  --space-3: 0.75rem;  /* 12 */
  --space-4: 1rem;     /* 16 */
  --space-6: 1.5rem;   /* 24 */
  --space-8: 2rem;     /* 32 */
  --space-12: 3rem;    /* 48 */
  --space-16: 4rem;    /* 64 */

  /* Type scale */
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.5rem;
  --font-size-2xl: 2rem;
  --font-size-3xl: 3rem;

  /* Radii */
  --radius-sm: 0.25rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-full: 9999px;

  /* Shadows */
  --shadow-sm: 0 1px 2px hsl(0 0% 0% / 0.06);
  --shadow-md: 0 4px 12px hsl(0 0% 0% / 0.08);
  --shadow-lg: 0 12px 32px hsl(0 0% 0% / 0.12);

  /* Motion */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --duration-fast: 150ms;
  --duration-base: 250ms;
  --duration-slow: 400ms;
}

[data-theme="dark"] {
  --color-bg: hsl(0 0% 7%);
  --color-fg: hsl(0 0% 96%);
  --color-fg-muted: hsl(0 0% 65%);
  --color-border: hsl(0 0% 18%);
}

Why CSS custom properties beat JS-only tokens: they cascade, they animate, they switch theme without re-rendering React, and they work in <style> attributes.

Tailwind: the senior playbook

Configure tokens in tailwind.config.ts

import type { Config } from 'tailwindcss';

export default {
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
  theme: {
    extend: {
      colors: {
        bg: 'hsl(var(--color-bg) / <alpha-value>)',
        fg: 'hsl(var(--color-fg) / <alpha-value>)',
        brand: {
          DEFAULT: 'hsl(var(--color-brand) / <alpha-value>)',
          hover: 'hsl(var(--color-brand-hover) / <alpha-value>)',
        },
      },
      fontFamily: {
        sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
      },
    },
  },
} satisfies Config;

This pattern (Tailwind classes → CSS variables → tokens) is what Radix UI, shadcn/ui, and every modern design system uses. Theme switching is just changing the variable values.

Tailwind patterns to use

  • cn helper for conditional classes:

    import { clsx, type ClassValue } from 'clsx';
    import { twMerge } from 'tailwind-merge';
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs));
    }
    
    <button className={cn('btn', variant === 'primary' && 'btn-primary', className)} />
    
  • cva (class-variance-authority) for variant components:

    import { cva } from 'class-variance-authority';
    const button = cva('inline-flex items-center justify-center font-medium rounded-md transition', {
      variants: {
        variant: {
          primary: 'bg-brand text-white hover:bg-brand-hover',
          ghost: 'bg-transparent text-fg hover:bg-fg/5',
        },
        size: {
          sm: 'h-8 px-3 text-sm',
          md: 'h-10 px-4 text-base',
        },
      },
      defaultVariants: { variant: 'primary', size: 'md' },
    });
    
  • Arbitrary values sparingly — if you need mt-[17px], you probably need a missing token. Use mt-[var(--specific-thing)] if it's a one-off architectural decision.

Tailwind smells

  • Long, unreadable class strings → extract a component or use cva
  • Inline conditional with ? chains → use cn helper
  • !important (!) for anything outside of utility overrides → fix specificity instead
  • Inline arbitrary colors → add a token

CSS Modules (alternative when Tailwind isn't on the table)

/* Button.module.css */
.button {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-4);
  background: var(--color-brand);
  color: white;
  border-radius: var(--radius-md);
  transition: background var(--duration-fast) var(--ease);
}
.button:hover { background: var(--color-brand-hover); }
.button:focus-visible {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.ghost {
  background: transparent;
  color: var(--color-fg);
}
.ghost:hover { background: hsl(var(--color-fg) / 0.06); }
import s from './Button.module.css';
import { cn } from '@/lib/cn';

<button className={cn(s.button, variant === 'ghost' && s.ghost)} />

Responsive: mobile-first by default

// Tailwind — mobile-first, breakpoint utilities only ADD constraints upward
<div className="
  grid
  grid-cols-1 gap-4 p-4
  md:grid-cols-2 md:gap-6 md:p-8
  lg:grid-cols-3
">
/* Plain CSS — mobile first */
.grid { display: grid; grid-template-columns: 1fr; gap: var(--space-4); }
@media (min-width: 768px) {
  .grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-6); }
}
@media (min-width: 1024px) {
  .grid { grid-template-columns: repeat(3, 1fr); }
}

Common breakpoints (Tailwind defaults are sensible):

  • sm 640px — phone landscape
  • md 768px — tablet
  • lg 1024px — small laptop
  • xl 1280px — desktop
  • 2xl 1536px — large desktop

Test at 320px wide. If it breaks there, it's not responsive.

Container queries (use when the layout depends on the container, not the viewport)

.card-container { container-type: inline-size; }
.card { display: grid; grid-template-columns: 1fr; }
@container (min-width: 400px) {
  .card { grid-template-columns: 1fr 2fr; }
}
// Tailwind v4 / with @tailwindcss/container-queries plugin
<div className="@container">
  <div className="grid grid-cols-1 @md:grid-cols-2">…</div>
</div>

Container queries are a strong senior signal because most candidates still use viewport queries everywhere. Use them when a card looks different in a sidebar vs. a full-width grid.

Layout: flex vs. grid

  • Grid: 2D layouts, page-level layouts, anything aligned in both rows and columns
  • Flex: 1D layouts — a row of buttons, a vertical stack, "push items to the ends"
{/* Common: nav with logo left, items right */}
<nav className="flex items-center justify-between">
  <Logo />
  <div className="flex items-center gap-4">…</div>
</nav>

{/* Common: card grid, auto-fit */}
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-6">

grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) is one of the most useful patterns: cards reflow without media queries.

Animation — the senior playbook

Animate the cheap properties only

GPU-accelerated, no layout: transform, opacity, filter. Expensive (cause layout/paint): width, height, top, left, margin.

/* Bad — causes layout on every frame */
.menu-enter { left: -300px; transition: left 0.3s; }
.menu-enter-active { left: 0; }

/* Good — composite-only */
.menu-enter { transform: translateX(-100%); transition: transform 0.3s var(--ease); }
.menu-enter-active { transform: translateX(0); }

Always respect prefers-reduced-motion

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

CSS transitions for state, keyframes for ongoing motion

/* State change — transition */
.btn { transform: scale(1); transition: transform var(--duration-fast) var(--ease); }
.btn:active { transform: scale(0.98); }

/* Ongoing motion — keyframes */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}
.skeleton { animation: pulse 1.5s ease-in-out infinite; }

Framer Motion (use sparingly, only when CSS can't)

import { motion, AnimatePresence } from 'framer-motion';

<AnimatePresence>
  {open && (
    <motion.div
      initial={{ opacity: 0, y: 8 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: 8 }}
      transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
    >
      …
    </motion.div>
  )}
</AnimatePresence>

Use Framer when:

  • You need exit animations (CSS-only can't animate elements leaving the DOM cleanly)
  • You need orchestration (stagger, sequence)
  • You need gestures (drag, swipe)

Otherwise CSS wins on bundle size and perf.

Scroll-triggered animation

Use IntersectionObserver, not scroll listeners:

function FadeInOnScroll({ children }: { children: ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      ([entry]) => entry.isIntersecting && setVisible(true),
      { rootMargin: '0px 0px -10% 0px' }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return (
    <div
      ref={ref}
      style={{
        opacity: visible ? 1 : 0,
        transform: visible ? 'translateY(0)' : 'translateY(12px)',
        transition: 'opacity 400ms var(--ease), transform 400ms var(--ease)',
      }}
    >
      {children}
    </div>
  );
}

For Tailwind, animation-delay utilities + the new @starting-style or libraries like react-intersection-observer work too.

Dark mode

Three approaches, ranked:

  1. class strategy + CSS variables (recommended): toggle a class on <html>, all tokens flip via cascade.

    <html class="dark">
    
    :root { --color-bg: white; }
    .dark { --color-bg: black; }
    

    In Tailwind: configure darkMode: 'class'.

  2. prefers-color-scheme only: respects system, no toggle.

    @media (prefers-color-scheme: dark) { :root { --color-bg: black; } }
    
  3. Hybrid: default to system, allow override via class.

Always test:

  • Contrast in both modes
  • Initial render — prevent flash of wrong theme (FOUC) by setting class on <html> synchronously before React hydrates (Next.js: a small inline script in <head>)

Modern CSS — the platform features worth knowing

These ship in evergreen browsers and let you delete React/JS in many cases. Most candidates still write 2018-era CSS while these newer features solve the same problems with less code.

:has() — parent selector

/* Card with featured badge → tinted background */
.card:has(.badge--featured) {
  background: hsl(173 80% 95%);
}

/* Form with any invalid field → show submit warning */
form:has(:user-invalid) .submit-warning {
  display: block;
}

/* Article with an image → adjust padding */
article:has(figure) {
  padding-block: 2rem;
}

Removes the need for state-driven className toggles in many cases.

:focus-within

.field:focus-within {
  outline: 2px solid var(--color-brand);
  outline-offset: 2px;
}

.field:focus-within .hint {
  opacity: 1;
}

Highlights a container when any descendant has focus — common for form fields, dropdowns, cards.

:user-valid and :user-invalid

input:user-invalid {
  border-color: var(--color-danger);
}
input:user-valid {
  border-color: var(--color-success);
}

Matches after the user has interacted. Unlike :invalid, which matches on blank required fields before the user even tries.

light-dark() and color-scheme

:root {
  color-scheme: light dark;
  --color-bg: light-dark(white, hsl(0 0% 7%));
  --color-fg: light-dark(hsl(0 0% 10%), hsl(0 0% 96%));
}

Theme switching without @media (prefers-color-scheme) blocks. color-scheme also tells the browser to use light/dark form controls + scrollbars.

color-mix() — derive colors at the CSS layer

:root {
  --color-brand: hsl(173 80% 40%);
  --color-brand-hover: color-mix(in oklch, var(--color-brand), black 10%);
  --color-brand-tint:  color-mix(in oklch, var(--color-brand), white 90%);
}

Define one brand color, derive hover/tint/shade without a build step. in oklch keeps the mix perceptually uniform.

@layer — order cascade explicitly

@layer reset, base, components, utilities;

@layer base {
  h1 { font-size: 2rem; }
}

@layer components {
  .card { padding: 1rem; }
}

@layer utilities {
  .text-center { text-align: center; }
}

Layers listed later win, regardless of specificity. Predictable cascade without specificity wars or !important.

@scope — scope styles without naming hacks

@scope (.card) {
  h2 { font-size: 1.5rem; }   /* only h2 inside .card */
}

CSS-Modules-like scoping without the build step. Chromium 118+, Safari/Firefox behind flag — keep as progressive enhancement.

text-wrap: balance and text-wrap: pretty

h1, h2, h3 { text-wrap: balance; }   /* even line lengths for headings */
p          { text-wrap: pretty; }     /* avoid widow words */

Tiny additions, huge typographic upgrade.

interpolate-size: allow-keywords

details {
  interpolate-size: allow-keywords;
}

details::details-content {
  block-size: 0;
  transition: block-size 0.3s ease;
}

details[open]::details-content {
  block-size: auto;
}

Finally enables smooth transition to/from auto — accordion open/close without JS measurement. Chromium 131+ only as of mid-2026.

scrollbar-gutter: stable

html {
  scrollbar-gutter: stable;
}

Reserves scrollbar space even when not visible. Prevents layout jumps when content height changes (e.g., modal opens → scrollbar disappears → page jumps).

Anchor positioning (cross-reference)

For positioning popovers/tooltips relative to triggers without JS — see react-html-platform for the full pattern.

Browser support cheat (mid-2026)

  • :has() — universal
  • :focus-within — universal
  • :user-valid / :user-invalid — universal
  • light-dark() — universal
  • color-mix() — universal
  • @layer — universal
  • @scope — Chromium 118+, others behind flag
  • text-wrap: balance — universal
  • text-wrap: pretty — Chromium 117+, others catching up
  • interpolate-size: allow-keywords — Chromium 131+ only
  • scrollbar-gutter — universal
  • Anchor positioning — Chromium 125+, others behind flag

For not-yet-universal features, fallback is usually "no animation" or "default positioning" — degrades gracefully.

Common styling mistakes (interviewer-visible)

MistakeFix
style={{ color: '#3a3a3a' }}Use a token
transform: scale(0.95) on click but no transitionAdd transition: transform 150ms ease
Hover-only states (no focus-visible)Always pair hover with focus
outline: none without replacementUse :focus-visible { outline: 2px solid var(--color-brand); }
Fixed width: 320pxUse max-width: 320px; width: 100%
100vh on mobile breaks (browser chrome)Use 100dvh or 100svh
Animating width for a sliding menuAnimate transform: translateX
Setting font-size: 14px everywhereUse a type scale, set base on <html> and rem elsewhere
position: absolute without position: relative parentAlways pair
Forgetting hover/focus/active/disabledRun through all 4 states for every interactive

Narration phrases

  • "I'm using CSS custom properties for tokens so theme switching is a single class toggle on <html> — no React re-render needed."
  • "All animation is on transform and opacity — those run on the compositor and don't trigger layout."
  • "I added prefers-reduced-motion because the hero animation isn't essential to the content."
  • "I'm using container queries here because this card lives in both a 3-column grid and a sidebar — viewport breakpoints would lie."
  • "The breakpoints are mobile-first; the only place I had to override down was the sticky nav at < 480px."

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.