Bug Report
A bug report people finish: title, severity, numbered steps, expected vs actual, and environment details attached.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/bug-report.json'use client'
import { useRef, useState } from 'react'
import './bug-report.css'
export interface BugReportProps {
env?: { label: string; value: string }[]
severity?: 'low' | 'medium' | 'high' | 'critical'
onFile?: (report: { title: string; severity: string; steps: string[]; expected: string; actual: string; env: string[] }) => void
fill?: 'dark' | 'light'
className?: string
}
const SEV = ['low', 'medium', 'high', 'critical'] as const
/** Structured bug report form. */
export function BugReport({ env = [], severity = 'low', onFile, fill = 'dark', className }: BugReportProps) {
const [title, setTitle] = useState('')
const [sev, setSev] = useState<string>(severity)
const [steps, setSteps] = useState<string[]>([''])
const [exp, setExp] = useState('')
const [act, setAct] = useState('')
const [keep, setKeep] = useState(() => new Set(env.map((e) => e.label)))
const [filed, setFiled] = useState('')
const refs = useRef<(HTMLInputElement | null)[]>([])
const real = steps.filter((s) => s.trim())
const missing = !title.trim() ? 'Add a title' : !real.length ? 'Add at least one step' : ''
const focus = (i: number) => setTimeout(() => refs.current[i]?.focus(), 0)
if (filed) return (
<div className={['sd-bug', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-bug__ok" role="status"><b>Report filed</b><code>{filed}</code><span style={{ fontSize: 12.5, color: 'var(--mute)' }}>{sev} · {real.length} {real.length === 1 ? 'step' : 'steps'} · {keep.size} details attached</span><button type="button" onClick={() => { setFiled(''); setTitle(''); setSteps(['']); setExp(''); setAct('') }}>File another</button></div>
</div>
)
return (
<form className={['sd-bug', className].filter(Boolean).join(' ')} data-fill={fill} onSubmit={(e) => { e.preventDefault(); if (missing) return; setFiled(`BUG-${1040 + title.length}`); onFile?.({ title, severity: sev, steps: real, expected: exp, actual: act, env: env.filter((x) => keep.has(x.label)).map((x) => `${x.label}: ${x.value}`) }) }}>
<input className="sd-bug__t" aria-label="Title" placeholder="What went wrong?" value={title} onChange={(e) => setTitle(e.target.value)} />
<div className="sd-bug__sev" role="radiogroup" aria-label="Severity">{SEV.map((s) => <button key={s} type="button" role="radio" data-v={s} aria-checked={sev === s} onClick={() => setSev(s)}>{s[0].toUpperCase() + s.slice(1)}</button>)}</div>
<h4>Steps to reproduce</h4>
<ol>{steps.map((s, i) => <li key={i}><input ref={(el) => { refs.current[i] = el }} aria-label={`Step ${i + 1}`} placeholder={i === 0 ? 'Open the page…' : 'Then…'} value={s}
onChange={(e) => setSteps(steps.map((x, j) => (j === i ? e.target.value : x)))}
onKeyDown={(e) => {
if (e.key === 'Enter') { e.preventDefault(); if (s.trim()) { setSteps([...steps.slice(0, i + 1), '', ...steps.slice(i + 1)]); focus(i + 1) } }
if (e.key === 'Backspace' && !s && steps.length > 1) { e.preventDefault(); setSteps(steps.filter((_, j) => j !== i)); focus(Math.max(0, i - 1)) }
}} /></li>)}</ol>
<div className="sd-bug__ea"><label>Expected<textarea value={exp} onChange={(e) => setExp(e.target.value)} /></label><label>Actual<textarea value={act} onChange={(e) => setAct(e.target.value)} /></label></div>
{env.length > 0 && <><h4>Attach details</h4><div className="sd-bug__env">{env.map((x) => <button key={x.label} type="button" aria-pressed={keep.has(x.label)} onClick={() => setKeep((k) => { const n = new Set(k); n.has(x.label) ? n.delete(x.label) : n.add(x.label); return n })}>{x.value}</button>)}</div></>}
<div className="sd-bug__f"><small aria-live="polite">{missing || 'Ready to file'}</small><button type="submit" disabled={!!missing}>File bug</button></div>
</form>
)
}.sd-bug { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-bug[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-bug :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-bug :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-bug .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-bug { width: min(100%, 340px); padding: 14px; grid-template-columns: minmax(0, 1fr); border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); display: grid; gap: 10px; }
.sd-bug input, .sd-bug textarea { width: 100%; box-sizing: border-box; border: 0; background: var(--soft); color: var(--fg); font: 15px ui-sans-serif, system-ui; outline: 0; border-radius: 10px; }
.sd-bug input:focus, .sd-bug textarea:focus { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-bug__t { height: 40px; padding: 0 10px; font-weight: 600 !important; }
.sd-bug__sev { display: flex; padding: 2px; border-radius: 10px; background: var(--soft); }
.sd-bug__sev button { flex: 1; height: 30px; border-radius: 8px; font-size: 12px; color: var(--mute) !important; }
.sd-bug__sev button[aria-checked='true'] { background: var(--bg) !important; color: var(--fg) !important; box-shadow: inset 0 0 0 1px var(--line); font-weight: 600; }
.sd-bug__sev button[aria-checked='true'][data-v='critical'] { background: var(--strong) !important; color: var(--on) !important; }
.sd-bug h4 { margin: 0; font-size: 11.5px; color: var(--mute); font-weight: 600; }
.sd-bug ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; counter-reset: s; }
.sd-bug ol li { display: flex; align-items: center; gap: 8px; counter-increment: s; }
.sd-bug ol li::before { content: counter(s); width: 20px; height: 20px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); }
.sd-bug ol input { height: 34px; padding: 0 9px; font-size: 14px; }
.sd-bug__ea { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 6px; }
.sd-bug__ea label { display: grid; gap: 4px; font-size: 11.5px; color: var(--mute); }
.sd-bug__ea textarea { height: 52px; resize: none; padding: 7px 9px; font-size: 13px; }
.sd-bug__env { display: flex; flex-wrap: wrap; gap: 4px; }
.sd-bug__env button { height: 26px; padding: 0 9px; border-radius: 99px; font-size: 11.5px; box-shadow: inset 0 0 0 1px var(--line); }
.sd-bug__env button[aria-pressed='false'] { color: var(--mute) !important; text-decoration: line-through; }
.sd-bug__f { display: flex; align-items: center; gap: 10px; }
.sd-bug__f small { flex: 1; font-size: 11.5px; color: var(--mute); }
.sd-bug__f button { padding: 0 16px; height: 40px; border-radius: 11px; font-size: 13px; font-weight: 600;background: var(--strong) !important; color: var(--on) !important; }
.sd-bug__f button:disabled { opacity: 0.4; }
.sd-bug__ok { display: grid; gap: 4px; padding: 18px 4px; text-align: center; }
.sd-bug__ok b { font-size: 16px; }
.sd-bug__ok code { font: 600 13px ui-monospace, monospace; }
.sd-bug__ok button { justify-self: center; margin-top: 8px; font-size: 12.5px; text-decoration: underline; text-underline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .sd-bug *, .sd-bug *::before, .sd-bug *::after { animation: none !important; transition: none !important; } }Overview
Steps are a numbered list: Enter adds the next step, Backspace on an empty step removes it. Expected and actual sit side by side as two short fields.
Browser, OS and screen size are detected and shown as chips that can be left out. Filing needs a title and one step.
When to use
- In-app feedback, beta programs, QA tools.
When not to use
- Feature requests: ask for the problem instead of steps.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/bug-report.json$
npx shadcn@latest add https://sundaeui.dev/r/bug-report.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/bug-report.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/bug-report.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy bug-report.tsx and bug-report.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { BugReport } from '@/components/sundae/bug-report'
<BugReport />Props
| Prop | Type | Default |
|---|---|---|
envEnvironment details to offer. | { label: string; value: string }[] | — |
onFileCalled with the report. | (report: object) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Severity is a radio group; steps are labelled inputs ("Step 2").
- Missing fields are explained next to the submit button.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-bug; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Bug Report free?
- Yes. Bug Report is free for personal and commercial projects, with full source on this page.
- How do I install Bug Report?
- Run npx shadcn@latest add https://sundaeui.dev/r/bug-report.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Bug Report support keyboard and screen readers?
- Severity is a radio group; steps are labelled inputs ("Step 2"). Missing fields are explained next to the submit button.
- Does Bug Report respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Rating Scale
Forms · RefinedWas This Helpful
Forms · RefinedOTP Field
Forms · Refined- Pro
Drop Zone
Forms · Refined - Pro
Onboarding Flow
Forms · Refined - Pro
Wheel Picker
Forms · Refined