Skip to content

Components

Models Gemma 4 Uses Ollama

Every visual element on this site comes from this catalogue. Build pages from these rather than writing new CSS — if something you need isn't here, add it to docs/stylesheets/components.css and document it on this page.

Note

Colours, spacing, radii and shadows are tokens, defined once in docs/stylesheets/gemma.css. It is the only stylesheet allowed to contain a raw hex value, and scripts/lint_docs.py --check css enforces that.

Colour tokens

Brand blue has one knob. --gemma-blue-seed in docs/stylesheets/gemma.css is the only colour to change; every step, both schemes and every component follow from it:

--gemma-blue-seed: #0B57D0;

The steps are the seed mixed with white (black at the dark end) in sRGB, so they stay on the seed's own hue. With the current seed:

Step Hex Contrast Role
--gemma-blue-50 / -100 #EBF2FB / #CEDDF6 — Tints, hover washes.
--gemma-blue-300 #80A8E7 6.65:1 on ink Dark-scheme text.
--gemma-blue-500 #417CDA 4.10:1 under white Decorative only — gradients, 2px rules.
--gemma-blue-600 #2368D5 5.23:1 under white Dark-scheme fills.
--gemma-blue-700 #0B57D0 6.39:1 both ways The primary.
--gemma-blue-800 #0944A2 8.90:1 under white Pressed / strong.

Before committing a new seed, check it:

uv run python scripts/check_contrast.py --seed "#RRGGBB"

It prints the derived ramp and exits non-zero if any step misses the threshold its role needs, so a colour change never silently breaks legibility.

Use the semantic roles in components, not the ramp directly:

Token Use
--gemma-accent Brand colour that is text-safe in the current scheme. Prefer this.
--gemma-accent-fill A fill that carries white text. Never point it at a light step.
--gemma-white, --gemma-ink Surfaces.
--gemma-blue, -deep, -light Legacy aliases into the ramp. Prefer the roles above.

Contrast is symmetric

A colour too light to read on white is also too light to sit behind white text — it is the same ratio. The palette this replaced was built on #3186FF at 3.51:1, which failed both ways; that is what made it read neon. Fills carrying white text come from 600 or 700.

Scales: --gemma-space-1…7, --gemma-radius-sm|md|lg|pill, --gemma-shadow-sm|md|lg.

A URL used on more than one page goes in data/links.yml, never inline. ai.google.dev/responsible appears on 26 pages; when a URL like that moves, this is the one line that changes.

hf-gemma4:
  url: https://huggingface.co/collections/google/gemma-4
  model: gemma-4        # only when the URL names a generation

Pages use it as an ordinary reference link — no URL, no attributes:

Every checkpoint is in the [Hugging Face collection][hf-gemma4].

scripts/generate_snippets.py renders the definitions into snippets/links.md, which pymdownx.snippets appends to every page. A definition renders as nothing, so appending it everywhere is free.

The model: key is what pinned_links.py reports, so a link's generation is declared once, beside the URL, instead of repeated at every use site.

An unresolved reference fails silently

[text][typo] renders as literal text — brackets and all — rather than erroring. The references lint check catches it: every reference must resolve, either from data/links.yml or from a definition on the page itself.

Glossary

Terms go in data/glossary.yml and render as abbreviations with a hover definition. The theme gives them cursor: help and a dotted grey underline, so a term reads as a term and not as a link — a link is blue with a pointer cursor.

Unlike shared links, the glossary is opt-in per page:

<!-- GENERATED from data/glossary.yml by scripts/generate_snippets.py -- do not edit. -->

