Display Settings
Reading and display preferences with a live preview: text size, contrast, reduced motion, line spacing and density.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/display-settings.json'use client'
import { useId, useState } from 'react'
import './display-settings.css'
export interface Prefs { size: number; contrast: boolean; motion: boolean; spacing: boolean; density: 'cozy' | 'compact' }
const DEF: Prefs = { size: 15, contrast: false, motion: true, spacing: false, density: 'cozy' }
export interface DisplaySettingsProps {
defaultValue?: Partial<Prefs>
onChange?: (p: Prefs) => void
fill?: 'dark' | 'light'
className?: string
}
/** Display preferences with a live preview. */
export function DisplaySettings({ defaultValue, onChange, fill = 'dark', className }: DisplaySettingsProps) {
const [p, setP] = useState<Prefs>({ ...DEF, ...defaultValue })
const id = useId()
const set = <K extends keyof Prefs>(k: K, v: Prefs[K]) => { const n = { ...p, [k]: v }; setP(n); onChange?.(n) }
const diff = [p.size !== DEF.size && `text ${p.size}px`, p.contrast && 'high contrast', !p.motion && 'reduced motion', p.spacing && 'wider spacing', p.density !== DEF.density && p.density].filter(Boolean) as string[]
const sw = (k: 'contrast' | 'motion' | 'spacing', label: string) => (
<div className="sd-dsp__row"><span id={`${id}${k}`}>{label}</span><button type="button" role="switch" aria-checked={p[k]} aria-labelledby={`${id}${k}`} className="sd-dsp__sw" onClick={() => set(k, !p[k])} /></div>
)
return (
<div className={['sd-dsp', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-dsp__pv" aria-hidden data-hc={p.contrast || undefined} data-motion={p.motion || undefined}
style={{ ['--fs' as string]: p.size, ['--lh' as string]: p.spacing ? 1.8 : 1.45, ['--pad' as string]: p.density === 'compact' ? 10 : 16 }}>
<b>Weekly summary</b>
<p>You finished 12 tasks and closed 3 projects. Your busiest day was Tuesday.</p>
<span><i>Open report</i></span>
</div>
<div className="sd-dsp__row"><label htmlFor={`${id}s`}>Text size</label><input id={`${id}s`} type="range" min={12} max={22} value={p.size} aria-valuetext={`${p.size} pixels`} onChange={(e) => set('size', +e.target.value)} /><output>{p.size}px</output></div>
{sw('contrast', 'High contrast')}
{sw('motion', 'Animations')}
{sw('spacing', 'Wider line spacing')}
<div className="sd-dsp__row"><span>Density</span><div className="sd-dsp__seg" role="radiogroup" aria-label="Density">{(['cozy', 'compact'] as const).map((d) => <button key={d} type="button" role="radio" aria-checked={p.density === d} onClick={() => set('density', d)}>{d === 'cozy' ? 'Cozy' : 'Compact'}</button>)}</div></div>
<div className="sd-dsp__foot"><span aria-live="polite">{diff.length ? `Custom: ${diff.join(', ')}` : 'Using defaults'}</span><button type="button" disabled={!diff.length} onClick={() => { setP(DEF); onChange?.(DEF) }}>Reset</button></div>
</div>
)
}.sd-dsp { --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-dsp[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-dsp :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-dsp :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-dsp .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-dsp { width: min(100%, 330px); padding: 14px; border-radius: 20px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); }
.sd-dsp__pv { padding: calc(var(--pad) * 1px); border-radius: 14px; background: var(--soft); margin-bottom: 12px; transition: padding 200ms; }
.sd-dsp__pv[data-hc] { background: var(--strong); color: var(--on); }
.sd-dsp__pv b { display: block; font-size: calc(var(--fs) * 1.25px); letter-spacing: -0.01em; transition: font-size 150ms; }
.sd-dsp__pv p { margin: 6px 0 0; font-size: calc(var(--fs) * 1px); line-height: var(--lh); color: color-mix(in srgb, currentColor 75%, transparent); transition: font-size 150ms, line-height 150ms; }
.sd-dsp__pv[data-hc] p { color: inherit; }
.sd-dsp__pv span { display: inline-block; margin-top: 10px; padding: 6px 12px; border-radius: 99px; background: currentColor; font-size: 12px; }
.sd-dsp__pv span i { font-style: normal; font-weight: 600; color: var(--bg); }
.sd-dsp__pv[data-hc] span i { color: var(--strong); }
.sd-dsp__pv[data-motion] span { animation: sd-dsp-bob 1.6s ease-in-out infinite; }
.sd-dsp__row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 2px; border-top: 1px solid var(--line); font-size: 13.5px; }
.sd-dsp__row input[type='range'] { flex: 1; max-width: 150px; accent-color: var(--fg); }
.sd-dsp__row output { width: 36px; text-align: right; font-size: 12.5px; color: var(--mute); font-variant-numeric: tabular-nums; }
.sd-dsp__sw { position: relative; width: 38px; height: 22px; flex: none; border-radius: 99px; background: var(--line) !important; }
.sd-dsp__sw::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 99px; background: var(--fg); transition: transform 180ms cubic-bezier(0.3, 1.5, 0.4, 1); }
.sd-dsp__sw[aria-checked='true'] { background: var(--strong) !important; }
.sd-dsp__sw[aria-checked='true']::after { transform: translateX(16px); background: var(--on); }
.sd-dsp__seg { display: flex; padding: 2px; border-radius: 8px; background: var(--soft); }
.sd-dsp__seg button { padding: 4px 9px; border-radius: 6px; font-size: 12px; color: var(--mute) !important; }
.sd-dsp__seg button[aria-checked='true'] { background: var(--bg); color: var(--fg) !important; box-shadow: 0 1px 2px rgb(0 0 0 / 0.25); }
.sd-dsp__foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 12px; color: var(--mute); }
.sd-dsp__foot button { padding: 6px 10px; border-radius: 8px; font-size: 12.5px; box-shadow: inset 0 0 0 1px var(--line); color: var(--fg) !important; flex: none; }
.sd-dsp__foot button:disabled { opacity: 0.35; }
@keyframes sd-dsp-bob { 50% { transform: translateY(-3px); } }
@media (prefers-reduced-motion: reduce) { .sd-dsp *, .sd-dsp *::before, .sd-dsp *::after { animation: none !important; transition: none !important; } }Overview
Every control updates the sample card above it, so people see the effect before they leave the page. Settings map to CSS variables you can apply app-wide.
Reset returns to defaults; the summary lists what differs from them.
When to use
- Account preferences, reading apps, accessibility menus.
When not to use
- OS-level settings the browser already exposes.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/display-settings.json$
npx shadcn@latest add https://sundaeui.dev/r/display-settings.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/display-settings.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/display-settings.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 display-settings.tsx and display-settings.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 { DisplaySettings } from '@/components/sundae/display-settings'
<DisplaySettings />Props
| Prop | Type | Default |
|---|---|---|
value / defaultValueSize, contrast, motion, spacing, density. | Prefs | — |
onChangeCalled on change. | (p: Prefs) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Controls are a slider, switches and a radiogroup with clear labels.
- The preview is aria-hidden; the summary text is the accessible readout.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-dsp; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Display Settings free?
- Yes. Display Settings is free for personal and commercial projects, with full source on this page.
- How do I install Display Settings?
- Run npx shadcn@latest add https://sundaeui.dev/r/display-settings.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Display Settings support keyboard and screen readers?
- Controls are a slider, switches and a radiogroup with clear labels. The preview is aria-hidden; the summary text is the accessible readout.
- Does Display Settings respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →Notification Matrix
Forms · RefinedSettings List
Forms · Refined- Pro
Shortcut Recorder
Forms · Refined Async Switch
Forms · Refined- Pro
Feature Flag
Forms · Refined Wi-Fi Networks
Forms · Refined