Step 2
Configure the API and styling.
Add clean CSS and prepare the JavaScript state. Replace the public key with the key from your CrownVote election dashboard.
code index.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f4f7fb;
color: #101828;
}
.app {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.card {
width: 100%;
max-width: 760px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 28px;
padding: 32px;
box-shadow: 0 20px 60px rgba(16, 24, 40, 0.08);
}
.eyebrow {
margin: 0 0 12px;
color: #2563eb;
font-weight: 700;
font-size: 14px;
}
h1 {
margin: 0;
font-size: clamp(30px, 5vw, 48px);
letter-spacing: -0.04em;
}
.muted {
color: #667085;
line-height: 1.7;
}
.form {
margin-top: 28px;
display: grid;
gap: 16px;
}
label {
display: grid;
gap: 8px;
font-size: 14px;
font-weight: 700;
}
input {
width: 100%;
border: 1px solid #d0d5dd;
border-radius: 14px;
padding: 14px 16px;
font: inherit;
}
button {
border: 0;
border-radius: 999px;
padding: 14px 18px;
background: #2563eb;
color: white;
font-weight: 800;
cursor: pointer;
}
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.notice {
margin-top: 20px;
padding: 14px 16px;
border-radius: 16px;
background: #eff6ff;
color: #1d4ed8;
font-size: 14px;
line-height: 1.6;
}
.voter {
margin-top: 24px;
padding: 14px 16px;
border-radius: 16px;
background: #f9fafb;
color: #344054;
}
.ballots {
margin-top: 24px;
display: grid;
gap: 24px;
}
.ballot {
border: 1px solid #e5e7eb;
border-radius: 24px;
padding: 20px;
background: #ffffff;
}
.ballot-header {
margin-bottom: 18px;
}
.ballot h2 {
margin: 0;
font-size: 22px;
letter-spacing: -0.02em;
}
.candidates {
display: grid;
gap: 14px;
}
.candidate {
position: relative;
display: grid;
grid-template-columns: auto 56px 1fr auto;
align-items: center;
gap: 14px;
border: 1px solid #e5e7eb;
border-radius: 18px;
padding: 14px;
cursor: pointer;
background: #ffffff;
transition:
border-color 0.2s ease,
background 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.candidate:hover {
border-color: #2563eb;
background: #eff6ff;
transform: translateY(-1px);
box-shadow: 0 12px 30px rgba(37, 99, 235, 0.08);
}
.candidate.selected {
border-color: #2563eb;
background: #eff6ff;
box-shadow: 0 12px 30px rgba(37, 99, 235, 0.12);
}
.candidate input {
width: 18px;
height: 18px;
accent-color: #2563eb;
}
.candidate-image {
width: 56px;
height: 56px;
overflow: hidden;
border-radius: 16px;
background: #dbeafe;
color: #2563eb;
display: grid;
place-items: center;
font-weight: 900;
}
.candidate-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.candidate-content {
display: grid;
gap: 4px;
}
.candidate-content strong {
font-size: 15px;
color: #101828;
}
.candidate-content small {
color: #667085;
line-height: 1.4;
}
.candidate-check {
width: 28px;
height: 28px;
border-radius: 999px;
display: grid;
place-items: center;
background: #2563eb;
color: #ffffff;
font-size: 14px;
font-weight: 900;
opacity: 0;
transform: scale(0.85);
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.candidate.selected .candidate-check {
opacity: 1;
transform: scale(1);
}
@media (max-width: 520px) {
.candidate {
grid-template-columns: auto 48px 1fr;
}
.candidate-image {
width: 48px;
height: 48px;
border-radius: 14px;
}
.candidate-check {
display: none;
}
}
.hidden {
display: none;
} code index.js
const API_URL = 'https://api.crownvote.com/api/v1'
const PUBLIC_KEY = 'cv_pub_xxxxx'
const state = {
election: null,
accessToken: null,
ballots: [],
selections: {},
}
const elements = {
title: document.querySelector('#electionTitle'),
description: document.querySelector('#electionDescription'),
notice: document.querySelector('#notice'),
loginForm: document.querySelector('#loginForm'),
voterId: document.querySelector('#voterId'),
password: document.querySelector('#password'),
ballotScreen: document.querySelector('#ballotScreen'),
voterInfo: document.querySelector('#voterInfo'),
ballots: document.querySelector('#ballots'),
submitVote: document.querySelector('#submitVote'),
}
function showNotice(message) {
elements.notice.textContent = message
elements.notice.classList.remove('hidden')
}
function hideNotice() {
elements.notice.textContent = ''
elements.notice.classList.add('hidden')
}