*[MTP]: Multi-Token Prediction — a small drafter model proposes several tokens at once and the main model verifies them in one pass
*[QAT]: Quantization-Aware Training — quantization is simulated during training so the model learns to compensate for it
*[PTQ]: Post-Training Quantization — compressing a finished model, which loses more quality than QAT at the same bit width
*[GGUF]: The file format llama.cpp and its ecosystem use for quantized weights
*[LoRA]: Low-Rank Adaptation — fine-tuning a small set of added weights instead of the whole model
*[QLoRA]: LoRA applied on top of a quantized base model, so fine-tuning fits on a single consumer GPU
*[SFT]: Supervised Fine-Tuning — training on prompt and response pairs
*[DPO]: Direct Preference Optimization — aligning a model from preferred and rejected response pairs
*[MoE]: Mixture of Experts — only a fraction of the model's parameters are active for any given token
*[PLE]: Per-Layer Embeddings — embedding tables kept in flash storage rather than in memory
*[KV cache]: The stored attention keys and values for tokens already processed; it grows with context length
*[IT]: Instruction-Tuned — trained to follow instructions, so it responds usefully without further training
*[PT]: Pre-Trained — the base model, before any instruction tuning
*[VLM]: Vision-Language Model — one that takes images alongside text
*[RAG]: Retrieval-Augmented Generation — retrieving relevant documents and putting them in the prompt
*[TPU]: Tensor Processing Unit — Google's accelerator for machine learning workloads
*[PEFT]: Parameter-Efficient Fine-Tuning — tuning a small subset of parameters rather than updating all model weights
*[FT]: Fine-Tuned — specialized for a specific benchmark, dataset, or domain task
*[ASR]: Automatic Speech Recognition — transcribing spoken audio into text
*[VQA]: Visual Question Answering — answering questions about visual content in an image
*[OCR]: Optical Character Recognition — extracting written or printed text directly from images
*[RLHF]: Reinforcement Learning from Human Feedback — aligning model behavior with human preferences

Python-Markdown marks up every occurrence of a term, so a global glossary would put eleven dotted underlines on the MTP page and forty-one on GGUF across the site. Include it where the acronyms are used but not explained; leave it off the page that is itself the explanation.

Icons

Five icon sets ship with Zensical: lucide (line icons, the default here), material, fontawesome, octicons, and simple (brand logos). Project-specific icons go in overrides/.icons/.

In prose, use the shortcode:

Run it locally :lucide-rocket: and you are done.

In markup — any card, grid or generated block — use the icon() macro instead:

<span class="card-icon">{{ icon('lucide/rocket') }}</span>
{{ icon('simple/ollama', 'engine-logo') }}

The shortcode does not work inside HTML

:lucide-rocket: is an inline Markdown construct. Inside a raw HTML block — which every card grid is — it stays literal text. Adding markdown to the block does render it, but md_in_html then wraps each card in a <p> and the grid layout breaks. The icon() macro runs in the Jinja preprocessor, before Markdown, so the SVG is already in place. It also reaches the grids generated from data/*.yml, where a shortcode cannot go at all.

Icons inherit currentColor, so set the colour on the parent and never on the SVG. Size comes from CSS in icons.css; the macro strips the file's own width/height so the sets cannot disagree.

Do not use emoji shortcodes

:rocket: renders as <img src="https://cdn.jsdelivr.net/...twemoji..."> — an external CDN request per emoji, which breaks offline viewing and adds a third-party dependency. Raw Unicode emoji avoid the request but render in the system font, so they differ on every platform and cannot be styled. Use an icon set.

Boxes

Every bordered container on the site — cards, engine tiles, tree nodes, tables — uses one recipe. Only padding changes.

background: var(--gemma-surface);
border: 1px solid var(--gemma-border);
border-radius: var(--gemma-radius-md);
/* Interactive boxes, on hover and focus */
border-color: var(--gemma-accent);
box-shadow: var(--gemma-shadow-sm);
Density Padding Used by
Compact --gemma-space-2 / -3 Engine tiles, tree nodes
Default --gemma-space-4 / -5 Cards
Panel --gemma-space-3 / -4 Admonitions, on --gemma-surface-sunken with a rail

