Escape

Report card template for JavaScript

A report card goes home with every student at the end of term, so a school prints hundreds in a week. Each one has the same layout and different marks. The marks already sit in the school’s records. What turns a percentage into a letter grade is a policy, and it has to be the same on every card. This is a ready-made quario report card — copy it, point it at your grades, and render to PDF, Word, Excel, or HTML.

The template

The whole report card is a definition — data you can version and review. The school line, the student block, the subjects by department, the averages, the tutor’s comment and the three signatures are all in here. This is the exact file the preview below renders:

report-card.report.json
{
  "data": "$.report.subjects[*]",
  "aggregates": { "average": "avg:[email protected]", "subjects": "count" },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "{{ $.input.report.school.name }}", "width": 60,
          "style": { "size": 10, "color": "#1e1f22" } },
        { "type": "text", "value": "{{ $.input.report.school.street }}, {{ $.input.report.school.city }}",
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceBefore": 28 },
      "slots": [
        { "type": "text", "value": "Report card", "width": 60,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "{{ $.input.report.term }} · Issued " }, { "value": "{{ $.input.report.issued }}" }],
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 34 }, { "width": 22 }, { "width": 22 }, {}],
      "rows": [
        {
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97", "paddingTop": 26 },
          "slots": [
            { "type": "text", "value": "Student" },
            { "type": "text", "value": "Year" },
            { "type": "text", "value": "Tutor" },
            { "type": "text", "value": "Attendance" }
          ]
        },
        {
          "style": { "size": 10, "bold": false, "color": "#1e1f22", "paddingBottom": 26 },
          "slots": [
            { "type": "text", "value": "{{ $.input.report.student.name }} · {{ $.input.report.student.number }}", "style": { "bold": true } },
            { "type": "text", "value": "{{ $.input.report.student.year }}" },
            { "type": "text", "value": "{{ $.input.report.student.tutor }}" },
            { "type": "text", "value": "{{ $.input.report.attendance.present }} of {{ $.input.report.attendance.sessions }} days" }
          ]
        }
      ]
    }
  ],
  "groups": [
    {
      "name": "department",
      "by": "[email protected]",
      "aggregates": { "average": "avg:[email protected]" },
      "header": [
        {
          "type": "split",
          "style": { "spaceBefore": 12, "spaceAfter": 2 },
          "slots": [
            { "type": "text", "value": "{{ department.key }}", "width": 50,
              "style": { "size": 11, "bold": true, "color": "#1e1f22" } },
            { "type": "text", "value": [{ "value": "Average " }, { "value": "{{ department.average / 100 }}", "style": { "format": { "kind": "percent", "digits": 0 } } }],
              "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right" } }
          ]
        }
      ]
    }
  ],
  "detail": {
    "header": {
      "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
        "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
    },
    "row": { "style": { "color": "#5e6062" } },
    "columns": [
      { "header": "Subject", "value": "{{ @.subject }}", "width": 18, "style": { "color": "#1e1f22" } },
      { "header": "Teacher", "value": "{{ @.teacher }}", "width": 20 },
      { "header": { "value": "Score", "style": { "align": "right" } }, "value": "{{ @.score / 100 }}", "width": 10,
        "style": { "align": "right", "color": "#1e1f22", "format": { "kind": "percent", "digits": 0 } } },
      { "header": { "value": "Grade", "style": { "align": "center" } },
        "value": "{{ @.score >= 85 ? 'A' : @.score >= 70 ? 'B' : @.score >= 55 ? 'C' : @.score >= 40 ? 'D' : 'E' }}", "width": 10,
        "style": { "align": "center", "bold": true, "color": "#1e1f22" } },
      { "header": "Comment", "value": "{{ @.comment }}", "width": 42, "style": { "size": 9 } }
    ]
  },
  "footer": [
    {
      "type": "split",
      "style": { "spaceBefore": 14, "paddingTop": 6,
        "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
      "slots": [
        { "type": "text", "value": "{{ $.subjects }} subjects", "width": 38,
          "style": { "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "Overall average " }, { "value": "{{ round($.average, 0) / 100 }}", "style": { "format": { "kind": "percent", "digits": 0 } } }],
          "style": { "bold": true, "color": "#1e1f22", "align": "right" } }
      ]
    },
    {
      "type": "text",
      "value": "Grades: A 85% and over · B 70–84% · C 55–69% · D 40–54% · E below 40%",
      "style": { "size": 8, "color": "#989a97", "spaceBefore": 4 }
    },
    {
      "type": "split",
      "style": { "spaceBefore": 26 },
      "slots": [
        { "type": "text", "value": "Tutor's comment", "width": 22,
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
        { "type": "text", "value": "{{ $.input.report.tutorComment }}", "style": { "color": "#1e1f22" } }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 30 }, { "width": 5 }, { "width": 30 }, { "width": 5 }, {}],
      "rows": [
        {
          "style": { "paddingTop": 64 },
          "slots": [
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } },
            { "type": "text", "value": "" },
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } },
            { "type": "text", "value": "" },
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } }
          ]
        },
        {
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97", "paddingTop": 4 },
          "slots": [
            { "type": "text", "value": "Tutor" },
            { "type": "text", "value": "" },
            { "type": "text", "value": "Principal" },
            { "type": "text", "value": "" },
            { "type": "text", "value": "Parent or guardian" }
          ]
        }
      ]
    }
  ],
  "page": {
    "footer": [
      {
        "type": "text",
        "value": "Scores are the term's combined coursework and assessment marks. Questions about this report go to the student's tutor.",
        "style": { "size": 8, "color": "#989a97", "paddingTop": 10,
          "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" }
      }
    ]
  }
}

