Your first component
Let's build something you can see and reuse: a card with a colored top border. Each time you use it, you can choose a new color and put different content inside.
The finished card runs with plain Python. You do not need to set up Django, FastAPI, or another web framework.
See the finished result before you start.
Before you start
It helps to recognize basic HTML, but you can copy the CSS as-is even if styling is new to you.
Create the card
A component is a reusable piece of HTML. You define it once, then use it wherever you need the same structure.
Save this as component.py:
"""A card with a colored top border and content of your choice."""
from __future__ import annotations
from citry import Component, SlotInput
class Card(Component):
"""Display any content inside a bordered card."""
class Kwargs:
accent: str
class Slots:
default: SlotInput
def css_data(self, kwargs: Kwargs, slots: Slots) -> dict[str, str]:
return {"accent": kwargs.accent}
template = """
<article class="demo-card">
<c-slot />
</article>
"""
css = """
.demo-card {
max-width: 24rem;
padding: 1rem 1.25rem;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
border-top: 0.25rem solid var(--accent);
border-radius: 8px;
background: Canvas;
color: CanvasText;
font-family: system-ui, sans-serif;
}
.demo-card__title {
margin: 0 0 0.25rem;
font-size: 1.1rem;
}
.demo-card__body {
margin: 0;
}
"""
It may look like a lot for a first component, so we'll unpack it one piece at a time.
Start with the HTML
The template is ordinary HTML with one Citry tag:
<article class="demo-card">
<c-slot />
</article>
<c-slot /> marks the place where the card's content will appear. When you write this:
<c-Card accent="#8250df">
<p>Hello from inside the card.</p>
</c-Card>
Citry puts the paragraph where <c-slot /> appears. This unnamed space is called the default slot.
Choose what can change
The two short classes near the top of Card describe the parts you can change each time you use it:
Kwargslists thename=valueoptions you can choose when you use the Card. This card has one option,accent, which chooses the border color.Slotslists places where text or HTML can go. This card has one place,default, for everything written between<c-Card>and</c-Card>.
The css_data() method sends the chosen accent to CSS as --accent.
Neither accent nor default has a fallback value, so you need to provide both when you use the card.
Add the styles
The css block travels with the Card. Citry adds it to any page that renders the component, and var(--accent) picks up the color you chose.
The .demo-card selector behaves like ordinary CSS: it styles every matching element on the page. Give component classes distinctive names so they do not accidentally style something else.
Use the card in a template
Here is the important part of the complete example page:
<c-Card accent="#8250df">
<h2 class="demo-card__title">Welcome</h2>
<p class="demo-card__body">
Choose the accent color, then add any content you like.
</p>
</c-Card>
The accent option makes the top border purple. The heading and paragraph go inside the card because they sit between its opening and closing tags.
Render it from Python
You can create the same Card directly from Python. Save this as render.py next to component.py:
from component import Card
card = Card(
accent="#8250df",
slots={"default": "Build something useful."},
)
print(card)
The slots dictionary is the Python way to fill the same default slot. Run the file:
python render.py
Citry prints the card's HTML and the styles it needs. The result looks like this (shortened):
<style>
/* Citry adds --accent: #8250df for this card. */
.demo-card {
border-top: 0.25rem solid var(--accent);
}
</style>
<article class="demo-card">
Build something useful.
</article>
The real HTML contains a few extra attributes that Citry uses. You do not need to write or remember them.
Use a different color for each card
The rules in Card.css are shared by every Card on the page. The value from css_data() stays with the Card it came from, so one Card can be blue while another is orange.
Save this as two_cards.py next to component.py:
from citry import Component
from component import Card
class CardList(Component):
template = """
<c-Card accent="#0969da">
<p>Blue card: Prepare the first draft.</p>
</c-Card>
<c-Card accent="#bc4c00">
<p>Orange card: Review the final copy.</p>
</c-Card>
"""
print(CardList())
Run python two_cards.py. Both cards use the same HTML and CSS, but each card keeps the color and text given to it.
This is useful whenever several copies of a component should share the same layout but keep their own colors, sizes, or other CSS values.
If you leave something out
Both the accent color and the content are required. If you forget the color, Citry cannot finish the Card:
str(Card(slots={"default": "Where is my color?"}))
# TypeError: Card.Kwargs.__init__() missing ... 'accent'
Add accent and the Card renders:
str(
Card(
accent="#8250df",
slots={"default": "Now the card has everything it needs."},
)
)
The same thing happens if you leave out the default slot. Citry checks these values when it turns the Card into HTML, so the error appears at str() or print(), not when Python first reaches Card(...).
Note
accent: str helps your editor and type checker, but it does not reject accent=123 while your program runs. If values come from a form, an API, or another source you do not control, read Typing and validation to add runtime checks.
What to try next
You now have a Card that:
- asks for an accent color;
- places text or HTML where
<c-slot />appears; - lets several Cards use different colors; and
- reports an error when required information is missing.
You can open the Card recipe to compare the component, complete page, and live result. To continue the guided journey, use Python data in components.