Do not reach for Material's --md-default-fg-color--lighter for a border. It is an 0.32-alpha black that draws a visibly heavier line than --gemma-border; cards used it for a long time, which is why they read as a different design language from everything around them.

Cards

A grid of links. Used on the cookbook, model overviews and ecosystem pages.

<div class="card-grid">
<a class="card" href="…">
  <div class="card-title">Card title <span class="card-ext">↗</span></div>
  <p class="card-desc">One or two lines describing where this card leads.</p>
</a>
</div>

.cookbook-grid / .cookbook-card are aliases kept for converter output and existing pages. Use .card-grid / .card in anything new.

Ending a page

Two headings, and only these two. Never Summary and next steps, never Next steps.

## Summary is a prose recap of what the reader just did. Optional -- use it on a tutorial that walked through something, skip it on a reference page. No cards.

## What's next is always a .card-grid, always with an icon, and always last. Two to four cards, and every one must point forward -- never back to a section the reader already passed through. Linking across sections is fine; a Get started page may point at Learn.

## What's next

<div class="card-grid">
<a class="card" href="../tuning/index.md">
  <span class="card-icon">{{ icon('lucide/graduation-cap') }}</span>
  <div class="card-title">Fine-tune Gemma</div>
  <p class="card-desc">QLoRA, full fine-tunes and distributed training on your own data.</p>
</a>
</div>

A card pointing off-site takes the ↗ in its title and no icon, so the arrow stays the signal that you are leaving:

<a class="card" href="https://…" target="_blank" rel="noopener" data-model="gemma-4">
  <div class="card-title">Gemma on Vertex AI <span class="card-ext">↗</span></div>
  <p class="card-desc">Use Gemma open models on Google Cloud.</p>
</a>

Note data-model sits on the anchor as a plain attribute. attr_list syntax does not work inside an HTML block, and scripts/pinned_links.py reads both forms.

Which pages get what:

Section Ends with
Get started, Learn What's next on every page
Run, Fine-tune What's next
Models Summary where a recap helps; What's next only where the page genuinely leads on. Model cards get neither -- a spec sheet ends.

Card grids are stripped from notebooks

A grid is site navigation: raw HTML plus {{ icon() }} calls that only the Jinja preprocessor resolves, both of which render as literal text in Jupyter. scripts/notebooks.py drops any card-grid block and the heading above it, so a page registered as a notebook source needs no special handling.

Badges

Inline chips for model attributes. Drive them from data/models.yml so a badge can never disagree with the tables.

12B 256K context Recommended Audio

<span class="badge">12B</span>
<span class="badge badge--accent">Recommended</span>
<span class="badge badge--muted">Audio</span>   <!-- capability this model lacks -->

--muted is how the model switcher (Step 4.2) will show a capability the selected model does not have — struck through rather than hidden, so the reader learns the limitation.

Prompt tokens

Gemma's control tokens are chips, not code. Colour carries the category, so a reader scanning get-started/prompt-formatting.md can tell a turn marker from a tool call without reading it.

<|turn|> system <|image|> <|tool_call|> <|think|>

Class Category Covers
.token-turn Turn structure <|turn|> <turn|> <start_of_turn> <end_of_turn>
.token-role Roles system user model assistant
.token-multi Modality placeholders <|image|> <|audio|>
.token-tool Tool use <|tool|> <|tool_call|> <|tool_response|> <|"|>
.token-think Reasoning <|think|> <|channel>thought <channel|>

Inline, in prose or a table cell, use attr_list on inline code:

Token to indicate a system instruction: `system`{.token-role}
Enable reasoning with `<|think|>`{.token-think}

In an example dialogue, use a <pre><code> block with spans, so the surrounding prompt text stays unstyled and line breaks are preserved. Escape the angle brackets:

<pre><code><span class="token-turn">&lt;|turn&gt;</span><span class="token-role">user</span>
Describe this image: <span class="token-multi">&lt;|image|&gt;</span><span class="token-turn">&lt;turn|&gt;</span></code></pre>

