Frontend UI Dark TS
MicrosoftOfficialCreated by Microsoft
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured foundation for building dark-themed React applications. It integrates Tailwind CSS for styling, Framer Motion for animations, and TypeScript for type safety. The system includes pre-configured design tokens, reusable UI components, and layout patterns specifically tailored for dashboards and data-rich interfaces.
What it does
- Provides a pre-configured Vite project structure with React and TypeScript.
- Implements a comprehensive dark-themed color palette and design tokens.
- Includes reusable UI components like buttons, inputs, and cards.
- Offers glassmorphism utility classes for modern visual effects.
- Integrates Framer Motion for page transitions and interactive animations.
- Configures mobile-responsive layouts with safe-area support.
When to use it
- When starting a new React project requiring a refined dark aesthetic.
- When building dashboards or admin panels that need data-rich visualization support.
- When you want to implement consistent glassmorphism effects across your UI.
- When you need a pre-defined set of design tokens and animation patterns.
Advantages
- Ready-to-use design system with consistent color and spacing scales.
- Built-in support for touch-friendly mobile interactions.
- Includes pre-configured PWA manifest and SEO meta tags.
- Type-safe component architecture using TypeScript.
Usage examples
- “How do I add a glass-styled card to my dashboard?”
- The agent provides the HTML structure using the .glass-card utility class defined in the global CSS.
- “Show me how to implement a button with a loading state.”
- The agent provides the Button component code, demonstrating the use of the isLoading prop and the built-in spinner.
Install
4 of its files are not mirrored here, so install it from its source:
https://github.com/microsoft/skills/tree/0696600436a9669184ce47e8d8b2ceac62e4bfac/.github/plugins/azure-sdk-typescript/skills/frontend-ui-dark-ts
Documentation by Microsoft
Frontend UI Dark Theme (TypeScript)
A modern dark-themed React UI system using Tailwind CSS and Framer Motion. Designed for dashboards, admin panels, and data-rich applications with glassmorphism effects and tasteful animations.
Stack
| Package | Version | Purpose |
|---|---|---|
react | ^18.x | UI framework |
react-dom | ^18.x | DOM rendering |
react-router-dom | ^6.x | Routing |
framer-motion | ^11.x | Animations |
clsx | ^2.x | Class merging |
tailwindcss | ^3.x | Styling |
vite | ^5.x | Build tool |
typescript | ^5.x | Type safety |
Quick Start
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install framer-motion clsx react-router-dom
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pProject Structure
public/
├── favicon.ico # Classic favicon (32x32)
├── favicon.svg # Modern SVG favicon
├── apple-touch-icon.png # iOS home screen (180x180)
├── og-image.png # Social sharing image (1200x630)
└── site.webmanifest # PWA manifest
src/
├── assets/
│ └── fonts/
│ ├── Segoe UI.ttf
│ ├── Segoe UI Bold.ttf
│ ├── Segoe UI Italic.ttf
│ └── Segoe UI Bold Italic.ttf
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ ├── Input.tsx
│ │ ├── Badge.tsx
│ │ ├── Dialog.tsx
│ │ ├── Tabs.tsx
│ │ └── index.ts
│ └── layout/
│ ├── AppShell.tsx
│ ├── Sidebar.tsx
│ └── PageHeader.tsx
├── styles/
│ └── globals.css
├── App.tsx
└── main.tsxConfiguration
index.html
The HTML entry point with mobile viewport, favicons, and social meta tags:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!-- Favicons -->
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Theme color for mobile browser chrome -->
<meta name="theme-color" content="#18181B" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="App Name" />
<meta property="og:description" content="App description" />
<meta property="og:image" content="https://example.com/og-image.png" />
<meta property="og:url" content="https://example.com" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="App Name" />
<meta name="twitter:description" content="App description" />
<meta name="twitter:image" content="https://example.com/og-image.png" />
<title>App Name</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>public/site.webmanifest
PWA manifest for installable web apps:
{
"name": "App Name",
"short_name": "App",
"icons": [
{ "src": "/favicon.ico", "sizes": "32x32", "type": "image/x-icon" },
{ "src": "/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
],
"theme_color": "#18181B",
"background_color": "#18181B",
"display": "standalone"
}tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Segoe UI', 'system-ui', 'sans-serif'],
},
colors: {
brand: {
DEFAULT: '#8251EE',
hover: '#9366F5',
light: '#A37EF5',
subtle: 'rgba(130, 81, 238, 0.15)',
},
neutral: {
bg1: 'hsl(240, 6%, 10%)',
bg2: 'hsl(240, 5%, 12%)',
bg3: 'hsl(240, 5%, 14%)',
bg4: 'hsl(240, 4%, 18%)',
bg5: 'hsl(240, 4%, 22%)',
bg6: 'hsl(240, 4%, 26%)',
},
text: {
primary: '#FFFFFF',
secondary: '#A1A1AA',
muted: '#71717A',
},
border: {
subtle: 'hsla(0, 0%, 100%, 0.08)',
DEFAULT: 'hsla(0, 0%, 100%, 0.12)',
strong: 'hsla(0, 0%, 100%, 0.20)',
},
status: {
success: '#10B981',
warning: '#F59E0B',
error: '#EF4444',
info: '#3B82F6',
},
dataviz: {
purple: '#8251EE',
blue: '#3B82F6',
green: '#10B981',
yellow: '#F59E0B',
red: '#EF4444',
pink: '#EC4899',
cyan: '#06B6D4',
},
},
borderRadius: {
DEFAULT: '0.5rem',
lg: '0.75rem',
xl: '1rem',
},
boxShadow: {
glow: '0 0 20px rgba(130, 81, 238, 0.3)',
'glow-lg': '0 0 40px rgba(130, 81, 238, 0.4)',
},
backdropBlur: {
xs: '2px',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
// Mobile: safe area insets for notched devices
spacing: {
'safe-top': 'env(safe-area-inset-top)',
'safe-bottom': 'env(safe-area-inset-bottom)',
'safe-left': 'env(safe-area-inset-left)',
'safe-right': 'env(safe-area-inset-right)',
},
// Mobile: minimum touch target sizes (44px per Apple/Google guidelines)
minHeight: {
'touch': '44px',
},
minWidth: {
'touch': '44px',
},
},
},
plugins: [],
};postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Font faces */
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Segoe UI';
src: url('../assets/fonts/Segoe UI Bold Italic.ttf') format('truetype');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* CSS Custom Properties */
:root {
/* Brand colors */
--color-brand: #8251EE;
--color-brand-hover: #9366F5;
--color-brand-light: #A37EF5;
--color-brand-subtle: rgba(130, 81, 238, 0.15);
/* Neutral backgrounds */
--color-bg-1: hsl(240, 6%, 10%);
--color-bg-2: hsl(240, 5%, 12%);
--color-bg-3: hsl(240, 5%, 14%);
--color-bg-4: hsl(240, 4%, 18%);
--color-bg-5: hsl(240, 4%, 22%);
--color-bg-6: hsl(240, 4%, 26%);
/* Text colors */
--color-text-primary: #FFFFFF;
--color-text-secondary: #A1A1AA;
--color-text-muted: #71717A;
/* Border colors */
--color-border-subtle: hsla(0, 0%, 100%, 0.08);
--color-border-default: hsla(0, 0%, 100%, 0.12);
--color-border-strong: hsla(0, 0%, 100%, 0.20);
/* Status colors */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease;
}
/* Base styles */
html {
color-scheme: dark;
}
body {
@apply bg-neutral-bg1 text-text-primary font-sans antialiased;
min-height: 100vh;
}
/* Focus styles */
*:focus-visible {
@apply outline-none ring-2 ring-brand ring-offset-2 ring-offset-neutral-bg1;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-neutral-bg2;
}
::-webkit-scrollbar-thumb {
@apply bg-neutral-bg5 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-bg6;
}
/* Glass utility classes */
@layer components {
.glass {
@apply backdrop-blur-md bg-white/5 border border-white/10;
}
.glass-card {
@apply backdrop-blur-md bg-white/5 border border-white/10 rounded-xl;
}
.glass-panel {
@apply backdrop-blur-lg bg-black/40 border border-white/5;
}
.glass-overlay {
@apply backdrop-blur-sm bg-black/60;
}
.glass-input {
@apply backdrop-blur-sm bg-white/5 border border-white/10 focus:border-brand focus:bg-white/10;
}
}
/* Animation utilities */
@layer utilities {
.animate-in {
animation: fadeIn 0.3s ease-out, slideUp 0.3s ease-out;
}
}src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './styles/globals.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);src/App.tsx
import { Routes, Route } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import { AppShell } from './components/layout/AppShell';
import { Dashboard } from './pages/Dashboard';
import { Settings } from './pages/Settings';
export default function App() {
return (
<AppShell>
<AnimatePresence mode="wait">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</AnimatePresence>
</AppShell>
);
}Animation Patterns
Framer Motion Variants
// Fade in on mount
export const fadeIn = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
transition: { duration: 0.2 },
};
// Slide up on mount
export const slideUp = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 },
transition: { duration: 0.3, ease: 'easeOut' },
};
// Scale on hover (for buttons/cards)
export const scaleOnHover = {
whileHover: { scale: 1.02 },
whileTap: { scale: 0.98 },
transition: { type: 'spring', stiffness: 400, damping: 17 },
};
// Stagger children
export const staggerContainer = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: 0.1,
},
},
};
export const staggerItem = {
hidden: { opacity: 0, y: 10 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.2, ease: 'easeOut' },
},
};Page Transition Wrapper
import { motion } from 'framer-motion';
import { ReactNode } from 'react';
interface PageTransitionProps {
children: ReactNode;
}
export function PageTransition({ children }: PageTransitionProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}Glass Effect Patterns
Glass Card
<div className="glass-card p-6">
<h2 className="text-lg font-semibold text-text-primary">Card Title</h2>
<p className="text-text-secondary mt-2">Card content goes here.</p>
</div>Glass Panel (Sidebar)
<aside className="glass-panel w-64 h-screen p-4">
<nav className="space-y-2">
{/* Navigation items */}
</nav>
</aside>Glass Modal Overlay
<motion.div
className="fixed inset-0 glass-overlay flex items-center justify-center z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
className="glass-card p-6 max-w-md w-full mx-4"
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
>
{/* Modal content */}
</motion.div>
</motion.div>Typography
| Element | Classes |
|---|---|
| Page title | text-2xl font-semibold text-text-primary |
| Section title | text-lg font-semibold text-text-primary |
| Card title | text-base font-medium text-text-primary |
| Body text | text-sm text-text-secondary |
| Caption | text-xs text-text-muted |
| Label | text-sm font-medium text-text-secondary |
Color Usage
| Use Case | Color | Class |
|---|---|---|
| Primary action | Brand purple | bg-brand text-white |
| Primary hover | Brand hover | hover:bg-brand-hover |
| Page background | Neutral bg1 | bg-neutral-bg1 |
| Card background | Neutral bg2 | bg-neutral-bg2 |
| Elevated surface | Neutral bg3 | bg-neutral-bg3 |
| Input background | Neutral bg2 | bg-neutral-bg2 |
| Input focus | Neutral bg3 | focus:bg-neutral-bg3 |
| Border default | Border default | border-border |
| Border subtle | Border subtle | border-border-subtle |
| Success | Status success | text-status-success |
| Warning | Status warning | text-status-warning |
| Error | Status error | text-status-error |
Related Files
Design Tokens — Complete color system, spacing, typography scales
Components — Button, Card, Input, Dialog, Tabs, and more
Patterns — Page layouts, navigation, lists, forms
Files
- ATTRIBUTION.mdAdded by AutoGineer976 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md14.4 KiB
- assets/Segoe UI Bold Italic.ttf(opens on GitHub)Not mirrored: opens on GitHub41.6 KiB
- assets/Segoe UI Bold.ttf(opens on GitHub)Not mirrored: opens on GitHub35.2 KiB
- assets/Segoe UI Italic.ttf(opens on GitHub)Not mirrored: opens on GitHub40.4 KiB
- assets/Segoe UI.ttf(opens on GitHub)Not mirrored: opens on GitHub33.4 KiB
- assets/foundry-logo-dark.png29.3 KiB
- assets/foundry-logo-light.png24.9 KiB
- references/components.md26.1 KiB
- references/design-tokens.md13.9 KiB
- references/patterns.md30.1 KiB
references/components.md
Components
Reusable UI components built with React, Tailwind CSS, and Framer Motion.
Button
import { motion, type HTMLMotionProps } from 'framer-motion';
import { clsx } from 'clsx';
import { forwardRef, type ReactNode } from 'react';
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends Omit<HTMLMotionProps<'button'>, 'children'> {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
isLoading?: boolean;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
}
const variants: Record<ButtonVariant, string> = {
primary: 'bg-brand text-white hover:bg-brand-hover active:bg-brand-light',
secondary: 'bg-neutral-bg3 text-text-primary border border-border hover:bg-neutral-bg4 hover:border-border-strong',
ghost: 'text-text-secondary hover:text-text-primary hover:bg-neutral-bg3',
danger: 'bg-status-error text-white hover:bg-red-600 active:bg-red-700',
};
// Size variants - all sizes meet 44px minimum height on mobile for touch targets
const sizes: Record<ButtonSize, string> = {
sm: 'h-8 min-h-touch px-3 text-xs gap-1.5 rounded', // 32px desktop, 44px mobile minimum
md: 'h-10 min-h-touch px-4 text-sm gap-2 rounded-lg', // 40px desktop, 44px mobile minimum
lg: 'h-12 px-6 text-base gap-2.5 rounded-lg', // 48px already exceeds 44px
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', children, isLoading, leftIcon, rightIcon, className, disabled, ...props }, ref) => {
return (
<motion.button
ref={ref}
whileHover={{ scale: disabled || isLoading ? 1 : 1.02 }}
whileTap={{ scale: disabled || isLoading ? 1 : 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
className={clsx(
'inline-flex items-center justify-center font-medium transition-colors duration-150',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-bg1',
'disabled:opacity-50 disabled:cursor-not-allowed',
variants[variant],
sizes[size],
className
)}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
) : (
<>
{leftIcon && <span className="flex-shrink-0">{leftIcon}</span>}
{children}
{rightIcon && <span className="flex-shrink-0">{rightIcon}</span>}
</>
)}
</motion.button>
);
}
);
Button.displayName = 'Button';Button Usage
// Primary (default)
<Button>Save Changes</Button>
// Secondary
<Button variant="secondary">Cancel</Button>
// Ghost
<Button variant="ghost">Learn More</Button>
// Danger
<Button variant="danger">Delete</Button>
// With icons
<Button leftIcon={<PlusIcon />}>Add Item</Button>
<Button rightIcon={<ArrowRightIcon />}>Continue</Button>
// Loading state
<Button isLoading>Saving...</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>Input
import { forwardRef, type InputHTMLAttributes, type ReactNode } from 'react';
import { clsx } from 'clsx';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
hint?: string;
leftIcon?: ReactNode;
rightIcon?: ReactNode;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, hint, leftIcon, rightIcon, className, id, ...props }, ref) => {
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-');
return (
<div className="flex flex-col gap-1.5">
{label && (
<label htmlFor={inputId} className="text-sm font-medium text-text-secondary">
{label}
</label>
)}
<div className="relative">
{leftIcon && (
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted">
{leftIcon}
</span>
)}
<input
ref={ref}
id={inputId}
className={clsx(
'w-full h-10 min-h-touch px-3 rounded-lg text-sm text-text-primary', // min-h-touch for 44px mobile touch target
'bg-neutral-bg2 border border-border',
'placeholder:text-text-muted',
'focus:outline-none focus:border-brand focus:bg-neutral-bg3',
'disabled:opacity-50 disabled:cursor-not-allowed',
'transition-colors duration-150',
leftIcon && 'pl-10',
rightIcon && 'pr-10',
error && 'border-status-error focus:border-status-error',
className
)}
{...props}
/>
{rightIcon && (
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted">
{rightIcon}
</span>
)}
</div>
{error && <p className="text-xs text-status-error">{error}</p>}
{hint && !error && <p className="text-xs text-text-muted">{hint}</p>}
</div>
);
}
);
Input.displayName = 'Input';Search Input Variant
import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/20/solid';
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) {
return (
<Input
type="search"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
leftIcon={<MagnifyingGlassIcon className="w-4 h-4" />}
rightIcon={
value && (
<button onClick={() => onChange('')} className="hover:text-text-primary transition-colors">
<XMarkIcon className="w-4 h-4" />
</button>
)
}
/>
);
}Card
import { motion, type HTMLMotionProps } from 'framer-motion';
import { clsx } from 'clsx';
import { forwardRef, type ReactNode } from 'react';
type CardVariant = 'default' | 'glass' | 'elevated';
interface CardProps extends HTMLMotionProps<'div'> {
variant?: CardVariant;
children: ReactNode;
interactive?: boolean;
padding?: 'none' | 'sm' | 'md' | 'lg';
}
const variants: Record<CardVariant, string> = {
default: 'bg-neutral-bg2 border border-border',
glass: 'backdrop-blur-md bg-white/5 border border-white/10',
elevated: 'bg-neutral-bg3 border border-border shadow-lg',
};
const paddings = {
none: '',
sm: 'p-3',
md: 'p-4',
lg: 'p-6',
};
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ variant = 'default', children, interactive = false, padding = 'md', className, ...props }, ref) => {
const baseClass = clsx(
'rounded-xl',
variants[variant],
paddings[padding],
interactive && 'cursor-pointer hover:border-border-strong transition-colors duration-150',
className
);
if (interactive) {
return (
<motion.div
ref={ref}
className={baseClass}
whileHover={{ scale: 1.01, y: -2 }}
whileTap={{ scale: 0.99 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
{...props}
>
{children}
</motion.div>
);
}
return (
<motion.div ref={ref} className={baseClass} {...props}>
{children}
</motion.div>
);
}
);
Card.displayName = 'Card';
// Subcomponents
export function CardHeader({ children, className }: { children: ReactNode; className?: string }) {
return <div className={clsx('mb-4', className)}>{children}</div>;
}
export function CardTitle({ children, className }: { children: ReactNode; className?: string }) {
return <h3 className={clsx('text-lg font-semibold text-text-primary', className)}>{children}</h3>;
}
export function CardDescription({ children, className }: { children: ReactNode; className?: string }) {
return <p className={clsx('text-sm text-text-secondary mt-1', className)}>{children}</p>;
}
export function CardContent({ children, className }: { children: ReactNode; className?: string }) {
return <div className={className}>{children}</div>;
}
export function CardFooter({ children, className }: { children: ReactNode; className?: string }) {
return <div className={clsx('mt-4 flex items-center gap-2', className)}>{children}</div>;
}Card Usage
// Default card
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
</Card>
// Glass card
<Card variant="glass">
<p>Glass card with blur effect</p>
</Card>
// Interactive card
<Card interactive onClick={() => console.log('clicked')}>
<p>Click me!</p>
</Card>Badge
import { clsx } from 'clsx';
import { type ReactNode } from 'react';
type BadgeVariant = 'default' | 'brand' | 'success' | 'warning' | 'error' | 'info';
type BadgeSize = 'sm' | 'md';
interface BadgeProps {
variant?: BadgeVariant;
size?: BadgeSize;
children: ReactNode;
className?: string;
}
const variants: Record<BadgeVariant, string> = {
default: 'bg-neutral-bg4 text-text-secondary',
brand: 'bg-brand-subtle text-brand-light',
success: 'bg-status-success-subtle text-status-success',
warning: 'bg-status-warning-subtle text-status-warning',
error: 'bg-status-error-subtle text-status-error',
info: 'bg-status-info-subtle text-status-info',
};
const sizes: Record<BadgeSize, string> = {
sm: 'text-xs px-1.5 py-0.5',
md: 'text-xs px-2 py-1',
};
export function Badge({ variant = 'default', size = 'md', children, className }: BadgeProps) {
return (
<span
className={clsx(
'inline-flex items-center font-medium rounded-md',
variants[variant],
sizes[size],
className
)}
>
{children}
</span>
);
}Badge Usage
<Badge>Default</Badge>
<Badge variant="brand">Brand</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="error">Error</Badge>
<Badge variant="info">Info</Badge>
// With dot indicator
<Badge variant="success">
<span className="w-1.5 h-1.5 rounded-full bg-current mr-1.5" />
Active
</Badge>Dialog
import { motion, AnimatePresence } from 'framer-motion';
import { clsx } from 'clsx';
import { type ReactNode, useEffect, useRef } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline';
interface DialogProps {
open: boolean;
onClose: () => void;
children: ReactNode;
title?: string;
description?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
}
const sizes = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
};
export function Dialog({ open, onClose, children, title, description, size = 'md' }: DialogProps) {
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (open) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [open, onClose]);
return (
<AnimatePresence>
{open && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<motion.div
ref={overlayRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="absolute inset-0 glass-overlay"
onClick={(e) => e.target === overlayRef.current && onClose()}
/>
{/* Dialog */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.2, ease: [0, 0, 0.2, 1] }}
className={clsx(
'relative w-full mx-4 glass-card p-6',
sizes[size]
)}
>
{/* Close button */}
<button
onClick={onClose}
className="absolute top-4 right-4 text-text-muted hover:text-text-primary transition-colors"
>
<XMarkIcon className="w-5 h-5" />
</button>
{/* Header */}
{(title || description) && (
<div className="mb-4 pr-8">
{title && <h2 className="text-lg font-semibold text-text-primary">{title}</h2>}
{description && <p className="text-sm text-text-secondary mt-1">{description}</p>}
</div>
)}
{/* Content */}
{children}
</motion.div>
</div>
)}
</AnimatePresence>
);
}
// Subcomponents
export function DialogFooter({ children, className }: { children: ReactNode; className?: string }) {
return (
<div className={clsx('mt-6 flex items-center justify-end gap-3', className)}>
{children}
</div>
);
}Dialog Usage
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Dialog</Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
title="Confirm Action"
description="Are you sure you want to continue?"
>
<p className="text-sm text-text-secondary">
This action cannot be undone.
</p>
<DialogFooter>
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleConfirm}>
Delete
</Button>
</DialogFooter>
</Dialog>Tabs
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
import { createContext, useContext, useState, type ReactNode } from 'react';
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
interface TabsProps {
defaultValue: string;
children: ReactNode;
className?: string;
}
export function Tabs({ defaultValue, children, className }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className={className}>{children}</div>
</TabsContext.Provider>
);
}
export function TabsList({ children, className }: { children: ReactNode; className?: string }) {
return (
<div
className={clsx(
'flex items-center gap-1 p-1 bg-neutral-bg2 rounded-lg',
className
)}
>
{children}
</div>
);
}
interface TabsTriggerProps {
value: string;
children: ReactNode;
className?: string;
}
export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabsTrigger must be used within Tabs');
const { activeTab, setActiveTab } = context;
const isActive = activeTab === value;
return (
<button
onClick={() => setActiveTab(value)}
className={clsx(
'relative px-3 py-1.5 text-sm font-medium rounded-md transition-colors duration-150',
isActive ? 'text-text-primary' : 'text-text-muted hover:text-text-secondary',
className
)}
>
{isActive && (
<motion.div
layoutId="activeTab"
className="absolute inset-0 bg-neutral-bg4 rounded-md"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
)}
<span className="relative z-10">{children}</span>
</button>
);
}
interface TabsContentProps {
value: string;
children: ReactNode;
className?: string;
}
export function TabsContent({ value, children, className }: TabsContentProps) {
const context = useContext(TabsContext);
if (!context) throw new Error('TabsContent must be used within Tabs');
if (context.activeTab !== value) return null;
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className={clsx('mt-4', className)}
>
{children}
</motion.div>
);
}Tabs Usage
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<p>Overview content</p>
</TabsContent>
<TabsContent value="analytics">
<p>Analytics content</p>
</TabsContent>
<TabsContent value="settings">
<p>Settings content</p>
</TabsContent>
</Tabs>Avatar
import { clsx } from 'clsx';
import { useState, type ImgHTMLAttributes } from 'react';
type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
interface AvatarProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'size'> {
size?: AvatarSize;
fallback?: string;
}
const sizes: Record<AvatarSize, string> = {
xs: 'w-6 h-6 text-xs',
sm: 'w-8 h-8 text-sm',
md: 'w-10 h-10 text-base',
lg: 'w-12 h-12 text-lg',
xl: 'w-16 h-16 text-xl',
};
export function Avatar({ size = 'md', fallback, src, alt, className, ...props }: AvatarProps) {
const [hasError, setHasError] = useState(false);
const initials = fallback || alt?.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase() || '?';
if (!src || hasError) {
return (
<div
className={clsx(
'flex items-center justify-center rounded-full bg-brand-subtle text-brand font-medium',
sizes[size],
className
)}
>
{initials}
</div>
);
}
return (
<img
src={src}
alt={alt}
onError={() => setHasError(true)}
className={clsx('rounded-full object-cover', sizes[size], className)}
{...props}
/>
);
}Checkbox
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
import { forwardRef, type InputHTMLAttributes } from 'react';
import { CheckIcon } from '@heroicons/react/20/solid';
interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: string;
}
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
({ label, className, id, ...props }, ref) => {
const checkboxId = id || label?.toLowerCase().replace(/\s+/g, '-');
return (
<label className={clsx('flex items-center gap-2 cursor-pointer', className)}>
<div className="relative">
<input
ref={ref}
type="checkbox"
id={checkboxId}
className="peer sr-only"
{...props}
/>
<div
className={clsx(
'w-5 h-5 rounded border-2 transition-colors duration-150',
'border-border bg-transparent',
'peer-checked:border-brand peer-checked:bg-brand',
'peer-focus-visible:ring-2 peer-focus-visible:ring-brand peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-neutral-bg1',
'peer-disabled:opacity-50 peer-disabled:cursor-not-allowed'
)}
/>
<motion.div
initial={false}
animate={{ scale: props.checked ? 1 : 0 }}
className="absolute inset-0 flex items-center justify-center text-white pointer-events-none"
>
<CheckIcon className="w-3.5 h-3.5" />
</motion.div>
</div>
{label && <span className="text-sm text-text-secondary">{label}</span>}
</label>
);
}
);
Checkbox.displayName = 'Checkbox';Select
import { clsx } from 'clsx';
import { forwardRef, type SelectHTMLAttributes } from 'react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
interface SelectOption {
value: string;
label: string;
}
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
label?: string;
options: SelectOption[];
error?: string;
placeholder?: string;
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ label, options, error, placeholder, className, id, ...props }, ref) => {
const selectId = id || label?.toLowerCase().replace(/\s+/g, '-');
return (
<div className="flex flex-col gap-1.5">
{label && (
<label htmlFor={selectId} className="text-sm font-medium text-text-secondary">
{label}
</label>
)}
<div className="relative">
<select
ref={ref}
id={selectId}
className={clsx(
'w-full h-10 px-3 pr-10 rounded-lg text-sm appearance-none',
'bg-neutral-bg2 border border-border text-text-primary',
'focus:outline-none focus:border-brand focus:bg-neutral-bg3',
'disabled:opacity-50 disabled:cursor-not-allowed',
'transition-colors duration-150',
error && 'border-status-error focus:border-status-error',
className
)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<ChevronDownIcon className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" />
</div>
{error && <p className="text-xs text-status-error">{error}</p>}
</div>
);
}
);
Select.displayName = 'Select';Toast
import { motion, AnimatePresence } from 'framer-motion';
import { clsx } from 'clsx';
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
import { CheckCircleIcon, ExclamationCircleIcon, InformationCircleIcon, XCircleIcon, XMarkIcon } from '@heroicons/react/24/outline';
type ToastVariant = 'success' | 'error' | 'warning' | 'info';
interface Toast {
id: string;
variant: ToastVariant;
title: string;
description?: string;
}
interface ToastContextValue {
toasts: Toast[];
addToast: (toast: Omit<Toast, 'id'>) => void;
removeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).slice(2);
setToasts((prev) => [...prev, { ...toast, id }]);
// Auto-remove after 5 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 5000);
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<ToastContainer />
</ToastContext.Provider>
);
}
export function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within ToastProvider');
return context;
}
const icons: Record<ToastVariant, typeof CheckCircleIcon> = {
success: CheckCircleIcon,
error: XCircleIcon,
warning: ExclamationCircleIcon,
info: InformationCircleIcon,
};
const variants: Record<ToastVariant, string> = {
success: 'text-status-success',
error: 'text-status-error',
warning: 'text-status-warning',
info: 'text-status-info',
};
function ToastContainer() {
const { toasts, removeToast } = useToast();
return (
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
<AnimatePresence>
{toasts.map((toast) => {
const Icon = icons[toast.variant];
return (
<motion.div
key={toast.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, x: 100, scale: 0.95 }}
transition={{ duration: 0.2 }}
className="glass-card p-4 min-w-[300px] max-w-[400px] flex items-start gap-3"
>
<Icon className={clsx('w-5 h-5 flex-shrink-0 mt-0.5', variants[toast.variant])} />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-primary">{toast.title}</p>
{toast.description && (
<p className="text-sm text-text-secondary mt-1">{toast.description}</p>
)}
</div>
<button
onClick={() => removeToast(toast.id)}
className="text-text-muted hover:text-text-primary transition-colors"
>
<XMarkIcon className="w-4 h-4" />
</button>
</motion.div>
);
})}
</AnimatePresence>
</div>
);
}Toast Usage
// Wrap app with provider
<ToastProvider>
<App />
</ToastProvider>
// Use in components
const { addToast } = useToast();
addToast({
variant: 'success',
title: 'Changes saved',
description: 'Your settings have been updated.',
});
addToast({
variant: 'error',
title: 'Error',
description: 'Something went wrong.',
});Component Index
Export all components from a central file:
// src/components/ui/index.ts
export { Button } from './Button';
export { Input, SearchInput } from './Input';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
export { Badge } from './Badge';
export { Dialog, DialogFooter } from './Dialog';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs';
export { Avatar } from './Avatar';
export { Checkbox } from './Checkbox';
export { Select } from './Select';
export { ToastProvider, useToast } from './Toast';Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Microsoft
- Publisher
- Microsoft (Official)
- Source
- microsoft/skills · .github/plugins/azure-sdk-typescript/skills/frontend-ui-dark-ts · commit 0696600
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Microsoft or Microsoft.
Get it from the source
4 of this skill’s files are not mirrored here, so there is no ZIP. Get the complete skill from its source: microsoft/skills