26 · Live interface
Components: buttons, fields and cards
Real components, with all their states. The preview and the code are the same: what you see is what you copy.
Buttons
Primary button states, in order: rest, hover, focus, active and disabled.
Fields
As it appears on the invoice.
State: focus
The domain is missing. Example: ana@example.com
State: disabled
Receive email notifications
Cards
Simple cardThin border, no shadow. Holds a single topic.
Link cardThe whole block is the link. It lifts 2 px on hover.
Code
:root{
--nt-bg:#ffffff; --nt-fg:#0b0c0d; --nt-muted:#5b5f66; --nt-line:rgba(11,12,13,.18);
--nt-surface:#f2f2f3; --nt-accent:#0b0c0d; --nt-on-accent:#ffffff;
--nt-r:10px; --nt-font:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
}
[data-theme="dark"]{
--nt-bg:#0b0c0d; --nt-fg:#f7f8f8; --nt-muted:#9aa0a9; --nt-line:rgba(255,255,255,.18);
--nt-surface:rgba(255,255,255,.05); --nt-accent:#f7f8f8; --nt-on-accent:#0b0c0d;
}.nt-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 20px;
border:1px solid var(--nt-line);border-radius:var(--nt-r);background:transparent;color:var(--nt-fg);
font:500 15px/1 var(--nt-font);cursor:pointer;transition:background .2s,border-color .2s,transform .12s}
.nt-btn:hover,.nt-btn.is-hover{background:var(--nt-surface);border-color:var(--nt-fg)}
.nt-btn:focus-visible,.nt-btn.is-focus{outline:2px solid var(--nt-fg);outline-offset:3px}
.nt-btn:active,.nt-btn.is-active{transform:translateY(1px)}
.nt-btn--primary{background:var(--nt-accent);border-color:var(--nt-accent);color:var(--nt-on-accent)}
.nt-btn--primary:hover,.nt-btn--primary.is-hover{background:var(--nt-accent);opacity:.88}
.nt-btn--ghost{border-color:transparent}
.nt-btn[disabled],.nt-btn.is-disabled{opacity:.4;cursor:not-allowed;pointer-events:none}
.nt-field{display:grid;gap:6px;font:500 14px var(--nt-font);color:var(--nt-fg)}
.nt-input{min-height:44px;padding:0 14px;border:1px solid var(--nt-line);border-radius:var(--nt-r);
background:var(--nt-bg);color:var(--nt-fg);font:400 16px var(--nt-font)}
.nt-input:hover,.nt-input.is-hover{border-color:var(--nt-muted)}
.nt-input:focus,.nt-input.is-focus{outline:2px solid var(--nt-fg);outline-offset:2px;border-color:var(--nt-fg)}
.nt-input[aria-invalid="true"],.nt-input.is-error{border:2px solid var(--nt-fg)}
.nt-input[disabled],.nt-input.is-disabled{opacity:.45;cursor:not-allowed}
.nt-hint{font:400 13px var(--nt-font);color:var(--nt-muted)}
.nt-error{display:flex;gap:6px;align-items:center;font:600 13px var(--nt-font)}
.nt-card{padding:22px;border:1px solid var(--nt-line);border-radius:calc(var(--nt-r) + 4px);background:var(--nt-bg);color:var(--nt-fg)}
.nt-card--link{transition:border-color .2s,transform .2s}
.nt-card--link:hover{border-color:var(--nt-fg);transform:translateY(-2px)}
.nt-alert{display:flex;gap:12px;padding:14px 16px;border:1px solid var(--nt-line);border-left:4px solid var(--nt-fg);
border-radius:var(--nt-r);background:var(--nt-surface);color:var(--nt-fg);font:400 15px/1.5 var(--nt-font)}
.nt-alert--error{border-width:2px;border-left-width:6px}
.nt-alert--warn{border-style:dashed;border-left-style:solid}
.nt-alert b{display:block}
.nt-tabs{display:flex;gap:4px;border-bottom:1px solid var(--nt-line)}
.nt-tab{min-height:44px;padding:0 16px;border:0;border-bottom:2px solid transparent;background:none;color:var(--nt-muted);font:500 15px var(--nt-font);cursor:pointer}
.nt-tab[aria-selected="true"]{color:var(--nt-fg);border-bottom-color:var(--nt-fg)}
.nt-tab:focus-visible{outline:2px solid var(--nt-fg);outline-offset:-2px}
.nt-table{width:100%;border-collapse:collapse;font:400 14px var(--nt-font);color:var(--nt-fg)}
.nt-table th{padding:10px 14px;text-align:left;font:500 12px "JetBrains Mono",monospace;text-transform:uppercase;letter-spacing:.05em;color:var(--nt-muted);border-bottom:1px solid var(--nt-line)}
.nt-table td{padding:12px 14px;border-bottom:1px solid var(--nt-line)}
.nt-table .num{text-align:right;font-variant-numeric:tabular-nums}
.nt-table tbody tr:hover{background:var(--nt-surface)}
.nt-switch{position:relative;width:48px;height:28px;border:1px solid var(--nt-line);border-radius:999px;background:transparent;cursor:pointer}
.nt-switch::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:var(--nt-muted);transition:transform .2s}
.nt-switch[aria-checked="true"]{background:var(--nt-accent);border-color:var(--nt-accent)}
.nt-switch[aria-checked="true"]::after{transform:translateX(20px);background:var(--nt-on-accent)}<button class="nt-btn nt-btn--primary">Create client</button>
<div class="nt-field">
<label for="email">Email</label>
<input id="email" class="nt-input" type="email" aria-describedby="email-hint">
<span class="nt-hint" id="email-hint">As it appears on the invoice.</span>
</div>
<!-- Error: text + icon + 2 px border, not just color -->
<input class="nt-input" aria-invalid="true" aria-describedby="email-error">
<span class="nt-error" id="email-error">The domain is missing.</span>Rules
Do
- One primary button per view
- Visible labels above the field
- Errors with text, icon and a 2 px border
- Minimum height of 44 px on everything touchable
Don't
- Two primary buttons side by side
- Placeholder as a label
- Showing an error with color alone
- Buttons under 44 px tall on touch screens