Which renders as:

<|turn>user
Describe this image: <|image|><turn|>

Keep a token in one chip when the model treats it as one unit — <|channel>thought is a single chip, not a chip followed by bare text. Do not use a fenced code block for a dialogue that needs tokens: syntax highlighting and these classes cannot both apply.

Colours live in gemma.css as --gemma-cat-* triplets and are categorical on purpose — they stay distinct from each other rather than being derived from the brand blue.

Page context strip

The quiet band under every H1. You never write this markup; it is rendered from frontmatter by overrides/partials/page-context.html. The strip on this page is the live example.

models: [gemma-4]        # what the page is about
frameworks: [ollama]     # primary tools the page teaches, linked to their home pages
requires: audio          # narrows the selector to models that accept audio
colab: https://colab.research.google.com/github/google-gemma/docs/blob/main/docs/notebooks/tutorial-gemma-4-e2b.ipynb # opens in Colab

Two kinds of item sit on the line, and keeping them apart is the design:

Item Comes from Meaning
Models models frontmatter What the page is about. Links to the local model page.
Uses frameworks frontmatter Primary tools the page teaches. Links out to each project.
Examples the page's own code What you want the commands to target. A preference.
Colab colab frontmatter Direct link to Colab notebook; dynamically updates to target the selected model.

Pairs are separated by a 1px rule, values all share one weight, and labels are muted — 16px of space alone was not enough to stop Frameworks Sentence Transformers reading as a single run of grey text.

Keep frameworks editorial rather than exhaustive. Name the user-facing stack needed to follow the page, not every imported or transitive dependency. For example, a Transformers tutorial that imports PyTorch tensors should say frameworks: [transformers]; add pytorch only when the page actually teaches its APIs or workflow.

Examples is deliberately not derived from models:. Those answer different questions: the Ollama page is about Gemma 4, but its commands could target anything Ollama publishes. So the selector is built from what the page's marked blocks can actually be rewritten to — which means it disappears on pages with nothing switchable rather than claiming a size the page never mentioned.

Swappable code blocks

Add { .model-swap } to a fence and the selector rewrites its model identifiers:

```bash { .model-swap }
ollama pull gemma4:12b
```

Substitution is opt-in per block because a page's code was validated as written. Unmarked blocks are never touched — which is what keeps the tag table on deployment/ollama.md listing every tag while the pull command above it follows the selector.

A model is only offered if it has a published id for every engine appearing in the page's marked blocks, so switching can never rewrite some snippets and silently leave others behind. If a page specifies a requires: modality in frontmatter (such as requires: audio), the picker and exported notebooks are additionally filtered to models supporting that modality. If an intentionally separate example lacks one size—such as a QAT checkpoint—mark it { .model-fixed } and explain the limitation beside it instead of unnecessarily narrowing the page's primary examples.

Identifier variants live under each size's ids mapping in data/models.yml. Use a distinct flavour when syntax has different semantics—for example hf_base for training, hf_qat for a quantization-aware Transformers checkpoint, and gguf_qat for its llama.cpp equivalent. The generator exposes those variants to the switcher; do not encode size substitutions by hand in JavaScript.

The choice is remembered across the site and reflected in the page URL. If a remembered model is unavailable, the examples remain at their authored default and the picker explains why; there is no silent substitution. Use { .model-fixed } to acknowledge an intentionally static block when the same page has swappable examples for that engine.

Exportable notebook cells

Markdown remains the source of truth for tutorials. Add .notebook-cell only to fences that should become executable cells in the generated notebook:

```python { .notebook-cell .model-swap }
model_id = "google/gemma-4-E2B-it"
```

Unmarked fences remain part of a Markdown cell, and { .output } blocks are omitted because a notebook recreates them when it runs.

