
Upgrading to Drupal 10: what changed for component architects
Drupal 10 stabilises patterns for component-first theming. Here's what moved and why it matters for how you structure themes.
The first thing I noticed was how the render array differed when I upgraded a theme from Drupal 9 to Drupal 10. Not radically — the shape was familiar — but there were differences in how attributes were handled in the Twig layer, and how components received their data structures. I spent an afternoon debugging what I thought was a regression and discovered it was actually a clarification that had been pending for two releases.
That is the texture of the Drupal 10 upgrade for component-first theming. Not a revolution. A series of refinements that make patterns clearer, constraints more explicit, and inheritance more predictable. For someone building themes where every piece is a component, that matters.
The shift in attribute handling
Drupal 9 passed attributes through the render array, and Twig could shape them. Drupal 10 is more opinionated: the render layer is where attributes live, and Twig’s job is cleaner — apply classes, respect the attributes object, but do not reconstruct it.
This sounds like detail work, and in a three-component theme, it is. Across a full component system where every component has a wrapper and every wrapper has potential attribute customisation, it is the difference between inherited and fragile. A component that mutates attributes in Twig will behave differently when called from different contexts, and debugging that means following the mutation through three layers of Twig and a half-dozen contexts before you see where the assumption breaks.
Drupal 10’s approach — attributes are final once they enter Twig, CSS classes are what you add — means a component’s output is deterministic given its input. A card component always produces a card, not a card that might have different attributes depending on what its parent decided to do.
The practical change is small:
{# Drupal 9 — mutating attributes in Twig #}{% set classes = attributes.addClass('card--highlighted') %}<article {{ attributes }}> {{ content }}</article>
{# Drupal 10 — attributes stay read-only, classes separate #}<article {{ attributes.addClass('card--highlighted') }}> {{ content }}</article>But the implication is large. Once you commit to read-only attributes, a component’s contract becomes: “given these props and this Drupal data, I will always output this structure.” That contract is what makes component libraries scale. Without it, you are building one-off solutions with component syntax.
How preprocess functions work now
Drupal 9 preprocess functions were where the complexity lived. A template would receive props, the preprocess function would transform them (often significantly), and the template itself was relatively simple. This worked fine for one-off templates. For component systems, it meant your preprocess logic could end up scattered — some in the preprocess, some in Twig filters, some in custom functions — with no clear boundary between “prepare the data” and “render it.”
Drupal 10 tightened that boundary. Preprocess functions still exist and still work, but Drupal 10’s patterns suggest they should be thinner. The data transformation should happen at the point where the component is called — in a controller, in a Twig template using Twig filters, in a component definition file if you are using Single Directory Components (SDC).
// Drupal 9 style — preprocess does heavy liftingpublic function preprocessCard(&$variables) { $node = $variables['node']; $variables['title'] = $this->getTitleForCard($node); $variables['excerpt'] = $this->generateExcerpt($node); $variables['image'] = $this->processImage($node->field_image); // ... five more transformations}
// Drupal 10 style — preprocess is thin, logic lives at the call sitepublic function preprocessCard(&$variables) { $variables['title'] = $variables['node']->getTitle(); // That is often all you need.}The benefit is that a component’s expected shape is clearer. You can look at the component file and understand what it actually receives, without chasing preprocess logic through six files. For a team inheriting a theme, that is foundational.
What Single Directory Components gained
Drupal 10 did not introduce SDC — that arrived in 9.3 — but Drupal 10 treated it as the future and changed how schemas work inside SDC files. Props are now typed at the definition level:
$schema: "https://git.drupalcode.org/project/drupal/-/raw/10.0.x/core/modules/sdc/schema.json"name: Carddescription: A card displaying node contentprops: schema: type: object properties: title: type: string excerpt: type: string image: type: object properties: src: { type: string } alt: { type: string }This is a small change in the schema, but it has consequences. A component with a defined prop schema is now self-documenting. You cannot pass arbitrary data. The component knows what it needs, and the tooling (linters, build systems, documentation generators) can work with that contract.
For someone building a multisite platform where five different teams are building on the same component library, that clarity is essential. It is the difference between “this component accepts these props” and “this component accepts whatever you pass to it and hopes you got it right.”
What I haven’t solved yet
The one thing that has not fully settled in Drupal 10 is how deeply components should own their data transformation. A card that receives a node should it transform the node itself (extract the title, handle missing images, truncate the excerpt), or should that transformation happen before the component sees the node? Drupal 10 gives you both options, which is honest and unsettling.
In a single-site context, it does not matter much. In a multisite context where a component is used by five teams with slightly different needs, the answer matters urgently. I am watching how the community settles this. If you have solved it on your own platform — if you have a clear pattern for where data transformation lives and how components express their own constraints — I would genuinely like to see it.
Until then, the Drupal 10 upgrade feels like moving into a house with clearer walls and better lighting, but you are still deciding where the furniture goes.
What this came from
- ArticleDrupal 10: What's New