The grade bands live in the definition, not in your code. The Grade column is one expression, @.score >= 85 ? 'A' : @.score >= 70 ? 'B' : …, and the line under the table prints the same bands in words. A school that moves the boundary for an A changes this file, and the change shows up in review beside the wording that explains it.

The departments are a group on [email protected]. Each one carries department.average, an avg over its own subjects, which its header prints on the right. The overall average in the footer is a report aggregate over every subject. round($.average, 0) makes sure the stored value is the whole percentage the card prints.

The data stores scores as whole numbers out of 100, and the cells divide by 100 under the percent kind with digits: 0. So 84 reads 84% on the card and is a real percentage in the workbook.

The data

A definition renders nothing on its own — you hand it a document. This is the sample the preview below uses. $.report.subjects[*] in the definition selects its rows, and $.input reads the student, the attendance and the comment:

report-card.data.json
{
  "report": {
    "term": "Autumn term 2026",
    "issued": "2026-12-18",
    "school": {
      "name": "Liffey Hill Secondary School",
      "street": "4 Ormond Quay Upper",
      "city": "Dublin 7"
    },
    "student": { "name": "Niamh Byrne", "number": "S-2291", "year": "Third year", "tutor": "Ms R. Doyle" },
    "attendance": { "present": 71, "sessions": 74 },
    "tutorComment": "Niamh has had a strong term. Her project work in science and art shows real care, and she now asks for help early when a topic doesn't click. Mathematics is the one subject to keep an eye on next term.",
    "subjects": [
      { "department": "Languages", "subject": "English", "teacher": "Mr P. Walsh", "score": 84, "comment": "Thoughtful essays; quote more from the text." },
      { "department": "Languages", "subject": "Irish", "teacher": "Ms S. Ní Bhriain", "score": 76, "comment": "Oral work much improved." },
      { "department": "Languages", "subject": "French", "teacher": "Mme C. Laurent", "score": 88, "comment": "Excellent listening and reading." },
      { "department": "Sciences", "subject": "Mathematics", "teacher": "Mr D. Kelly", "score": 63, "comment": "Algebra needs regular practice." },
      { "department": "Sciences", "subject": "Science", "teacher": "Dr A. Murphy", "score": 91, "comment": "Outstanding lab report on density." },
      { "department": "Humanities", "subject": "History", "teacher": "Mr J. O'Neill", "score": 79, "comment": "Good use of sources." },
      { "department": "Humanities", "subject": "Geography", "teacher": "Ms L. Kavanagh", "score": 72, "comment": "Fieldwork write-up was rushed." },
      { "department": "Arts", "subject": "Art", "teacher": "Ms E. Brennan", "score": 93, "comment": "A portfolio to be proud of." },
      { "department": "Arts", "subject": "Music", "teacher": "Mr T. Quinn", "score": 81, "comment": "Confident ensemble playing." }
    ]
  }
}

What it renders

This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders them in your browser, right here. The PDF button hands you the same report card as real @quario/pdf bytes, one A4 page. The DOCX button beside it hands the same definition to @quario/docx, where Word paginates and resolves the faces itself:

Rendering live with @quario/viewerA4

Use it

shell
npm install quario @quario/pdf

A year group is one definition and one data document per student. Compile once, then render in a loop:

report-cards.ts
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import reportCard from './report-card.report.json' with { type: 'json' }

const report = quario({ locale: 'en-IE' }).report(reportCard)
const target = pdf({ page: { size: 'A4', margin: 54 } })

for (const card of await getReportCards('2026-autumn', 3)) {   // your student records
  writeFileSync(`report-card-${card.student.number}.pdf`, await report.render(target, { report: card }))
}

The issue date presents through the instance’s locale, 18 Dec 2026 under en-IE. The card holds no money, so the instance needs no currency.

Make it yours

Because the report card is declarative, changing it means changing data. A different grading scale is the one expression in the Grade column and the line that explains it. Effort marks are one more field per subject and one more column. A comment per department is a group footer. A card per term across a whole year is a group on the term with "break": "before". It renders without eval, so a teacher’s comment never becomes code. Security →

FAQ

Can I print a whole year group at once?

Yes. Render one PDF per student, as the sample above does, and hand them to a print queue. For one file to print in one run, put every student in one data document and add a group on the student with "break": "before".

Can I export the grades to Excel?

Yes — @quario/xlsx renders the same definition to a workbook. Each department lands as its own section, and every score is a real percentage the head of year can sort and average. Full example →

Can I use this report card template commercially?

The definition is yours — the report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Sending real report cards home is past evaluation, so that needs a license for each production deployment. Its users, agents, and renders are never counted. Pricing →

Where are the other templates?

Browse the template gallery → — certificates, resumes, summary reports, and more.