When a tutorial uses { .model-swap }, scripts/notebooks.py exports separate model-specific notebooks named <page>-gemma-4-<size>.ipynb (e.g., thinking-gemma-4-e2b.ipynb, thinking-gemma-4-12b.ipynb) for every candidate model supported by the page's engines and requires: modality constraint. Tutorials without .model-swap export a single <page>.ipynb.

Add the default model notebook's Colab URL to frontmatter to show a quiet, accessible Colab icon in the page context strip:

colab: https://colab.research.google.com/github/google-gemma/docs/blob/main/docs/notebooks/tutorial-gemma-4-e2b.ipynb

When readers select another model in the page context dropdown, the model switcher dynamically updates the Colab button's destination to match the selected model notebook.

Wrap any other site-only controls in <!-- notebook:skip-start --> and <!-- notebook:skip-end -->. Register the source page in SOURCES in scripts/notebooks.py, then generate every registered notebook with uv run python scripts/notebooks.py; use --check during verification. Do not edit the generated .ipynb directly.

Engine grid

The "run it your way" tiles on the quickstart. A compact relative of .card for when the reader is picking one of a dozen tools rather than reading about one.

Generated from data/frameworks.yml — never hand-write the tiles. An engine appears once it has page, blurb and mark keys; the page chooses which ids to feature and in what order, since that is editorial rather than a fact about the framework.

{% set featured = ["ollama", "llamacpp"] %}
<ul class="engine-grid">
{% for id in featured %}{% for fw in frameworkdata.frameworks if fw.id == id %}
  <li><a class="engine" href="../{{ fw.page }}" title="{{ fw.blurb }}">
    <span class="engine-mark" aria-hidden="true">{{ fw.mark }}</span>
    <span class="engine-name">{{ fw.short_name | default(fw.name) }}</span>
  </a></li>
{% endfor %}{% endfor %}
</ul>

mark is a two- or three-character monogram standing in for a real vendor logo. Replacing .engine-mark's text with an <img> needs no other markup change. Add .engine--all to a trailing tile to make it read as an action rather than another engine.

Add .engine-grid--detailed to show each blurb on the card instead of behind a hover, and wrap the name and blurb in .engine-body. Use it where the grid is the page and the reader is deciding rather than recognising — the Run overview does — and the compact form where the grid is a shortcut past a list of names, as on the quickstart.

An entry with no page is a hosted endpoint: link straight to its url, and mark it up as an external link.

Some external URLs name a generation — a Hugging Face collection, a provider's model page — and go stale when the next one ships. Tag them so they can be found later:

