# Build a Markdoc tag This topic explains how to register custom components that can be invoked by your content creators in Markdoc (Markdown) files. Follow the tutorial to create two sample components: a simple line break component and a quiz component for some extra credit. The line break is a very simple component, that adds a linebreak (`
`). In reality, you can achieve the linebreak in Markdoc by ending a line with a `\`. However, it makes for the simple "Hello World" of custom tags. And it requires almost no React knowledge. The quiz component, while a bit rough around the edges, is a lot more robust. To understand it, you'll need to understand React. However, if you do, and want to do more complex behavior, this is a good example to help you understand how to do that. ## Create directories If they don't exist yet, create the `@theme` directory in your project root. Inside of that, create a `markdoc` directory. This is what your directory structure may look after you follow this tutorial. ```treeview ├─ @theme/ | └── markdoc/ | ├── components.tsx | └── schema.ts ``` ## Create your components Create a file `components.tsx` inside of the `markdoc` directory, and paste the following contents into it. ```tsx import * as React from 'react'; export function Break() { return
; } ``` We'll create the `Quiz.tsx` as extra credit later. ## Create your tags Create a file `schema.ts` inside of the `markdoc` directory. Paste the following contents in the file. ```ts import type { Schema } from '@markdoc/markdoc'; export const tags: Record = { br: { render: 'Break', selfClosing: true, }, }; ``` You've created your first Markdoc tag. The only thing left to do is use it. In any file that ends with `.md` add the following `{% br /%}`. ## Add the Quiz component and tag Add `Quiz` component to the `components.tsx` file. You can either implement component inline or re-export it from another file. Let's implement it in `Quiz.tsx` and re-export it from `components.tsx`. ```tsx import * as React from 'react'; export { Quiz } from './components/Quiz'; export function Break() { return
; } ``` This is the directory structure after you've added the `Quiz` component. ```treeview ├─ @theme/ | ├── markdoc/ | ├── components.tsx | ├── schema.ts | └── components/ | └── Quiz.tsx ``` details summary See @theme/markdoc/components/Quiz.tsx ```tsx import * as React from 'react'; import styled from 'styled-components'; const { useState, useEffect, Fragment } = React; function Question({ question, setAnswerStatus }) { const [selectedAnswerIndex, setSelectedAnswerIndex] = useState(null); useEffect(() => { if (selectedAnswerIndex != null) { setAnswerStatus(selectedAnswerIndex === question.correctAnswerIndex); } }, [selectedAnswerIndex]); useEffect(() => { setSelectedAnswerIndex(null); }, [question]); const getClasses = (index) => { let classes = []; if (selectedAnswerIndex != null) { if (selectedAnswerIndex === index) { classes.push('selected'); } if (index === question.correctAnswerIndex) { if (selectedAnswerIndex === index) { classes.push('correct'); } else { classes.push('incorrect'); } } } return classes.join(' '); }; return ( {question.question} {question.answers.map((answer, index) => { return ( selectedAnswerIndex == null && setSelectedAnswerIndex(index)} > {answer} ); })} ); } function ProgressBar({ currentQuestionIndex, totalQuestionsCount }) { const progressPercentage = (currentQuestionIndex / totalQuestionsCount) * 100; return ( {currentQuestionIndex} answered ({totalQuestionsCount - currentQuestionIndex} remaining) ); } export function Quiz({ questions }) { const [questionIndex, setQuestionIndex] = useState(null); const [answerStatus, setAnswerStatus] = useState(null); const [correctAnswerCount, setCorrectAnswerCount] = useState(0); const [quizComplete, setQuizComplete] = useState(false); useEffect(() => { setAnswerStatus(null); }, [questionIndex]); useEffect(() => { if (answerStatus) { setCorrectAnswerCount((count) => count + 1); } }, [answerStatus]); const onNextClick = () => { if (questionIndex === questions.length - 1) { setQuizComplete(true); } else { setQuestionIndex(questionIndex == null ? 0 : questionIndex + 1); } }; const onRestartClick = () => { setQuizComplete(false); setQuestionIndex(null); setCorrectAnswerCount(0); }; if (questionIndex == null) { return (

Apply what was learned

); } return ( {quizComplete ? (

Quiz complete!

You answered {correctAnswerCount} questions correctly (out of a total {questions.length}{' '} questions)

) : ( {answerStatus != null && (
{!!answerStatus ? 'Correct! :)' : 'Your answer was incorrect :('}
)}
)} {questionIndex != null && ( )}
); } const Wrapper = styled.div` display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; width: 600px; margin: auto; `; const QuizWrapper = styled.div` display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; width: 600px; margin: auto; `; const Button = styled.button` background: #e8e8e8; border: 0; padding: 10px 20px; cursor: pointer; border-bottom: 3px solid #c9c9c9; border-radius: 3px; &.next { background: #6ad85c; border-bottom: 3px solid #5abc4e; } &.start { margin-top: 20px; } &.restart { margin-top: 20px; } `; const QuestionEl = styled.div` width: 100%; `; const QuestionText = styled.div` font-size: 1.2em; margin: 20px 0; `; const Answers = styled.div` margin-bottom: 20px; `; const AnswerElement = styled.div` padding: 4px; text-align: center; background: #f3f3f3; margin-bottom: 5px; border-radius: 3px; cursor: pointer; &.selected { background: gainsboro; } &.correct { background: #6ad85c; font-weight: bold; } &.incorrect { background: #df3636; font-weight: bold; } `; const AnswerStatusEl = styled.div` font-weight: bold; margin-bottom: 20px; `; const ProgressBarEl = styled.div` width: 100%; background: #f3f3f3; height: 20px; position: relative; display: flex; justify-content: center; align-items: center; border-radius: 3px; `; const ProgressBarInner = styled.div` background: #6ad85c; position: absolute; height: 100%; top: 0; left: 0; transition: ease all 0.5s; border-radius: 3px; `; const ProgressBarText = styled.div` font-size: 0.7em; position: absolute; z-index: 10; `; ``` Next, add the quiz tag schema to `schema.ts` ```ts import type { Schema } from '@markdoc/markdoc'; export const tags: Record = { br: { render: 'Break', selfClosing: true, }, quiz: { attributes: { questions: { type: 'Object', required: true, }, }, render: 'Quiz', // please make sure to export it in components.ts, selfClosing: true, }, }; ``` You can use the quiz tag. However, the quiz tag expects a property to define the quiz questions, which is easier to do in the front matter of the Markdown file. The front matter is passed in as shown below. ```markdoc --- questions: - question: Did you learn how to add a custom Markdoc tag? answers: - Yes - No - Maybe correctAnswerIndex: 1 - question: How many places do you need to adjust to configure custom Markdoc tags? answers: - '0' - '1' - '2' - '3' - '4' correctAnswerIndex: 4 --- # Hello Here is my quiz. {% quiz questions=$frontmatter.questions /%} ``` We'd show you a working quiz here, but we'd rather spend more time on it and make it look and function better first. ## Extra credit Make a custom tag of your own (it could be simple). Our philosophy is in line with Confucious: > I hear and I forget, I see and I remember, I do and I understand. Go do!