An example drop-in stylesheet — pure tag + data-* selectors, no class names in the markup. The library itself ships no CSS; this is just one opinionated look you can copy and tweak.
/* ---------------------------------------------------------------
json-schema-to-form — default stylesheet
Pure tag + data-* selectors; zero class names in the markup.
Drop-in on any form rendered by the library.
--------------------------------------------------------------- */
:root {
--jsf-bg: #ffffff;
--jsf-surface: #ffffff;
--jsf-surface-2: #f7f8fa;
--jsf-border: #d9dde3;
--jsf-border-2: #c0c6cf;
--jsf-text: #1c1f24;
--jsf-muted: #5a6270;
--jsf-accent: #2563eb;
--jsf-accent-bg: #eff5ff;
--jsf-danger: #dc2626;
--jsf-radius: 8px;
--jsf-radius-sm: 6px;
--jsf-gap: 10px;
--jsf-pad-y: 9px;
--jsf-pad-x: 12px;
--jsf-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--jsf-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
--jsf-focus: 0 0 0 3px rgba(37, 99, 235, 0.25);
}
form {
font-family: var(--jsf-font);
color: var(--jsf-text);
display: flex;
flex-direction: column;
gap: 16px;
line-height: 1.45;
}
/* --- every row --- */
form [data-name] {
display: flex;
flex-direction: column;
gap: var(--jsf-gap);
}
/* --- labels & legends --- */
form [data-name] > label,
form [data-name] > legend {
font-weight: 600;
font-size: 0.9375rem;
color: var(--jsf-text);
padding: 0;
}
/* required marker — only the field's own label/legend, not option labels */
form [data-required]:not([data-variant="group"]) > label::after,
form [data-required] > legend::after {
content: " *";
color: var(--jsf-danger);
font-weight: 700;
}
/* --- hint text --- */
form [data-name] > small {
color: var(--jsf-muted);
font-size: 0.8125rem;
line-height: 1.4;
}
/* --- controls (text-like) --- */
form input,
form select,
form textarea {
font: inherit;
color: inherit;
background: var(--jsf-surface);
border: 1px solid var(--jsf-border);
border-radius: var(--jsf-radius-sm);
padding: var(--jsf-pad-y) var(--jsf-pad-x);
width: 100%;
box-sizing: border-box;
box-shadow: var(--jsf-shadow);
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
}
form input:hover,
form select:hover,
form textarea:hover {
border-color: var(--jsf-border-2);
}
form input:focus,
form select:focus,
form textarea:focus {
outline: none;
border-color: var(--jsf-accent);
box-shadow: var(--jsf-focus);
}
form input:disabled,
form select:disabled,
form textarea:disabled {
background: var(--jsf-surface-2);
color: var(--jsf-muted);
cursor: not-allowed;
}
/* Invalid state (fires after the user has interacted) */
form input:user-invalid,
form select:user-invalid,
form textarea:user-invalid {
border-color: var(--jsf-danger);
}
form textarea {
min-height: 6em;
resize: vertical;
}
form select[multiple] {
min-height: 7.5em;
padding: 4px;
}
form select[multiple] option {
padding: 6px 10px;
border-radius: var(--jsf-radius-sm);
}
/* --- range --- */
form [data-widget="range"] input {
padding: 0;
border: none;
box-shadow: none;
background: transparent;
accent-color: var(--jsf-accent);
}
/* --- checkbox / radio (single boolean row) ---
Row has <input> then <label>; render them inline. */
form [data-widget="checkbox"][data-type="boolean"] {
flex-direction: row;
align-items: center;
gap: 8px;
}
form [data-widget="checkbox"][data-type="boolean"] > input {
width: auto;
margin: 0;
box-shadow: none;
accent-color: var(--jsf-accent);
}
form [data-widget="checkbox"][data-type="boolean"] > label {
font-weight: 500;
margin: 0;
}
/* --- enum groups: radios or multi-checkboxes --- */
form fieldset[data-variant="group"] {
border: 1px solid var(--jsf-border);
border-radius: var(--jsf-radius);
background: var(--jsf-surface);
padding: 10px 14px 12px;
box-shadow: var(--jsf-shadow);
display: flex;
flex-direction: column;
gap: 6px;
margin: 0;
}
form fieldset[data-variant="group"] > legend {
padding: 0 4px;
margin-left: -4px;
}
form fieldset[data-variant="group"] > label {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
padding: 4px 8px;
border-radius: var(--jsf-radius-sm);
font-weight: 500;
cursor: pointer;
user-select: none;
transition: background 120ms ease;
}
form fieldset[data-variant="group"] > label:hover {
background: var(--jsf-accent-bg);
}
form fieldset[data-variant="group"] > label > input {
width: auto;
margin: 0;
box-shadow: none;
accent-color: var(--jsf-accent);
}
/* Horizontal flow for enum groups with 4 or fewer visible children */
form fieldset[data-variant="group"] {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
form fieldset[data-variant="group"] > legend {
flex-basis: 100%;
margin-bottom: 2px;
}
form fieldset[data-variant="group"] > small {
flex-basis: 100%;
}
/* --- nested object blocks --- */
form fieldset[data-variant="object"] {
border: 1px solid var(--jsf-border);
border-radius: var(--jsf-radius);
background: var(--jsf-surface-2);
padding: 14px 16px 16px;
margin: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
form fieldset[data-variant="object"] > legend {
padding: 0 6px;
margin-left: -6px;
font-size: 0.9375rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--jsf-muted);
}
/* --- submit buttons placed alongside the form --- */
form button[type="submit"] {
align-self: flex-start;
background: var(--jsf-accent);
color: white;
border: 1px solid var(--jsf-accent);
border-radius: var(--jsf-radius-sm);
padding: 10px 18px;
font: inherit;
font-weight: 600;
cursor: pointer;
box-shadow: var(--jsf-shadow);
transition: filter 120ms ease, transform 60ms ease;
}
form button[type="submit"]:hover {
filter: brightness(1.05);
}
form button[type="submit"]:active {
transform: translateY(1px);
}
<form method="post" action="#">
<div data-name="email" data-type="string" data-widget="email" data-required="true">
<label for="email">Email</label>
<input id="email" name="email" type="email" required="" aria-required="true" aria-describedby="email-hint"/>
<small id="email-hint">Used for sign-in and account recovery.</small>
</div>
<div data-name="handle" data-type="string" data-widget="text" data-required="true">
<label for="handle">Handle</label>
<input id="handle" name="handle" type="text" required="" aria-required="true" aria-describedby="handle-hint" minlength="3" maxlength="24" pattern="^[a-zA-Z0-9_]+$" title="^[a-zA-Z0-9_]+$"/>
<small id="handle-hint">3-24 letters, numbers, or underscores.</small>
</div>
<div data-name="homepage" data-type="string" data-widget="url">
<label for="homepage">Homepage</label>
<input id="homepage" name="homepage" type="url" aria-describedby="homepage-hint"/>
<small id="homepage-hint">Optional — link to your site or profile.</small>
</div>
<div data-name="plan" data-type="string" data-widget="select" data-required="true">
<label for="plan">Plan</label>
<select id="plan" name="plan" required="" aria-required="true" aria-describedby="plan-hint">
<option value="free">free</option>
<option value="pro" selected="">pro</option>
<option value="team">team</option>
</select>
<small id="plan-hint">You can upgrade or downgrade any time.</small>
</div>
<fieldset data-variant="group" data-name="billing" data-type="string" data-widget="radio">
<legend>Billing cycle</legend>
<label>
<input type="radio" name="billing" value="monthly"/>
<span>monthly</span>
</label>
<label>
<input type="radio" name="billing" value="yearly" checked=""/>
<span>yearly</span>
</label>
</fieldset>
<div data-name="seats" data-type="integer" data-widget="range">
<label for="seats">Seats</label>
<input id="seats" name="seats" type="range" aria-describedby="seats-hint" value="5" min="1" max="50" step="1"/>
<small id="seats-hint">Only relevant for team plans.</small>
</div>
<div data-name="notifications" data-type="boolean" data-widget="checkbox">
<input id="notifications" name="notifications" type="checkbox" checked=""/>
<label for="notifications">Email me product updates</label>
</div>
<fieldset data-variant="group" aria-required="true" aria-describedby="topics-hint" data-name="topics" data-type="array" data-widget="checkbox" data-required="true">
<legend>Topics you care about</legend>
<label>
<input type="checkbox" name="topics" value="frontend" checked=""/>
<span>frontend</span>
</label>
<label>
<input type="checkbox" name="topics" value="backend" checked=""/>
<span>backend</span>
</label>
<label>
<input type="checkbox" name="topics" value="design"/>
<span>design</span>
</label>
<label>
<input type="checkbox" name="topics" value="ops"/>
<span>ops</span>
</label>
<small id="topics-hint">Pick at least one.</small>
</fieldset>
<div data-name="channels" data-type="array" data-widget="select">
<label for="channels">Notification channels</label>
<select id="channels" name="channels" multiple="">
<option value="email" selected="">email</option>
<option value="in-app" selected="">in-app</option>
<option value="sms">sms</option>
<option value="push">push</option>
<option value="rss">rss</option>
</select>
</div>
<fieldset data-variant="object" data-name="profile" data-type="object" data-widget="object">
<legend>Profile</legend>
<div data-name="profile.name" data-type="string" data-widget="text">
<label for="profile-name">Full name</label>
<input id="profile-name" name="profile.name" type="text" minlength="1"/>
</div>
<div data-name="profile.bio" data-type="string" data-widget="textarea">
<label for="profile-bio">Short bio</label>
<textarea id="profile-bio" name="profile.bio" aria-describedby="profile-bio-hint" maxlength="280">
</textarea>
<small id="profile-bio-hint">Up to 280 characters.</small>
</div>
</fieldset> <button type="submit">Create account</button>
</form>