[Hugging Face](https://huggingface.co/collections/google/gemma-4){ data-model="gemma-4" }

In data/frameworks.yml the equivalent is a pinned: key on the entry. Either way:

uv run python scripts/pinned_links.py --model gemma-4

lists every one of them. Tag only genuinely version-pinned URLs — a project's home page is not one, and marking it up would bury the links that do need revisiting.

Two things the name alone will mislead you about. paligemma-2 and shieldgemma-2 are PaliGemma 2 and ShieldGemma 2, not Gemma 2; tag them paligemma and shieldgemma. And pages registered in scripts/notebooks.py are exported to Jupyter, which renders attr_list as literal text — leave those untagged.

Model family tree

The lineage visual on the models overview. Generated from data/models.yml families: generation orders the mainline spine, and based_on attaches each variant to the generation its own model page names as its base.

Interaction is pure CSS — hovering or keyboard-focusing one generation dims the others. The markup underneath is an ordered list of generations each containing a list of descendants, so it still reads correctly with no CSS at all.

<div class="model-tree">
<ol class="tree-spine">
  <li class="tree-gen">
    <a class="tree-node tree-node--gen" href="…">
      <span class="tree-gen-name">Gemma 4</span>
      <span class="tree-gen-note">One line about the generation.</span>
    </a>
    <ul class="tree-branch">
      <li><a class="tree-node" href="…">
        <span class="tree-name">DiffusionGemma</span>
        <span class="tree-tagline">What it is for.</span>
      </a></li>
    </ul>
  </li>
</ol>
<ul class="tree-aside">…</ul>   <!-- families with no Gemma generation as a base -->
</div>

Never add a based_on a model's own page does not state, and never invent a release date to order the tree — the spine already orders it. A family whose page describes a non-Gemma architecture takes based_on: null and renders below the dashed rule.

List containers need extra specificity

The theme styles lists with [dir="ltr"] .md-typeset ol li (0,2,2) and .md-typeset ul:not([hidden]) (0,2,1). A plain .md-typeset .my-grid is only (0,2,0) and silently loses, so display: grid never applies and items keep their 1.25em indent. Qualify the tag — .md-typeset ul.my-grid:not([hidden]) — for any list-based component.

Steps

A numbered walkthrough. Built on an ordered list, so it still reads correctly without CSS.

  1. Install the dependencies Anything can go inside a step, including code blocks.
  2. Run the model The connecting rail is drawn automatically between steps.
<ol class="steps" markdown>
<li markdown>
**Step title**

Body content, including code blocks.
</li>
</ol>

The markdown attribute is required — it tells md_in_html to keep parsing Markdown inside the tag.

Content tabs do not work inside a step

pymdownx.tabbed is a block-level extension and does not run inside an md_in_html block, so === "Label" inside a <li markdown> renders as literal text above an un-tabbed code block. Use labelled fences inside the step, or move the tabs outside the list.

Figures

A framed diagram with a caption.

<figure class="figure" markdown>
![Alt text](../assets/example.png)
<figcaption>What the diagram shows.</figcaption>
</figure>

For a diagram that needs different artwork per theme, mark the pair and let CSS choose:

![Alt text](../assets/diagram-light.png){ .only-light }
![Alt text](../assets/diagram-dark.png){ .only-dark }

Video

Emitted automatically by scripts/importers/convert_notebook.py from a standalone [Video](youtube…) link. Full content width, 16:9, lazy-loaded, privacy-mode host.

<div class="video-embed">
  <iframe src="https://www.youtube.com/embed/VIDEO_ID"
          title="Video" loading="lazy" allowfullscreen></iframe>
</div>

Model-card header

Centres the banner, resource links and License/Authors line on a model card. Also emitted by the converter — you shouldn't need to write it by hand.

<div class="model-card-header" markdown>
![Banner](../assets/banner.png)

[Hugging Face](…) \| [GitHub](…)

**License** : [Apache 2.0](…)
</div>

Admonitions

Standard Zensical admonitions, used throughout. The converter produces these from upstream's > NOTE: blockquotes and > [!NOTE] callouts.

Note

Neutral supporting information.

Tip

A recommendation or shortcut.

Warning

Something that will cost the reader time or money if ignored.

Motion

Shared component transitions live in docs/stylesheets/motion.css, including a blanket prefers-reduced-motion opt-out — anything added later inherits it. Motion is feedback (something is interactive, something changed), never decoration.

The isolated homepage animation also checks that preference before it starts.

To check the reduced-motion path, enable Reduce motion in your OS accessibility settings and reload; hover effects should become instant.

Stylesheet layout

File Owns
gemma.css Tokens. The only file with raw hex.
layout.css Page grid, sidebars, typography scale
nav.css Header, tabs, sidebars, table of contents
content.css Tables, admonitions, code, content tabs
components.css Cards, badges, figures, steps, embeds
engine-grid.css The quickstart's engine tiles
model-tree.css The model family lineage tree
page-context.css The applicability bar above every H1
tokens.css Chat-token badges (<start_of_turn> etc.)
home.css Landing page: chrome, backdrop, hero
motion.css Transitions and the reduced-motion opt-out

Each stays under 250 lines so it can be read end-to-end before editing. Adding a stylesheet for a new concern is the right move; growing one into a catch-all is not.